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 lang(type_ = 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, 152);
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, 296);
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}