protocol.js 25 KB

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