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