protocol.js 28 KB

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