protocol.js 27 KB

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