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