jingle.rs

  1// Copyright (c) 2017 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
  2//
  3// This Source Code Form is subject to the terms of the Mozilla Public
  4// License, v. 2.0. If a copy of the MPL was not distributed with this
  5// file, You can obtain one at http://mozilla.org/MPL/2.0/.
  6
  7use crate::error::Error;
  8use crate::iq::IqSetPayload;
  9use crate::ns;
 10use jid::Jid;
 11use minidom::Element;
 12use std::str::FromStr;
 13use try_from::TryFrom;
 14
 15generate_attribute!(
 16    /// The action attribute.
 17    Action, "action", {
 18        /// Accept a content-add action received from another party.
 19        ContentAccept => "content-accept",
 20
 21        /// Add one or more new content definitions to the session.
 22        ContentAdd => "content-add",
 23
 24        /// Change the directionality of media sending.
 25        ContentModify => "content-modify",
 26
 27        /// Reject a content-add action received from another party.
 28        ContentReject => "content-reject",
 29
 30        /// Remove one or more content definitions from the session.
 31        ContentRemove => "content-remove",
 32
 33        /// Exchange information about parameters for an application type.
 34        DescriptionInfo => "description-info",
 35
 36        /// Exchange information about security preconditions.
 37        SecurityInfo => "security-info",
 38
 39        /// Definitively accept a session negotiation.
 40        SessionAccept => "session-accept",
 41
 42        /// Send session-level information, such as a ping or a ringing message.
 43        SessionInfo => "session-info",
 44
 45        /// Request negotiation of a new Jingle session.
 46        SessionInitiate => "session-initiate",
 47
 48        /// End an existing session.
 49        SessionTerminate => "session-terminate",
 50
 51        /// Accept a transport-replace action received from another party.
 52        TransportAccept => "transport-accept",
 53
 54        /// Exchange transport candidates.
 55        TransportInfo => "transport-info",
 56
 57        /// Reject a transport-replace action received from another party.
 58        TransportReject => "transport-reject",
 59
 60        /// Redefine a transport method or replace it with a different method.
 61        TransportReplace => "transport-replace",
 62    }
 63);
 64
 65generate_attribute!(
 66    /// Which party originally generated the content type.
 67    Creator, "creator", {
 68        /// This content was created by the initiator of this session.
 69        Initiator => "initiator",
 70
 71        /// This content was created by the responder of this session.
 72        Responder => "responder",
 73    }
 74);
 75
 76generate_attribute!(
 77    /// Which parties in the session will be generating content.
 78    Senders, "senders", {
 79        /// Both parties can send for this content.
 80        Both => "both",
 81
 82        /// Only the initiator can send for this content.
 83        Initiator => "initiator",
 84
 85        /// No one can send for this content.
 86        None => "none",
 87
 88        /// Only the responder can send for this content.
 89        Responder => "responder",
 90    }, Default = Both
 91);
 92
 93generate_attribute!(
 94    /// How the content definition is to be interpreted by the recipient. The
 95    /// meaning of this attribute matches the "Content-Disposition" header as
 96    /// defined in RFC 2183 and applied to SIP by RFC 3261.
 97    ///
 98    /// Possible values are defined here:
 99    /// https://www.iana.org/assignments/cont-disp/cont-disp.xhtml
100    Disposition, "disposition", {
101        /// Displayed automatically.
102        Inline => "inline",
103
104        /// User controlled display.
105        Attachment => "attachment",
106
107        /// Process as form response.
108        FormData => "form-data",
109
110        /// Tunneled content to be processed silently.
111        Signal => "signal",
112
113        /// The body is a custom ring tone to alert the user.
114        Alert => "alert",
115
116        /// The body is displayed as an icon to the user.
117        Icon => "icon",
118
119        /// The body should be displayed to the user.
120        Render => "render",
121
122        /// The body contains a list of URIs that indicates the recipients of
123        /// the request.
124        RecipientListHistory => "recipient-list-history",
125
126        /// The body describes a communications session, for example, an
127        /// RFC2327 SDP body.
128        Session => "session",
129
130        /// Authenticated Identity Body.
131        Aib => "aib",
132
133        /// The body describes an early communications session, for example,
134        /// and [RFC2327] SDP body.
135        EarlySession => "early-session",
136
137        /// The body includes a list of URIs to which URI-list services are to
138        /// be applied.
139        RecipientList => "recipient-list",
140
141        /// The payload of the message carrying this Content-Disposition header
142        /// field value is an Instant Message Disposition Notification as
143        /// requested in the corresponding Instant Message.
144        Notification => "notification",
145
146        /// The body needs to be handled according to a reference to the body
147        /// that is located in the same SIP message as the body.
148        ByReference => "by-reference",
149
150        /// The body contains information associated with an Info Package.
151        InfoPackage => "info-package",
152
153        /// The body describes either metadata about the RS or the reason for
154        /// the metadata snapshot request as determined by the MIME value
155        /// indicated in the Content-Type.
156        RecordingSession => "recording-session",
157    }, Default = Session
158);
159
160generate_id!(
161    /// An unique identifier in a session, referencing a
162    /// [struct.Content.html](Content element).
163    ContentId
164);
165
166generate_element!(
167    /// Describes a session’s content, there can be multiple content in one
168    /// session.
169    Content, "content", JINGLE,
170    attributes: [
171        /// Who created this content.
172        creator: Creator = "creator" => required,
173
174        /// How the content definition is to be interpreted by the recipient.
175        disposition: Disposition = "disposition" => default,
176
177        /// A per-session unique identifier for this content.
178        name: ContentId = "name" => required,
179
180        /// Who can send data for this content.
181        senders: Senders = "senders" => default
182    ],
183    children: [
184        /// What to send.
185        description: Option<Element> = ("description", JINGLE) => Element,
186
187        /// How to send it.
188        transport: Option<Element> = ("transport", JINGLE) => Element,
189
190        /// With which security.
191        security: Option<Element> = ("security", JINGLE) => Element
192    ]
193);
194
195impl Content {
196    /// Create a new content.
197    pub fn new(creator: Creator, name: ContentId) -> Content {
198        Content {
199            creator,
200            name,
201            disposition: Disposition::Session,
202            senders: Senders::Both,
203            description: None,
204            transport: None,
205            security: None,
206        }
207    }
208
209    /// Set how the content is to be interpreted by the recipient.
210    pub fn with_disposition(mut self, disposition: Disposition) -> Content {
211        self.disposition = disposition;
212        self
213    }
214
215    /// Specify who can send data for this content.
216    pub fn with_senders(mut self, senders: Senders) -> Content {
217        self.senders = senders;
218        self
219    }
220
221    /// Set the description of this content.
222    pub fn with_description(mut self, description: Element) -> Content {
223        self.description = Some(description);
224        self
225    }
226
227    /// Set the transport of this content.
228    pub fn with_transport(mut self, transport: Element) -> Content {
229        self.transport = Some(transport);
230        self
231    }
232
233    /// Set the security of this content.
234    pub fn with_security(mut self, security: Element) -> Content {
235        self.security = Some(security);
236        self
237    }
238}
239
240/// Lists the possible reasons to be included in a Jingle iq.
241#[derive(Debug, Clone, PartialEq)]
242pub enum Reason {
243    /// The party prefers to use an existing session with the peer rather than
244    /// initiate a new session; the Jingle session ID of the alternative
245    /// session SHOULD be provided as the XML character data of the <sid/>
246    /// child.
247    AlternativeSession, //(String),
248
249    /// The party is busy and cannot accept a session.
250    Busy,
251
252    /// The initiator wishes to formally cancel the session initiation request.
253    Cancel,
254
255    /// The action is related to connectivity problems.
256    ConnectivityError,
257
258    /// The party wishes to formally decline the session.
259    Decline,
260
261    /// The session length has exceeded a pre-defined time limit (e.g., a
262    /// meeting hosted at a conference service).
263    Expired,
264
265    /// The party has been unable to initialize processing related to the
266    /// application type.
267    FailedApplication,
268
269    /// The party has been unable to establish connectivity for the transport
270    /// method.
271    FailedTransport,
272
273    /// The action is related to a non-specific application error.
274    GeneralError,
275
276    /// The entity is going offline or is no longer available.
277    Gone,
278
279    /// The party supports the offered application type but does not support
280    /// the offered or negotiated parameters.
281    IncompatibleParameters,
282
283    /// The action is related to media processing problems.
284    MediaError,
285
286    /// The action is related to a violation of local security policies.
287    SecurityError,
288
289    /// The action is generated during the normal course of state management
290    /// and does not reflect any error.
291    Success,
292
293    /// A request has not been answered so the sender is timing out the
294    /// request.
295    Timeout,
296
297    /// The party supports none of the offered application types.
298    UnsupportedApplications,
299
300    /// The party supports none of the offered transport methods.
301    UnsupportedTransports,
302}
303
304impl FromStr for Reason {
305    type Err = Error;
306
307    fn from_str(s: &str) -> Result<Reason, Error> {
308        Ok(match s {
309            "alternative-session" => Reason::AlternativeSession,
310            "busy" => Reason::Busy,
311            "cancel" => Reason::Cancel,
312            "connectivity-error" => Reason::ConnectivityError,
313            "decline" => Reason::Decline,
314            "expired" => Reason::Expired,
315            "failed-application" => Reason::FailedApplication,
316            "failed-transport" => Reason::FailedTransport,
317            "general-error" => Reason::GeneralError,
318            "gone" => Reason::Gone,
319            "incompatible-parameters" => Reason::IncompatibleParameters,
320            "media-error" => Reason::MediaError,
321            "security-error" => Reason::SecurityError,
322            "success" => Reason::Success,
323            "timeout" => Reason::Timeout,
324            "unsupported-applications" => Reason::UnsupportedApplications,
325            "unsupported-transports" => Reason::UnsupportedTransports,
326
327            _ => return Err(Error::ParseError("Unknown reason.")),
328        })
329    }
330}
331
332impl From<Reason> for Element {
333    fn from(reason: Reason) -> Element {
334        Element::builder(match reason {
335            Reason::AlternativeSession => "alternative-session",
336            Reason::Busy => "busy",
337            Reason::Cancel => "cancel",
338            Reason::ConnectivityError => "connectivity-error",
339            Reason::Decline => "decline",
340            Reason::Expired => "expired",
341            Reason::FailedApplication => "failed-application",
342            Reason::FailedTransport => "failed-transport",
343            Reason::GeneralError => "general-error",
344            Reason::Gone => "gone",
345            Reason::IncompatibleParameters => "incompatible-parameters",
346            Reason::MediaError => "media-error",
347            Reason::SecurityError => "security-error",
348            Reason::Success => "success",
349            Reason::Timeout => "timeout",
350            Reason::UnsupportedApplications => "unsupported-applications",
351            Reason::UnsupportedTransports => "unsupported-transports",
352        })
353        .build()
354    }
355}
356
357/// Informs the recipient of something.
358#[derive(Debug, Clone)]
359pub struct ReasonElement {
360    /// The list of possible reasons to be included in a Jingle iq.
361    pub reason: Reason,
362
363    /// A human-readable description of this reason.
364    pub text: Option<String>,
365}
366
367impl TryFrom<Element> for ReasonElement {
368    type Err = Error;
369
370    fn try_from(elem: Element) -> Result<ReasonElement, Error> {
371        check_self!(elem, "reason", JINGLE);
372        let mut reason = None;
373        let mut text = None;
374        for child in elem.children() {
375            if !child.has_ns(ns::JINGLE) {
376                return Err(Error::ParseError("Reason contains a foreign element."));
377            }
378            match child.name() {
379                "text" => {
380                    if text.is_some() {
381                        return Err(Error::ParseError(
382                            "Reason must not have more than one text.",
383                        ));
384                    }
385                    text = Some(child.text());
386                }
387                name => {
388                    if reason.is_some() {
389                        return Err(Error::ParseError(
390                            "Reason must not have more than one reason.",
391                        ));
392                    }
393                    reason = Some(name.parse()?);
394                }
395            }
396        }
397        let reason = reason.ok_or(Error::ParseError(
398            "Reason doesn’t contain a valid reason.",
399        ))?;
400        Ok(ReasonElement {
401            reason: reason,
402            text: text,
403        })
404    }
405}
406
407impl From<ReasonElement> for Element {
408    fn from(reason: ReasonElement) -> Element {
409        Element::builder("reason")
410            .append(Element::from(reason.reason))
411            .append(reason.text)
412            .build()
413    }
414}
415
416generate_id!(
417    /// Unique identifier for a session between two JIDs.
418    SessionId
419);
420
421/// The main Jingle container, to be included in an iq stanza.
422#[derive(Debug, Clone)]
423pub struct Jingle {
424    /// The action to execute on both ends.
425    pub action: Action,
426
427    /// Who the initiator is.
428    pub initiator: Option<Jid>,
429
430    /// Who the responder is.
431    pub responder: Option<Jid>,
432
433    /// Unique session identifier between two entities.
434    pub sid: SessionId,
435
436    /// A list of contents to be negociated in this session.
437    pub contents: Vec<Content>,
438
439    /// An optional reason.
440    pub reason: Option<ReasonElement>,
441
442    /// Payloads to be included.
443    pub other: Vec<Element>,
444}
445
446impl IqSetPayload for Jingle {}
447
448impl Jingle {
449    /// Create a new Jingle element.
450    pub fn new(action: Action, sid: SessionId) -> Jingle {
451        Jingle {
452            action: action,
453            sid: sid,
454            initiator: None,
455            responder: None,
456            contents: Vec::new(),
457            reason: None,
458            other: Vec::new(),
459        }
460    }
461
462    /// Set the initiator’s JID.
463    pub fn with_initiator(mut self, initiator: Jid) -> Jingle {
464        self.initiator = Some(initiator);
465        self
466    }
467
468    /// Set the responder’s JID.
469    pub fn with_responder(mut self, responder: Jid) -> Jingle {
470        self.responder = Some(responder);
471        self
472    }
473
474    /// Add a content to this Jingle container.
475    pub fn add_content(mut self, content: Content) -> Jingle {
476        self.contents.push(content);
477        self
478    }
479
480    /// Set the reason in this Jingle container.
481    pub fn set_reason(mut self, content: Content) -> Jingle {
482        self.contents.push(content);
483        self
484    }
485}
486
487impl TryFrom<Element> for Jingle {
488    type Err = Error;
489
490    fn try_from(root: Element) -> Result<Jingle, Error> {
491        check_self!(root, "jingle", JINGLE, "Jingle");
492        check_no_unknown_attributes!(root, "Jingle", ["action", "initiator", "responder", "sid"]);
493
494        let mut jingle = Jingle {
495            action: get_attr!(root, "action", required),
496            initiator: get_attr!(root, "initiator", optional),
497            responder: get_attr!(root, "responder", optional),
498            sid: get_attr!(root, "sid", required),
499            contents: vec![],
500            reason: None,
501            other: vec![],
502        };
503
504        for child in root.children().cloned() {
505            if child.is("content", ns::JINGLE) {
506                let content = Content::try_from(child)?;
507                jingle.contents.push(content);
508            } else if child.is("reason", ns::JINGLE) {
509                if jingle.reason.is_some() {
510                    return Err(Error::ParseError(
511                        "Jingle must not have more than one reason.",
512                    ));
513                }
514                let reason = ReasonElement::try_from(child)?;
515                jingle.reason = Some(reason);
516            } else {
517                jingle.other.push(child);
518            }
519        }
520
521        Ok(jingle)
522    }
523}
524
525impl From<Jingle> for Element {
526    fn from(jingle: Jingle) -> Element {
527        Element::builder("jingle")
528            .ns(ns::JINGLE)
529            .attr("action", jingle.action)
530            .attr("initiator", jingle.initiator)
531            .attr("responder", jingle.responder)
532            .attr("sid", jingle.sid)
533            .append(jingle.contents)
534            .append(jingle.reason)
535            .build()
536    }
537}
538
539#[cfg(test)]
540mod tests {
541    use super::*;
542
543    #[cfg(target_pointer_width = "32")]
544    #[test]
545    fn test_size() {
546        assert_size!(Action, 1);
547        assert_size!(Creator, 1);
548        assert_size!(Senders, 1);
549        assert_size!(Disposition, 1);
550        assert_size!(ContentId, 12);
551        assert_size!(Content, 172);
552        assert_size!(Reason, 1);
553        assert_size!(ReasonElement, 16);
554        assert_size!(SessionId, 12);
555        assert_size!(Jingle, 128);
556    }
557
558    #[cfg(target_pointer_width = "64")]
559    #[test]
560    fn test_size() {
561        assert_size!(Action, 1);
562        assert_size!(Creator, 1);
563        assert_size!(Senders, 1);
564        assert_size!(Disposition, 1);
565        assert_size!(ContentId, 24);
566        assert_size!(Content, 344);
567        assert_size!(Reason, 1);
568        assert_size!(ReasonElement, 32);
569        assert_size!(SessionId, 24);
570        assert_size!(Jingle, 256);
571    }
572
573    #[test]
574    fn test_simple() {
575        let elem: Element =
576            "<jingle xmlns='urn:xmpp:jingle:1' action='session-initiate' sid='coucou'/>"
577                .parse()
578                .unwrap();
579        let jingle = Jingle::try_from(elem).unwrap();
580        assert_eq!(jingle.action, Action::SessionInitiate);
581        assert_eq!(jingle.sid, SessionId(String::from("coucou")));
582    }
583
584    #[test]
585    fn test_invalid_jingle() {
586        let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1'/>".parse().unwrap();
587        let error = Jingle::try_from(elem).unwrap_err();
588        let message = match error {
589            Error::ParseError(string) => string,
590            _ => panic!(),
591        };
592        assert_eq!(message, "Required attribute 'action' missing.");
593
594        let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='session-info'/>"
595            .parse()
596            .unwrap();
597        let error = Jingle::try_from(elem).unwrap_err();
598        let message = match error {
599            Error::ParseError(string) => string,
600            _ => panic!(),
601        };
602        assert_eq!(message, "Required attribute 'sid' missing.");
603
604        let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='coucou' sid='coucou'/>"
605            .parse()
606            .unwrap();
607        let error = Jingle::try_from(elem).unwrap_err();
608        let message = match error {
609            Error::ParseError(string) => string,
610            _ => panic!(),
611        };
612        assert_eq!(message, "Unknown value for 'action' attribute.");
613    }
614
615    #[test]
616    fn test_content() {
617        let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='session-initiate' sid='coucou'><content creator='initiator' name='coucou'><description/><transport/></content></jingle>".parse().unwrap();
618        let jingle = Jingle::try_from(elem).unwrap();
619        assert_eq!(jingle.contents[0].creator, Creator::Initiator);
620        assert_eq!(jingle.contents[0].name, ContentId(String::from("coucou")));
621        assert_eq!(jingle.contents[0].senders, Senders::Both);
622        assert_eq!(jingle.contents[0].disposition, Disposition::Session);
623
624        let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='session-initiate' sid='coucou'><content creator='initiator' name='coucou' senders='both'><description/><transport/></content></jingle>".parse().unwrap();
625        let jingle = Jingle::try_from(elem).unwrap();
626        assert_eq!(jingle.contents[0].senders, Senders::Both);
627
628        let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='session-initiate' sid='coucou'><content creator='initiator' name='coucou' disposition='early-session'><description/><transport/></content></jingle>".parse().unwrap();
629        let jingle = Jingle::try_from(elem).unwrap();
630        assert_eq!(jingle.contents[0].disposition, Disposition::EarlySession);
631    }
632
633    #[test]
634    fn test_invalid_content() {
635        let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='session-initiate' sid='coucou'><content/></jingle>".parse().unwrap();
636        let error = Jingle::try_from(elem).unwrap_err();
637        let message = match error {
638            Error::ParseError(string) => string,
639            _ => panic!(),
640        };
641        assert_eq!(message, "Required attribute 'creator' missing.");
642
643        let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='session-initiate' sid='coucou'><content creator='initiator'/></jingle>".parse().unwrap();
644        let error = Jingle::try_from(elem).unwrap_err();
645        let message = match error {
646            Error::ParseError(string) => string,
647            _ => panic!(),
648        };
649        assert_eq!(message, "Required attribute 'name' missing.");
650
651        let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='session-initiate' sid='coucou'><content creator='coucou' name='coucou'/></jingle>".parse().unwrap();
652        let error = Jingle::try_from(elem).unwrap_err();
653        let message = match error {
654            Error::ParseError(string) => string,
655            _ => panic!(),
656        };
657        assert_eq!(message, "Unknown value for 'creator' attribute.");
658
659        let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='session-initiate' sid='coucou'><content creator='initiator' name='coucou' senders='coucou'/></jingle>".parse().unwrap();
660        let error = Jingle::try_from(elem).unwrap_err();
661        let message = match error {
662            Error::ParseError(string) => string,
663            _ => panic!(),
664        };
665        assert_eq!(message, "Unknown value for 'senders' attribute.");
666
667        let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='session-initiate' sid='coucou'><content creator='initiator' name='coucou' senders=''/></jingle>".parse().unwrap();
668        let error = Jingle::try_from(elem).unwrap_err();
669        let message = match error {
670            Error::ParseError(string) => string,
671            _ => panic!(),
672        };
673        assert_eq!(message, "Unknown value for 'senders' attribute.");
674    }
675
676    #[test]
677    fn test_reason() {
678        let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='session-initiate' sid='coucou'><reason><success/></reason></jingle>".parse().unwrap();
679        let jingle = Jingle::try_from(elem).unwrap();
680        let reason = jingle.reason.unwrap();
681        assert_eq!(reason.reason, Reason::Success);
682        assert_eq!(reason.text, None);
683
684        let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='session-initiate' sid='coucou'><reason><success/><text>coucou</text></reason></jingle>".parse().unwrap();
685        let jingle = Jingle::try_from(elem).unwrap();
686        let reason = jingle.reason.unwrap();
687        assert_eq!(reason.reason, Reason::Success);
688        assert_eq!(reason.text, Some(String::from("coucou")));
689    }
690
691    #[test]
692    fn test_invalid_reason() {
693        let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='session-initiate' sid='coucou'><reason/></jingle>".parse().unwrap();
694        let error = Jingle::try_from(elem).unwrap_err();
695        let message = match error {
696            Error::ParseError(string) => string,
697            _ => panic!(),
698        };
699        assert_eq!(message, "Reason doesn’t contain a valid reason.");
700
701        let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='session-initiate' sid='coucou'><reason><a/></reason></jingle>".parse().unwrap();
702        let error = Jingle::try_from(elem).unwrap_err();
703        let message = match error {
704            Error::ParseError(string) => string,
705            _ => panic!(),
706        };
707        assert_eq!(message, "Unknown reason.");
708
709        let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='session-initiate' sid='coucou'><reason><a xmlns='http://www.w3.org/1999/xhtml'/></reason></jingle>".parse().unwrap();
710        let error = Jingle::try_from(elem).unwrap_err();
711        let message = match error {
712            Error::ParseError(string) => string,
713            _ => panic!(),
714        };
715        assert_eq!(message, "Reason contains a foreign element.");
716
717        let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='session-initiate' sid='coucou'><reason><decline/></reason><reason/></jingle>".parse().unwrap();
718        let error = Jingle::try_from(elem).unwrap_err();
719        let message = match error {
720            Error::ParseError(string) => string,
721            _ => panic!(),
722        };
723        assert_eq!(message, "Jingle must not have more than one reason.");
724
725        let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='session-initiate' sid='coucou'><reason><decline/><text/><text/></reason></jingle>".parse().unwrap();
726        let error = Jingle::try_from(elem).unwrap_err();
727        let message = match error {
728            Error::ParseError(string) => string,
729            _ => panic!(),
730        };
731        assert_eq!(message, "Reason must not have more than one text.");
732    }
733}