protocol.js 28 KB

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