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 xso::{AsXml, FromXml};
8
9use crate::iq::IqSetPayload;
10use crate::jingle_grouping::Group;
11use crate::jingle_ibb::Transport as IbbTransport;
12use crate::jingle_ice_udp::Transport as IceUdpTransport;
13use crate::jingle_rtp::Description as RtpDescription;
14use crate::jingle_s5b::Transport as Socks5Transport;
15use crate::ns;
16use alloc::{collections::BTreeMap, fmt};
17use jid::Jid;
18use minidom::Element;
19use xso::error::Error;
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(AsXml, Debug, Clone, PartialEq)]
174#[xml()]
175pub enum Description {
176    /// Jingle RTP Sessions (XEP-0167) description.
177    #[xml(transparent)]
178    Rtp(RtpDescription),
179
180    /// To be used for any description that isn’t known at compile-time.
181    // TODO: replace with `#[xml(element, name = ..)]` once we have it.
182    #[xml(transparent)]
183    Unknown(Element),
184}
185
186impl TryFrom<Element> for Description {
187    type Error = Error;
188
189    fn try_from(elem: Element) -> Result<Description, Error> {
190        Ok(if elem.is("description", ns::JINGLE_RTP) {
191            Description::Rtp(RtpDescription::try_from(elem)?)
192        } else if elem.name() == "description" {
193            Description::Unknown(elem)
194        } else {
195            return Err(Error::Other("Invalid description."));
196        })
197    }
198}
199
200impl ::xso::FromXml for Description {
201    type Builder = ::xso::minidom_compat::FromEventsViaElement<Description>;
202
203    fn from_events(
204        qname: ::xso::exports::rxml::QName,
205        attrs: ::xso::exports::rxml::AttrMap,
206        _ctx: &::xso::Context<'_>,
207    ) -> Result<Self::Builder, ::xso::error::FromEventsError> {
208        if qname.1 != "description" {
209            return Err(::xso::error::FromEventsError::Mismatch { name: qname, attrs });
210        }
211        Self::Builder::new(qname, attrs)
212    }
213}
214
215impl From<RtpDescription> for Description {
216    fn from(desc: RtpDescription) -> Description {
217        Description::Rtp(desc)
218    }
219}
220
221/// Enum wrapping all of the various supported transports of a Content.
222#[derive(AsXml, Debug, Clone, PartialEq)]
223#[xml()]
224pub enum Transport {
225    /// Jingle ICE-UDP Bytestreams (XEP-0176) transport.
226    #[xml(transparent)]
227    IceUdp(IceUdpTransport),
228
229    /// Jingle In-Band Bytestreams (XEP-0261) transport.
230    #[xml(transparent)]
231    Ibb(IbbTransport),
232
233    /// Jingle SOCKS5 Bytestreams (XEP-0260) transport.
234    #[xml(transparent)]
235    Socks5(Socks5Transport),
236
237    /// To be used for any transport that isn’t known at compile-time.
238    // TODO: replace with `#[xml(element, name = ..)]` once we have it.
239    #[xml(transparent)]
240    Unknown(Element),
241}
242
243impl TryFrom<Element> for Transport {
244    type Error = Error;
245
246    fn try_from(elem: Element) -> Result<Transport, Error> {
247        Ok(if elem.is("transport", ns::JINGLE_ICE_UDP) {
248            Transport::IceUdp(IceUdpTransport::try_from(elem)?)
249        } else if elem.is("transport", ns::JINGLE_IBB) {
250            Transport::Ibb(IbbTransport::try_from(elem)?)
251        } else if elem.is("transport", ns::JINGLE_S5B) {
252            Transport::Socks5(Socks5Transport::try_from(elem)?)
253        } else if elem.name() == "transport" {
254            Transport::Unknown(elem)
255        } else {
256            return Err(Error::Other("Invalid transport."));
257        })
258    }
259}
260
261impl ::xso::FromXml for Transport {
262    type Builder = ::xso::minidom_compat::FromEventsViaElement<Transport>;
263
264    fn from_events(
265        qname: ::xso::exports::rxml::QName,
266        attrs: ::xso::exports::rxml::AttrMap,
267        _ctx: &::xso::Context<'_>,
268    ) -> Result<Self::Builder, ::xso::error::FromEventsError> {
269        if qname.1 != "transport" {
270            return Err(::xso::error::FromEventsError::Mismatch { name: qname, attrs });
271        }
272        Self::Builder::new(qname, attrs)
273    }
274}
275
276impl From<IceUdpTransport> for Transport {
277    fn from(transport: IceUdpTransport) -> Transport {
278        Transport::IceUdp(transport)
279    }
280}
281
282impl From<IbbTransport> for Transport {
283    fn from(transport: IbbTransport) -> Transport {
284        Transport::Ibb(transport)
285    }
286}
287
288impl From<Socks5Transport> for Transport {
289    fn from(transport: Socks5Transport) -> Transport {
290        Transport::Socks5(transport)
291    }
292}
293
294/// A security element inside a Jingle content, stubbed for now.
295#[derive(FromXml, AsXml, Debug, Clone, PartialEq)]
296#[xml(namespace = ns::JINGLE, name = "security")]
297pub struct Security;
298
299/// Describes a session’s content, there can be multiple content in one
300/// session.
301#[derive(FromXml, AsXml, Debug, Clone, PartialEq)]
302#[xml(namespace = ns::JINGLE, name = "content")]
303pub struct Content {
304    /// Who created this content.
305    #[xml(attribute)]
306    pub creator: Creator,
307
308    /// How the content definition is to be interpreted by the recipient.
309    #[xml(attribute(default))]
310    pub disposition: Disposition,
311
312    /// A per-session unique identifier for this content.
313    #[xml(attribute)]
314    pub name: ContentId,
315
316    /// Who can send data for this content.
317    #[xml(attribute(default))]
318    pub senders: Senders,
319
320    /// What to send.
321    #[xml(child(default))]
322    pub description: Option<Description>,
323
324    /// How to send it.
325    #[xml(child(default))]
326    pub transport: Option<Transport>,
327
328    /// With which security.
329    #[xml(child(default))]
330    pub security: Option<Security>,
331}
332
333impl Content {
334    /// Create a new content.
335    pub fn new(creator: Creator, name: ContentId) -> Content {
336        Content {
337            creator,
338            name,
339            disposition: Disposition::Session,
340            senders: Senders::Both,
341            description: None,
342            transport: None,
343            security: None,
344        }
345    }
346
347    /// Set how the content is to be interpreted by the recipient.
348    pub fn with_disposition(mut self, disposition: Disposition) -> Content {
349        self.disposition = disposition;
350        self
351    }
352
353    /// Specify who can send data for this content.
354    pub fn with_senders(mut self, senders: Senders) -> Content {
355        self.senders = senders;
356        self
357    }
358
359    /// Set the description of this content.
360    pub fn with_description<D: Into<Description>>(mut self, description: D) -> Content {
361        self.description = Some(description.into());
362        self
363    }
364
365    /// Set the transport of this content.
366    pub fn with_transport<T: Into<Transport>>(mut self, transport: T) -> Content {
367        self.transport = Some(transport.into());
368        self
369    }
370
371    /// Set the security of this content.
372    pub fn with_security(mut self, security: Security) -> Content {
373        self.security = Some(security);
374        self
375    }
376}
377
378/// Lists the possible reasons to be included in a Jingle iq.
379#[derive(FromXml, AsXml, Debug, Clone, PartialEq)]
380#[xml(namespace = ns::JINGLE)]
381pub enum Reason {
382    /// The party prefers to use an existing session with the peer rather than
383    /// initiate a new session; the Jingle session ID of the alternative
384    /// session SHOULD be provided as the XML character data of the \<sid/\>
385    /// child.
386    #[xml(name = "alternative-session")]
387    AlternativeSession {
388        /// Session ID of the alternative session.
389        #[xml(extract(namespace = ns::JINGLE, name = "sid", default, fields(text(type_ = String))))]
390        sid: Option<String>,
391    },
392
393    /// The party is busy and cannot accept a session.
394    #[xml(name = "busy")]
395    Busy,
396
397    /// The initiator wishes to formally cancel the session initiation request.
398    #[xml(name = "cancel")]
399    Cancel,
400
401    /// The action is related to connectivity problems.
402    #[xml(name = "connectivity-error")]
403    ConnectivityError,
404
405    /// The party wishes to formally decline the session.
406    #[xml(name = "decline")]
407    Decline,
408
409    /// The session length has exceeded a pre-defined time limit (e.g., a
410    /// meeting hosted at a conference service).
411    #[xml(name = "expired")]
412    Expired,
413
414    /// The party has been unable to initialize processing related to the
415    /// application type.
416    #[xml(name = "failed-application")]
417    FailedApplication,
418
419    /// The party has been unable to establish connectivity for the transport
420    /// method.
421    #[xml(name = "failed-transport")]
422    FailedTransport,
423
424    /// The action is related to a non-specific application error.
425    #[xml(name = "general-error")]
426    GeneralError,
427
428    /// The entity is going offline or is no longer available.
429    #[xml(name = "gone")]
430    Gone,
431
432    /// The party supports the offered application type but does not support
433    /// the offered or negotiated parameters.
434    #[xml(name = "incompatible-parameters")]
435    IncompatibleParameters,
436
437    /// The action is related to media processing problems.
438    #[xml(name = "media-error")]
439    MediaError,
440
441    /// The action is related to a violation of local security policies.
442    #[xml(name = "security-error")]
443    SecurityError,
444
445    /// The action is generated during the normal course of state management
446    /// and does not reflect any error.
447    #[xml(name = "success")]
448    Success,
449
450    /// A request has not been answered so the sender is timing out the
451    /// request.
452    #[xml(name = "timeout")]
453    Timeout,
454
455    /// The party supports none of the offered application types.
456    #[xml(name = "unsupported-applications")]
457    UnsupportedApplications,
458
459    /// The party supports none of the offered transport methods.
460    #[xml(name = "unsupported-transports")]
461    UnsupportedTransports,
462}
463
464type Lang = String;
465
466/// Informs the recipient of something.
467#[derive(FromXml, AsXml, Debug, Clone, PartialEq)]
468#[xml(namespace = ns::JINGLE, name = "reason")]
469pub struct ReasonElement {
470    /// The list of possible reasons to be included in a Jingle iq.
471    #[xml(child)]
472    pub reason: Reason,
473
474    /// A human-readable description of this reason.
475    #[xml(extract(n = .., namespace = ns::JINGLE, name = "text", fields(
476        attribute(type_ = String, name = "xml:lang", default),
477        text(type_ = String),
478    )))]
479    pub texts: BTreeMap<Lang, String>,
480}
481
482impl fmt::Display for ReasonElement {
483    fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
484        write!(fmt, "{}", Element::from(self.reason.clone()).name())?;
485        if let Some(text) = self.texts.get("en") {
486            write!(fmt, ": {}", text)?;
487        } else if let Some(text) = self.texts.get("") {
488            write!(fmt, ": {}", text)?;
489        }
490        Ok(())
491    }
492}
493
494generate_id!(
495    /// Unique identifier for a session between two JIDs.
496    SessionId
497);
498
499/// The main Jingle container, to be included in an iq stanza.
500#[derive(FromXml, AsXml, Debug, Clone, PartialEq)]
501#[xml(namespace = ns::JINGLE, name = "jingle")]
502pub struct Jingle {
503    /// The action to execute on both ends.
504    #[xml(attribute)]
505    pub action: Action,
506
507    /// Who the initiator is.
508    #[xml(attribute(default))]
509    pub initiator: Option<Jid>,
510
511    /// Who the responder is.
512    #[xml(attribute(default))]
513    pub responder: Option<Jid>,
514
515    /// Unique session identifier between two entities.
516    #[xml(attribute)]
517    pub sid: SessionId,
518
519    /// A list of contents to be negotiated in this session.
520    #[xml(child(n = ..))]
521    pub contents: Vec<Content>,
522
523    /// An optional reason.
524    #[xml(child(default))]
525    pub reason: Option<ReasonElement>,
526
527    /// An optional grouping.
528    #[xml(child(default))]
529    pub group: Option<Group>,
530
531    /// Payloads to be included.
532    #[xml(child(n = ..))]
533    pub other: Vec<Element>,
534}
535
536impl IqSetPayload for Jingle {}
537
538impl Jingle {
539    /// Create a new Jingle element.
540    pub fn new(action: Action, sid: SessionId) -> Jingle {
541        Jingle {
542            action,
543            sid,
544            initiator: None,
545            responder: None,
546            contents: Vec::new(),
547            reason: None,
548            group: None,
549            other: Vec::new(),
550        }
551    }
552
553    /// Set the initiator’s JID.
554    pub fn with_initiator(mut self, initiator: Jid) -> Jingle {
555        self.initiator = Some(initiator);
556        self
557    }
558
559    /// Set the responder’s JID.
560    pub fn with_responder(mut self, responder: Jid) -> Jingle {
561        self.responder = Some(responder);
562        self
563    }
564
565    /// Add a content to this Jingle container.
566    pub fn add_content(mut self, content: Content) -> Jingle {
567        self.contents.push(content);
568        self
569    }
570
571    /// Set the reason in this Jingle container.
572    pub fn set_reason(mut self, reason: ReasonElement) -> Jingle {
573        self.reason = Some(reason);
574        self
575    }
576
577    /// Set the grouping in this Jingle container.
578    pub fn set_group(mut self, group: Group) -> Jingle {
579        self.group = Some(group);
580        self
581    }
582}
583
584#[cfg(test)]
585mod tests {
586    use super::*;
587    use xso::error::FromElementError;
588
589    #[cfg(target_pointer_width = "32")]
590    #[test]
591    fn test_size() {
592        assert_size!(Action, 1);
593        assert_size!(Creator, 1);
594        assert_size!(Senders, 1);
595        assert_size!(Disposition, 1);
596        assert_size!(ContentId, 12);
597        assert_size!(Content, 156);
598        assert_size!(Reason, 12);
599        assert_size!(ReasonElement, 24);
600        assert_size!(SessionId, 12);
601        assert_size!(Jingle, 112);
602    }
603
604    #[cfg(target_pointer_width = "64")]
605    #[test]
606    fn test_size() {
607        assert_size!(Action, 1);
608        assert_size!(Creator, 1);
609        assert_size!(Senders, 1);
610        assert_size!(Disposition, 1);
611        assert_size!(ContentId, 24);
612        assert_size!(Content, 312);
613        assert_size!(Reason, 24);
614        assert_size!(ReasonElement, 48);
615        assert_size!(SessionId, 24);
616        assert_size!(Jingle, 224);
617    }
618
619    #[test]
620    fn test_simple() {
621        let elem: Element =
622            "<jingle xmlns='urn:xmpp:jingle:1' action='session-initiate' sid='coucou'/>"
623                .parse()
624                .unwrap();
625        let jingle = Jingle::try_from(elem).unwrap();
626        assert_eq!(jingle.action, Action::SessionInitiate);
627        assert_eq!(jingle.sid, SessionId(String::from("coucou")));
628    }
629
630    #[test]
631    fn test_invalid_jingle() {
632        let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1'/>".parse().unwrap();
633        let error = Jingle::try_from(elem).unwrap_err();
634        let message = match error {
635            FromElementError::Invalid(Error::Other(string)) => string,
636            _ => panic!(),
637        };
638        assert_eq!(
639            message,
640            "Required attribute field 'action' on Jingle element missing."
641        );
642
643        let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='session-info'/>"
644            .parse()
645            .unwrap();
646        let error = Jingle::try_from(elem).unwrap_err();
647        let message = match error {
648            FromElementError::Invalid(Error::Other(string)) => string,
649            _ => panic!(),
650        };
651        assert_eq!(
652            message,
653            "Required attribute field 'sid' on Jingle element missing."
654        );
655
656        let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='coucou' sid='coucou'/>"
657            .parse()
658            .unwrap();
659        let error = Jingle::try_from(elem).unwrap_err();
660        let message = match error {
661            FromElementError::Invalid(Error::TextParseError(string)) => string,
662            _ => panic!(),
663        };
664        assert_eq!(message.to_string(), "Unknown value for 'action' attribute.");
665    }
666
667    #[test]
668    fn test_content() {
669        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();
670        let jingle = Jingle::try_from(elem).unwrap();
671        assert_eq!(jingle.contents[0].creator, Creator::Initiator);
672        assert_eq!(jingle.contents[0].name, ContentId(String::from("coucou")));
673        assert_eq!(jingle.contents[0].senders, Senders::Both);
674        assert_eq!(jingle.contents[0].disposition, Disposition::Session);
675
676        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();
677        let jingle = Jingle::try_from(elem).unwrap();
678        assert_eq!(jingle.contents[0].senders, Senders::Both);
679
680        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();
681        let jingle = Jingle::try_from(elem).unwrap();
682        assert_eq!(jingle.contents[0].disposition, Disposition::EarlySession);
683    }
684
685    #[test]
686    fn test_invalid_content() {
687        let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='session-initiate' sid='coucou'><content/></jingle>".parse().unwrap();
688        let error = Jingle::try_from(elem).unwrap_err();
689        let message = match error {
690            FromElementError::Invalid(Error::Other(string)) => string,
691            _ => panic!(),
692        };
693        assert_eq!(
694            message,
695            "Required attribute field 'creator' on Content element missing."
696        );
697
698        let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='session-initiate' sid='coucou'><content creator='initiator'/></jingle>".parse().unwrap();
699        let error = Jingle::try_from(elem).unwrap_err();
700        let message = match error {
701            FromElementError::Invalid(Error::Other(string)) => string,
702            _ => panic!(),
703        };
704        assert_eq!(
705            message,
706            "Required attribute field 'name' on Content element missing."
707        );
708
709        let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='session-initiate' sid='coucou'><content creator='coucou' name='coucou'/></jingle>".parse().unwrap();
710        let error = Jingle::try_from(elem).unwrap_err();
711        let message = match error {
712            FromElementError::Invalid(Error::TextParseError(string)) => string,
713            other => panic!("unexpected result: {:?}", other),
714        };
715        assert_eq!(
716            message.to_string(),
717            "Unknown value for 'creator' attribute."
718        );
719
720        let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='session-initiate' sid='coucou'><content creator='initiator' name='coucou' senders='coucou'/></jingle>".parse().unwrap();
721        let error = Jingle::try_from(elem).unwrap_err();
722        let message = match error {
723            FromElementError::Invalid(Error::TextParseError(string)) => string,
724            _ => panic!(),
725        };
726        assert_eq!(
727            message.to_string(),
728            "Unknown value for 'senders' attribute."
729        );
730
731        let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='session-initiate' sid='coucou'><content creator='initiator' name='coucou' senders=''/></jingle>".parse().unwrap();
732        let error = Jingle::try_from(elem).unwrap_err();
733        let message = match error {
734            FromElementError::Invalid(Error::TextParseError(string)) => string,
735            _ => panic!(),
736        };
737        assert_eq!(
738            message.to_string(),
739            "Unknown value for 'senders' attribute."
740        );
741    }
742
743    #[test]
744    fn test_reason() {
745        let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='session-initiate' sid='coucou'><reason><success/></reason></jingle>".parse().unwrap();
746        let jingle = Jingle::try_from(elem).unwrap();
747        let reason = jingle.reason.unwrap();
748        assert_eq!(reason.reason, Reason::Success);
749        assert_eq!(reason.texts, BTreeMap::new());
750
751        let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='session-initiate' sid='coucou'><reason><success/><text>coucou</text></reason></jingle>".parse().unwrap();
752        let jingle = Jingle::try_from(elem).unwrap();
753        let reason = jingle.reason.unwrap();
754        assert_eq!(reason.reason, Reason::Success);
755        assert_eq!(reason.texts.get(""), Some(&String::from("coucou")));
756    }
757
758    #[test]
759    fn test_missing_reason_text() {
760        let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='session-initiate' sid='coucou'><reason/></jingle>".parse().unwrap();
761        let error = Jingle::try_from(elem).unwrap_err();
762        let message = match error {
763            FromElementError::Invalid(Error::Other(string)) => string,
764            _ => panic!(),
765        };
766        assert_eq!(
767            message,
768            "Missing child field 'reason' in ReasonElement element."
769        );
770    }
771
772    #[test]
773    #[cfg_attr(feature = "disable-validation", should_panic = "Result::unwrap_err")]
774    fn test_invalid_child_in_reason() {
775        let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='session-initiate' sid='coucou'><reason><decline/><a/></reason></jingle>".parse().unwrap();
776        let error = Jingle::try_from(elem).unwrap_err();
777        let message = match error {
778            FromElementError::Invalid(Error::Other(string)) => string,
779            _ => panic!(),
780        };
781        assert_eq!(message, "Unknown child in ReasonElement element.");
782    }
783
784    #[test]
785    fn test_multiple_reason_children() {
786        let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='session-initiate' sid='coucou'><reason><decline/></reason><reason/></jingle>".parse().unwrap();
787        let error = Jingle::try_from(elem).unwrap_err();
788        let message = match error {
789            FromElementError::Invalid(Error::Other(string)) => string,
790            _ => panic!(),
791        };
792        assert_eq!(
793            message,
794            "Jingle element must not have more than one child in field 'reason'."
795        );
796
797        // TODO: Reenable this test once xso is able to validate that no more than one text is
798        // there for every xml:lang.
799        /*
800        let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='session-initiate' sid='coucou'><reason><decline/><text/><text/></reason></jingle>".parse().unwrap();
801        let error = Jingle::try_from(elem).unwrap_err();
802        let message = match error {
803            FromElementError::Invalid(Error::Other(string)) => string,
804            _ => panic!(),
805        };
806        assert_eq!(message, "Text element present twice for the same xml:lang.");
807        */
808    }
809
810    #[test]
811    fn test_serialize_jingle() {
812        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>"
813        .parse()
814        .unwrap();
815
816        let jingle = Jingle {
817            action: Action::SessionInitiate,
818            initiator: None,
819            responder: None,
820            sid: SessionId(String::from("a73sjjvkla37jfea")),
821            contents: vec![Content {
822                creator: Creator::Initiator,
823                disposition: Disposition::default(),
824                name: ContentId(String::from("this-is-a-stub")),
825                senders: Senders::default(),
826                description: Some(Description::Unknown(
827                    Element::builder("description", "urn:xmpp:jingle:apps:stub:0").build(),
828                )),
829                transport: Some(Transport::Unknown(
830                    Element::builder("transport", "urn:xmpp:jingle:transports:stub:0").build(),
831                )),
832                security: None,
833            }],
834            reason: None,
835            group: None,
836            other: vec![],
837        };
838        let serialized: Element = jingle.into();
839        assert_eq!(serialized, reference);
840    }
841}