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