protocol.js 28 KB

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