protocol.js 28 KB

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