protocol.js 27 KB

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