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}