protocol.js 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534
  1. /*global mock */
  2. // See: https://xmpp.org/rfcs/rfc3921.html
  3. describe("The Protocol", function () {
  4. describe("Integration of Roster Items and Presence Subscriptions", function () {
  5. // Stub the trimChat method. It causes havoc when running with
  6. // phantomJS.
  7. /* Some level of integration between roster items and presence
  8. * subscriptions is normally expected by an instant messaging user
  9. * regarding the user's subscriptions to and from other contacts. This
  10. * section describes the level of integration that MUST be supported
  11. * within an XMPP instant messaging applications.
  12. *
  13. * There are four primary subscription states:
  14. *
  15. * None -- the user does not have a subscription to the contact's
  16. * presence information, and the contact does not have a subscription
  17. * to the user's presence information
  18. * To -- the user has a subscription to the contact's presence
  19. * information, but the contact does not have a subscription to the
  20. * user's presence information
  21. * From -- the contact has a subscription to the user's presence
  22. * information, but the user does not have a subscription to the
  23. * contact's presence information
  24. * Both -- both the user and the contact have subscriptions to each
  25. * other's presence information (i.e., the union of 'from' and 'to')
  26. *
  27. * Each of these states is reflected in the roster of both the user and
  28. * the contact, thus resulting in durable subscription states.
  29. *
  30. * The 'from' and 'to' addresses are OPTIONAL in roster pushes; if
  31. * included, their values SHOULD be the full JID of the resource for
  32. * that session. A client MUST acknowledge each roster push with an IQ
  33. * stanza of type "result".
  34. */
  35. it("Subscribe to contact, contact accepts and subscribes back",
  36. mock.initConverse(
  37. ['rosterGroupsFetched'],
  38. { roster_groups: false },
  39. async function (done, _converse) {
  40. const { u, $iq, $pres, sizzle, Strophe } = converse.env;
  41. let contact, sent_stanza, IQ_id, stanza;
  42. await mock.waitUntilDiscoConfirmed(_converse, 'montague.lit', [], ['vcard-temp']);
  43. await u.waitUntil(() => _converse.xmppstatus.vcard.get('fullname'), 300);
  44. /* The process by which a user subscribes to a contact, including
  45. * the interaction between roster items and subscription states.
  46. */
  47. mock.openControlBox(_converse);
  48. const cbview = _converse.chatboxviews.get('controlbox');
  49. spyOn(_converse.roster, "addAndSubscribe").and.callThrough();
  50. spyOn(_converse.roster, "addContactToRoster").and.callThrough();
  51. spyOn(_converse.roster, "sendContactAddIQ").and.callThrough();
  52. spyOn(_converse.api.vcard, "get").and.callThrough();
  53. const sendIQ = _converse.connection.sendIQ;
  54. spyOn(_converse.connection, 'sendIQ').and.callFake(function (iq, callback, errback) {
  55. sent_stanza = iq;
  56. IQ_id = sendIQ.bind(this)(iq, callback, errback);
  57. });
  58. cbview.el.querySelector('.add-contact').click()
  59. const modal = _converse.rosterview.add_contact_modal;
  60. await u.waitUntil(() => u.isVisible(modal.el), 1000);
  61. spyOn(modal, "addContactFromForm").and.callThrough();
  62. modal.delegateEvents();
  63. // Fill in the form and submit
  64. const form = modal.el.querySelector('form.add-xmpp-contact');
  65. form.querySelector('input').value = 'contact@example.org';
  66. form.querySelector('[type="submit"]').click();
  67. /* In preparation for being able to render the contact in the
  68. * user's client interface and for the server to keep track of the
  69. * subscription, the user's client SHOULD perform a "roster set"
  70. * for the new roster item.
  71. */
  72. expect(modal.addContactFromForm).toHaveBeenCalled();
  73. expect(_converse.roster.addAndSubscribe).toHaveBeenCalled();
  74. expect(_converse.roster.addContactToRoster).toHaveBeenCalled();
  75. /* _converse request consists of sending an IQ
  76. * stanza of type='set' containing a <query/> element qualified by
  77. * the 'jabber:iq:roster' namespace, which in turn contains an
  78. * <item/> element that defines the new roster item; the <item/>
  79. * element MUST possess a 'jid' attribute, MAY possess a 'name'
  80. * attribute, MUST NOT possess a 'subscription' attribute, and MAY
  81. * contain one or more <group/> child elements:
  82. *
  83. * <iq type='set' id='set1'>
  84. * <query xmlns='jabber:iq:roster'>
  85. * <item
  86. * jid='contact@example.org'
  87. * name='MyContact'>
  88. * <group>MyBuddies</group>
  89. * </item>
  90. * </query>
  91. * </iq>
  92. */
  93. await mock.waitForRoster(_converse, 'all', 0);
  94. expect(_converse.roster.sendContactAddIQ).toHaveBeenCalled();
  95. expect(sent_stanza.toLocaleString()).toBe(
  96. `<iq id="${IQ_id}" type="set" xmlns="jabber:client">`+
  97. `<query xmlns="jabber:iq:roster">`+
  98. `<item jid="contact@example.org"/>`+
  99. `</query>`+
  100. `</iq>`
  101. );
  102. /* As a result, the user's server (1) MUST initiate a roster push
  103. * for the new roster item to all available resources associated
  104. * with _converse user that have requested the roster, setting the
  105. * 'subscription' attribute to a value of "none"; and (2) MUST
  106. * reply to the sending resource with an IQ result indicating the
  107. * success of the roster set:
  108. *
  109. * <iq type='set'>
  110. * <query xmlns='jabber:iq:roster'>
  111. * <item
  112. * jid='contact@example.org'
  113. * subscription='none'
  114. * name='MyContact'>
  115. * <group>MyBuddies</group>
  116. * </item>
  117. * </query>
  118. * </iq>
  119. */
  120. const create = _converse.roster.create;
  121. const sent_stanzas = [];
  122. spyOn(_converse.connection, 'send').and.callFake(function (stanza) {
  123. sent_stanza = stanza;
  124. sent_stanzas.push(stanza.toLocaleString());
  125. });
  126. spyOn(_converse.roster, 'create').and.callFake(function () {
  127. contact = create.apply(_converse.roster, arguments);
  128. spyOn(contact, 'subscribe').and.callThrough();
  129. return contact;
  130. });
  131. stanza = $iq({'type': 'set'}).c('query', {'xmlns': 'jabber:iq:roster'})
  132. .c('item', {
  133. 'jid': 'contact@example.org',
  134. 'subscription': 'none',
  135. 'name': 'contact@example.org'});
  136. _converse.connection._dataRecv(mock.createRequest(stanza));
  137. /* <iq type='result' id='set1'/>
  138. */
  139. stanza = $iq({'type': 'result', 'id':IQ_id});
  140. _converse.connection._dataRecv(mock.createRequest(stanza));
  141. await u.waitUntil(() => _converse.roster.create.calls.count());
  142. // A contact should now have been created
  143. expect(_converse.roster.get('contact@example.org') instanceof _converse.RosterContact).toBeTruthy();
  144. expect(contact.get('jid')).toBe('contact@example.org');
  145. await u.waitUntil(() => contact.initialized);
  146. /* To subscribe to the contact's presence information,
  147. * the user's client MUST send a presence stanza of
  148. * type='subscribe' to the contact:
  149. *
  150. * <presence to='contact@example.org' type='subscribe'/>
  151. */
  152. const sent_presence = await u.waitUntil(() => sent_stanzas.filter(s => s.match('presence')).pop());
  153. expect(contact.subscribe).toHaveBeenCalled();
  154. expect(sent_presence).toBe(
  155. `<presence to="contact@example.org" type="subscribe" xmlns="jabber:client">`+
  156. `<nick xmlns="http://jabber.org/protocol/nick">Romeo Montague</nick>`+
  157. `</presence>`
  158. );
  159. /* As a result, the user's server MUST initiate a second roster
  160. * push to all of the user's available resources that have
  161. * requested the roster, setting the contact to the pending
  162. * sub-state of the 'none' subscription state; _converse pending
  163. * sub-state is denoted by the inclusion of the ask='subscribe'
  164. * attribute in the roster item:
  165. *
  166. * <iq type='set'>
  167. * <query xmlns='jabber:iq:roster'>
  168. * <item
  169. * jid='contact@example.org'
  170. * subscription='none'
  171. * ask='subscribe'
  172. * name='MyContact'>
  173. * <group>MyBuddies</group>
  174. * </item>
  175. * </query>
  176. * </iq>
  177. */
  178. spyOn(_converse.roster, "updateContact").and.callThrough();
  179. stanza = $iq({'type': 'set', 'from': _converse.bare_jid})
  180. .c('query', {'xmlns': 'jabber:iq:roster'})
  181. .c('item', {
  182. 'jid': 'contact@example.org',
  183. 'subscription': 'none',
  184. 'ask': 'subscribe',
  185. 'name': 'contact@example.org'});
  186. _converse.connection._dataRecv(mock.createRequest(stanza));
  187. expect(_converse.roster.updateContact).toHaveBeenCalled();
  188. // Check that the user is now properly shown as a pending
  189. // contact in the roster.
  190. await u.waitUntil(() => {
  191. const header = sizzle('a:contains("Pending contacts")', _converse.rosterview.el).pop();
  192. const contacts = Array.from(header.parentElement.querySelectorAll('li')).filter(u.isVisible);
  193. return contacts.length;
  194. }, 600);
  195. let header = sizzle('a:contains("Pending contacts")', _converse.rosterview.el).pop();
  196. let contacts = header.parentElement.querySelectorAll('li');
  197. expect(contacts.length).toBe(1);
  198. expect(u.isVisible(contacts[0])).toBe(true);
  199. spyOn(contact, "ackSubscribe").and.callThrough();
  200. /* Here we assume the "happy path" that the contact
  201. * approves the subscription request
  202. *
  203. * <presence
  204. * to='user@example.com'
  205. * from='contact@example.org'
  206. * type='subscribed'/>
  207. */
  208. stanza = $pres({
  209. 'to': _converse.bare_jid,
  210. 'from': 'contact@example.org',
  211. 'type': 'subscribed'
  212. });
  213. sent_stanza = ""; // Reset
  214. _converse.connection._dataRecv(mock.createRequest(stanza));
  215. /* Upon receiving the presence stanza of type "subscribed",
  216. * the user SHOULD acknowledge receipt of that
  217. * subscription state notification by sending a presence
  218. * stanza of type "subscribe".
  219. */
  220. expect(contact.ackSubscribe).toHaveBeenCalled();
  221. expect(sent_stanza.toLocaleString()).toBe( // Strophe adds the xmlns attr (although not in spec)
  222. `<presence to="contact@example.org" type="subscribe" xmlns="jabber:client"/>`
  223. );
  224. /* The user's server MUST initiate a roster push to all of the user's
  225. * available resources that have requested the roster,
  226. * containing an updated roster item for the contact with
  227. * the 'subscription' attribute set to a value of "to";
  228. *
  229. * <iq type='set'>
  230. * <query xmlns='jabber:iq:roster'>
  231. * <item
  232. * jid='contact@example.org'
  233. * subscription='to'
  234. * name='MyContact'>
  235. * <group>MyBuddies</group>
  236. * </item>
  237. * </query>
  238. * </iq>
  239. */
  240. IQ_id = _converse.connection.getUniqueId('roster');
  241. stanza = $iq({'type': 'set', 'id': IQ_id})
  242. .c('query', {'xmlns': 'jabber:iq:roster'})
  243. .c('item', {
  244. 'jid': 'contact@example.org',
  245. 'subscription': 'to',
  246. 'name': 'Nicky'});
  247. _converse.connection._dataRecv(mock.createRequest(stanza));
  248. // Check that the IQ set was acknowledged.
  249. expect(Strophe.serialize(sent_stanza)).toBe( // Strophe adds the xmlns attr (although not in spec)
  250. `<iq from="romeo@montague.lit/orchard" id="${IQ_id}" type="result" xmlns="jabber:client"/>`
  251. );
  252. expect(_converse.roster.updateContact).toHaveBeenCalled();
  253. // The contact should now be visible as an existing
  254. // contact (but still offline).
  255. await u.waitUntil(() => {
  256. const header = sizzle('a:contains("My contacts")', _converse.rosterview.el);
  257. return sizzle('li', header[0].parentNode).filter(l => u.isVisible(l)).length;
  258. }, 600);
  259. header = sizzle('a:contains("My contacts")', _converse.rosterview.el);
  260. expect(header.length).toBe(1);
  261. expect(u.isVisible(header[0])).toBeTruthy();
  262. contacts = header[0].parentNode.querySelectorAll('li');
  263. expect(contacts.length).toBe(1);
  264. // Check that it has the right classes and text
  265. expect(u.hasClass('to', contacts[0])).toBeTruthy();
  266. expect(u.hasClass('both', contacts[0])).toBeFalsy();
  267. expect(u.hasClass('current-xmpp-contact', contacts[0])).toBeTruthy();
  268. expect(contacts[0].textContent.trim()).toBe('Nicky');
  269. expect(contact.presence.get('show')).toBe('offline');
  270. /* <presence
  271. * from='contact@example.org/resource'
  272. * to='user@example.com/resource'/>
  273. */
  274. stanza = $pres({'to': _converse.bare_jid, 'from': 'contact@example.org/resource'});
  275. _converse.connection._dataRecv(mock.createRequest(stanza));
  276. // Now the contact should also be online.
  277. expect(contact.presence.get('show')).toBe('online');
  278. /* Section 8.3. Creating a Mutual Subscription
  279. *
  280. * If the contact wants to create a mutual subscription,
  281. * the contact MUST send a subscription request to the
  282. * user.
  283. *
  284. * <presence from='contact@example.org' to='user@example.com' type='subscribe'/>
  285. */
  286. spyOn(contact, 'authorize').and.callThrough();
  287. spyOn(_converse.roster, 'handleIncomingSubscription').and.callThrough();
  288. stanza = $pres({
  289. 'to': _converse.bare_jid,
  290. 'from': 'contact@example.org/resource',
  291. 'type': 'subscribe'});
  292. _converse.connection._dataRecv(mock.createRequest(stanza));
  293. expect(_converse.roster.handleIncomingSubscription).toHaveBeenCalled();
  294. /* The user's client MUST send a presence stanza of type
  295. * "subscribed" to the contact in order to approve the
  296. * subscription request.
  297. *
  298. * <presence to='contact@example.org' type='subscribed'/>
  299. */
  300. expect(contact.authorize).toHaveBeenCalled();
  301. expect(sent_stanza.toLocaleString()).toBe(
  302. `<presence to="contact@example.org" type="subscribed" xmlns="jabber:client"/>`
  303. );
  304. /* As a result, the user's server MUST initiate a
  305. * roster push containing a roster item for the
  306. * contact with the 'subscription' attribute set to
  307. * a value of "both".
  308. *
  309. * <iq type='set'>
  310. * <query xmlns='jabber:iq:roster'>
  311. * <item
  312. * jid='contact@example.org'
  313. * subscription='both'
  314. * name='MyContact'>
  315. * <group>MyBuddies</group>
  316. * </item>
  317. * </query>
  318. * </iq>
  319. */
  320. stanza = $iq({'type': 'set'}).c('query', {'xmlns': 'jabber:iq:roster'})
  321. .c('item', {
  322. 'jid': 'contact@example.org',
  323. 'subscription': 'both',
  324. 'name': 'contact@example.org'});
  325. _converse.connection._dataRecv(mock.createRequest(stanza));
  326. expect(_converse.roster.updateContact).toHaveBeenCalled();
  327. // The class on the contact will now have switched.
  328. await u.waitUntil(() => !u.hasClass('to', contacts[0]));
  329. expect(u.hasClass('both', contacts[0])).toBe(true);
  330. done();
  331. }));
  332. it("Alternate Flow: Contact Declines Subscription Request",
  333. mock.initConverse(
  334. ['rosterGroupsFetched'], {},
  335. function (done, _converse) {
  336. const { $iq, $pres } = converse.env;
  337. /* The process by which a user subscribes to a contact, including
  338. * the interaction between roster items and subscription states.
  339. */
  340. var contact, stanza, sent_stanza, sent_IQ;
  341. mock.openControlBox(_converse);
  342. // Add a new roster contact via roster push
  343. stanza = $iq({'type': 'set'}).c('query', {'xmlns': 'jabber:iq:roster'})
  344. .c('item', {
  345. 'jid': 'contact@example.org',
  346. 'subscription': 'none',
  347. 'ask': 'subscribe',
  348. 'name': 'contact@example.org'});
  349. _converse.connection._dataRecv(mock.createRequest(stanza));
  350. // A pending contact should now exist.
  351. contact = _converse.roster.get('contact@example.org');
  352. expect(_converse.roster.get('contact@example.org') instanceof _converse.RosterContact).toBeTruthy();
  353. spyOn(contact, "ackUnsubscribe").and.callThrough();
  354. spyOn(_converse.connection, 'send').and.callFake(stanza => { sent_stanza = stanza });
  355. spyOn(_converse.connection, 'sendIQ').and.callFake(iq => { sent_IQ = iq });
  356. /* We now assume the contact declines the subscription
  357. * requests.
  358. *
  359. * Upon receiving the presence stanza of type "unsubscribed"
  360. * addressed to the user, the user's server (1) MUST deliver
  361. * that presence stanza to the user and (2) MUST initiate a
  362. * roster push to all of the user's available resources that
  363. * have requested the roster, containing an updated roster
  364. * item for the contact with the 'subscription' attribute
  365. * set to a value of "none" and with no 'ask' attribute:
  366. *
  367. * <presence
  368. * from='contact@example.org'
  369. * to='user@example.com'
  370. * type='unsubscribed'/>
  371. *
  372. * <iq type='set'>
  373. * <query xmlns='jabber:iq:roster'>
  374. * <item
  375. * jid='contact@example.org'
  376. * subscription='none'
  377. * name='MyContact'>
  378. * <group>MyBuddies</group>
  379. * </item>
  380. * </query>
  381. * </iq>
  382. */
  383. // FIXME: also add the <iq>
  384. stanza = $pres({
  385. 'to': _converse.bare_jid,
  386. 'from': 'contact@example.org',
  387. 'type': 'unsubscribed'
  388. });
  389. _converse.connection._dataRecv(mock.createRequest(stanza));
  390. /* Upon receiving the presence stanza of type "unsubscribed",
  391. * the user SHOULD acknowledge receipt of that subscription
  392. * state notification through either "affirming" it by
  393. * sending a presence stanza of type "unsubscribe
  394. */
  395. expect(contact.ackUnsubscribe).toHaveBeenCalled();
  396. expect(sent_stanza.toLocaleString()).toBe(
  397. `<presence to="contact@example.org" type="unsubscribe" xmlns="jabber:client"/>`
  398. );
  399. /* _converse.js will then also automatically remove the
  400. * contact from the user's roster.
  401. */
  402. expect(sent_IQ.toLocaleString()).toBe(
  403. `<iq type="set" xmlns="jabber:client">`+
  404. `<query xmlns="jabber:iq:roster">`+
  405. `<item jid="contact@example.org" subscription="remove"/>`+
  406. `</query>`+
  407. `</iq>`
  408. );
  409. done();
  410. }));
  411. it("Unsubscribe to a contact when subscription is mutual",
  412. mock.initConverse(
  413. ['rosterGroupsFetched'],
  414. { roster_groups: false },
  415. async function (done, _converse) {
  416. const { u, $iq, sizzle, Strophe } = converse.env;
  417. const jid = 'abram@montague.lit';
  418. await mock.openControlBox(_converse);
  419. await mock.waitForRoster(_converse, 'current');
  420. spyOn(window, 'confirm').and.returnValue(true);
  421. // We now have a contact we want to remove
  422. expect(_converse.roster.get(jid) instanceof _converse.RosterContact).toBeTruthy();
  423. const header = sizzle('a:contains("My contacts")', _converse.rosterview.el).pop();
  424. await u.waitUntil(() => header.parentElement.querySelectorAll('li').length);
  425. // remove the first user
  426. header.parentElement.querySelector('li .remove-xmpp-contact').click();
  427. expect(window.confirm).toHaveBeenCalled();
  428. /* Section 8.6 Removing a Roster Item and Cancelling All
  429. * Subscriptions
  430. *
  431. * First the user is removed from the roster
  432. * Because there may be many steps involved in completely
  433. * removing a roster item and cancelling subscriptions in
  434. * both directions, the roster management protocol includes
  435. * a "shortcut" method for doing so. The process may be
  436. * initiated no matter what the current subscription state
  437. * is by sending a roster set containing an item for the
  438. * contact with the 'subscription' attribute set to a value
  439. * of "remove":
  440. *
  441. * <iq type='set' id='remove1'>
  442. * <query xmlns='jabber:iq:roster'>
  443. * <item jid='contact@example.org' subscription='remove'/>
  444. * </query>
  445. * </iq>
  446. */
  447. const sent_iq = _converse.connection.IQ_stanzas.pop();
  448. expect(Strophe.serialize(sent_iq)).toBe(
  449. `<iq id="${sent_iq.getAttribute('id')}" type="set" xmlns="jabber:client">`+
  450. `<query xmlns="jabber:iq:roster">`+
  451. `<item jid="abram@montague.lit" subscription="remove"/>`+
  452. `</query>`+
  453. `</iq>`);
  454. // Receive confirmation from the contact's server
  455. // <iq type='result' id='remove1'/>
  456. const stanza = $iq({'type': 'result', 'id': sent_iq.getAttribute('id')});
  457. _converse.connection._dataRecv(mock.createRequest(stanza));
  458. // Our contact has now been removed
  459. await u.waitUntil(() => typeof _converse.roster.get(jid) === "undefined");
  460. done();
  461. }));
  462. it("Receiving a subscription request", mock.initConverse(
  463. ['rosterGroupsFetched'], {},
  464. async function (done, _converse) {
  465. const { u, $pres, sizzle, Strophe } = converse.env;
  466. spyOn(_converse.api, "trigger").and.callThrough();
  467. await mock.openControlBox(_converse);
  468. await mock.waitForRoster(_converse, 'current');
  469. /* <presence
  470. * from='user@example.com'
  471. * to='contact@example.org'
  472. * type='subscribe'/>
  473. */
  474. const stanza = $pres({
  475. 'to': _converse.bare_jid,
  476. 'from': 'contact@example.org',
  477. 'type': 'subscribe'
  478. }).c('nick', {
  479. 'xmlns': Strophe.NS.NICK,
  480. }).t('Clint Contact');
  481. _converse.connection._dataRecv(mock.createRequest(stanza));
  482. await u.waitUntil(() => {
  483. const header = sizzle('a:contains("Contact requests")', _converse.rosterview.el).pop();
  484. const contacts = Array.from(header.parentElement.querySelectorAll('li')).filter(u.isVisible);
  485. return contacts.length;
  486. }, 500);
  487. expect(_converse.api.trigger).toHaveBeenCalledWith('contactRequest', jasmine.any(Object));
  488. const header = sizzle('a:contains("Contact requests")', _converse.rosterview.el).pop();
  489. expect(u.isVisible(header)).toBe(true);
  490. const contacts = header.parentElement.querySelectorAll('li');
  491. expect(contacts.length).toBe(1);
  492. done();
  493. }));
  494. });
  495. });