protocol.js 28 KB

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