2
0

protocol.js 28 KB

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