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