protocol.js 27 KB

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