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