xmpp_parsers/
roster.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 jid::BareJid;
10
11use crate::iq::{IqGetPayload, IqResultPayload, IqSetPayload};
12use crate::ns;
13
14generate_elem_id!(
15    /// Represents a group a contact is part of.
16    Group,
17    "group",
18    ROSTER
19);
20
21generate_attribute!(
22    /// The state of your mutual subscription with a contact.
23    Subscription, "subscription", {
24        /// The user doesn’t have any subscription to this contact’s presence,
25        /// and neither does this contact.
26        None => "none",
27
28        /// Only this contact has a subscription with you, not the opposite.
29        From => "from",
30
31        /// Only you have a subscription with this contact, not the opposite.
32        To => "to",
33
34        /// Both you and your contact are subscribed to each other’s presence.
35        Both => "both",
36
37        /// In a roster set, this asks the server to remove this contact item
38        /// from your roster.
39        Remove => "remove",
40    }, Default = None
41);
42
43generate_attribute!(
44    /// The sub-state of subscription with a contact.
45    Ask, "ask", (
46        /// Pending sub-state of the 'none' subscription state.
47        Subscribe => "subscribe"
48    )
49);
50
51/// Contact from the user’s contact list.
52#[derive(FromXml, AsXml, PartialEq, Debug, Clone)]
53#[xml(namespace = ns::ROSTER, name = "item")]
54pub struct Item {
55    /// JID of this contact.
56    #[xml(attribute)]
57    pub jid: BareJid,
58
59    /// Name of this contact.
60    #[xml(attribute(default))]
61    pub name: Option<String>,
62
63    /// Subscription status of this contact.
64    #[xml(attribute(default))]
65    pub subscription: Subscription,
66
67    /// Indicates “Pending Out” sub-states for this contact.
68    #[xml(attribute(default))]
69    pub ask: Ask,
70
71    /// Groups this contact is part of.
72    #[xml(child(n = ..))]
73    pub groups: Vec<Group>,
74}
75
76/// The contact list of the user.
77#[derive(FromXml, AsXml, PartialEq, Debug, Clone)]
78#[xml(namespace = ns::ROSTER, name = "query")]
79pub struct Roster {
80    /// Version of the contact list.
81    ///
82    /// This is an opaque string that should only be sent back to the server on
83    /// a new connection, if this client is storing the contact list between
84    /// connections.
85    #[xml(attribute(default))]
86    pub ver: Option<String>,
87
88    /// List of the contacts of the user.
89    #[xml(child(n = ..))]
90    pub items: Vec<Item>,
91}
92
93impl IqGetPayload for Roster {}
94impl IqSetPayload for Roster {}
95impl IqResultPayload for Roster {}
96
97#[cfg(test)]
98mod tests {
99    use super::*;
100    use core::str::FromStr;
101    use minidom::Element;
102    use xso::error::{Error, FromElementError};
103
104    #[cfg(target_pointer_width = "32")]
105    #[test]
106    fn test_size() {
107        assert_size!(Group, 12);
108        assert_size!(Subscription, 1);
109        assert_size!(Ask, 1);
110        assert_size!(Item, 44);
111        assert_size!(Roster, 24);
112    }
113
114    #[cfg(target_pointer_width = "64")]
115    #[test]
116    fn test_size() {
117        assert_size!(Group, 24);
118        assert_size!(Subscription, 1);
119        assert_size!(Ask, 1);
120        assert_size!(Item, 88);
121        assert_size!(Roster, 48);
122    }
123
124    #[test]
125    fn test_get() {
126        let elem: Element = "<query xmlns='jabber:iq:roster'/>".parse().unwrap();
127        let roster = Roster::try_from(elem).unwrap();
128        assert!(roster.ver.is_none());
129        assert!(roster.items.is_empty());
130    }
131
132    #[test]
133    fn test_result() {
134        let elem: Element = "<query xmlns='jabber:iq:roster' ver='ver7'><item jid='nurse@example.com'/><item jid='romeo@example.net'/></query>".parse().unwrap();
135        let roster = Roster::try_from(elem).unwrap();
136        assert_eq!(roster.ver, Some(String::from("ver7")));
137        assert_eq!(roster.items.len(), 2);
138
139        let elem: Element = "<query xmlns='jabber:iq:roster' ver='ver9'/>"
140            .parse()
141            .unwrap();
142        let roster = Roster::try_from(elem).unwrap();
143        assert_eq!(roster.ver, Some(String::from("ver9")));
144        assert!(roster.items.is_empty());
145
146        let elem: Element = r#"<query xmlns='jabber:iq:roster' ver='ver11'>
147  <item jid='romeo@example.net'
148        name='Romeo'
149        subscription='both'>
150    <group>Friends</group>
151  </item>
152  <item jid='mercutio@example.com'
153        name='Mercutio'
154        subscription='from'/>
155  <item jid='benvolio@example.net'
156        name='Benvolio'
157        subscription='both'/>
158  <item jid='contact@example.org'
159        subscription='none'
160        ask='subscribe'
161        name='MyContact'>
162      <group>MyBuddies</group>
163  </item>
164</query>
165"#
166        .parse()
167        .unwrap();
168        let roster = Roster::try_from(elem).unwrap();
169        assert_eq!(roster.ver, Some(String::from("ver11")));
170        assert_eq!(roster.items.len(), 4);
171        assert_eq!(
172            roster.items[0].jid,
173            BareJid::new("romeo@example.net").unwrap()
174        );
175        assert_eq!(roster.items[0].name, Some(String::from("Romeo")));
176        assert_eq!(roster.items[0].subscription, Subscription::Both);
177        assert_eq!(roster.items[0].ask, Ask::None);
178        assert_eq!(
179            roster.items[0].groups,
180            vec!(Group::from_str("Friends").unwrap())
181        );
182
183        assert_eq!(
184            roster.items[3].jid,
185            BareJid::new("contact@example.org").unwrap()
186        );
187        assert_eq!(roster.items[3].name, Some(String::from("MyContact")));
188        assert_eq!(roster.items[3].subscription, Subscription::None);
189        assert_eq!(roster.items[3].ask, Ask::Subscribe);
190        assert_eq!(
191            roster.items[3].groups,
192            vec!(Group::from_str("MyBuddies").unwrap())
193        );
194    }
195
196    #[test]
197    fn test_multiple_groups() {
198        let elem: Element = "<query xmlns='jabber:iq:roster'><item jid='test@example.org'><group>A</group><group>B</group></item></query>"
199        .parse()
200        .unwrap();
201        let elem1 = elem.clone();
202        let roster = Roster::try_from(elem).unwrap();
203        assert!(roster.ver.is_none());
204        assert_eq!(roster.items.len(), 1);
205        assert_eq!(
206            roster.items[0].jid,
207            BareJid::new("test@example.org").unwrap()
208        );
209        assert_eq!(roster.items[0].name, None);
210        assert_eq!(roster.items[0].groups.len(), 2);
211        assert_eq!(roster.items[0].groups[0], Group::from_str("A").unwrap());
212        assert_eq!(roster.items[0].groups[1], Group::from_str("B").unwrap());
213        let elem2 = roster.into();
214        assert_eq!(elem1, elem2);
215    }
216
217    #[test]
218    fn test_set() {
219        let elem: Element =
220            "<query xmlns='jabber:iq:roster'><item jid='nurse@example.com'/></query>"
221                .parse()
222                .unwrap();
223        let roster = Roster::try_from(elem).unwrap();
224        assert!(roster.ver.is_none());
225        assert_eq!(roster.items.len(), 1);
226
227        let elem: Element = r#"<query xmlns='jabber:iq:roster'>
228  <item jid='nurse@example.com'
229        name='Nurse'>
230    <group>Servants</group>
231  </item>
232</query>"#
233            .parse()
234            .unwrap();
235        let roster = Roster::try_from(elem).unwrap();
236        assert!(roster.ver.is_none());
237        assert_eq!(roster.items.len(), 1);
238        assert_eq!(
239            roster.items[0].jid,
240            BareJid::new("nurse@example.com").unwrap()
241        );
242        assert_eq!(roster.items[0].name, Some(String::from("Nurse")));
243        assert_eq!(roster.items[0].groups.len(), 1);
244        assert_eq!(
245            roster.items[0].groups[0],
246            Group::from_str("Servants").unwrap()
247        );
248
249        let elem: Element = r#"<query xmlns='jabber:iq:roster'>
250  <item jid='nurse@example.com'
251        subscription='remove'/>
252</query>"#
253            .parse()
254            .unwrap();
255        let roster = Roster::try_from(elem).unwrap();
256        assert!(roster.ver.is_none());
257        assert_eq!(roster.items.len(), 1);
258        assert_eq!(
259            roster.items[0].jid,
260            BareJid::new("nurse@example.com").unwrap()
261        );
262        assert!(roster.items[0].name.is_none());
263        assert!(roster.items[0].groups.is_empty());
264        assert_eq!(roster.items[0].subscription, Subscription::Remove);
265    }
266
267    #[cfg(not(feature = "disable-validation"))]
268    #[test]
269    fn test_invalid() {
270        let elem: Element = "<query xmlns='jabber:iq:roster'><coucou/></query>"
271            .parse()
272            .unwrap();
273        let error = Roster::try_from(elem).unwrap_err();
274        let message = match error {
275            FromElementError::Invalid(Error::Other(string)) => string,
276            _ => panic!(),
277        };
278        assert_eq!(message, "Unknown child in Roster element.");
279
280        let elem: Element = "<query xmlns='jabber:iq:roster' coucou=''/>"
281            .parse()
282            .unwrap();
283        let error = Roster::try_from(elem).unwrap_err();
284        let message = match error {
285            FromElementError::Invalid(Error::Other(string)) => string,
286            _ => panic!(),
287        };
288        assert_eq!(message, "Unknown attribute in Roster element.");
289    }
290
291    #[test]
292    fn test_item_missing_jid() {
293        let elem: Element = "<query xmlns='jabber:iq:roster'><item/></query>"
294            .parse()
295            .unwrap();
296        let error = Roster::try_from(elem).unwrap_err();
297        let message = match error {
298            FromElementError::Invalid(Error::Other(string)) => string,
299            _ => panic!(),
300        };
301        assert_eq!(
302            message,
303            "Required attribute field 'jid' on Item element missing."
304        );
305    }
306
307    #[test]
308    fn test_item_invalid_jid() {
309        let elem: Element = "<query xmlns='jabber:iq:roster'><item jid=''/></query>"
310            .parse()
311            .unwrap();
312        let error = Roster::try_from(elem).unwrap_err();
313        assert_eq!(
314            format!("{error}"),
315            "text parse error: domain doesn’t pass idna validation"
316        );
317    }
318
319    #[test]
320    #[cfg_attr(feature = "disable-validation", should_panic = "Result::unwrap_err")]
321    fn test_item_unknown_child() {
322        let elem: Element =
323            "<query xmlns='jabber:iq:roster'><item jid='coucou'><coucou/></item></query>"
324                .parse()
325                .unwrap();
326        let error = Roster::try_from(elem).unwrap_err();
327        let message = match error {
328            FromElementError::Invalid(Error::Other(string)) => string,
329            _ => panic!(),
330        };
331        assert_eq!(message, "Unknown child in Item element.");
332    }
333}