protocol.js 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367
  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", $.proxy(function (mock, test_utils) {
  18. describe("Integration of Roster Items and Presence Subscriptions", $.proxy(function (mock, test_utils) {
  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. beforeEach(function () {
  48. test_utils.closeAllChatBoxes();
  49. test_utils.removeControlBox();
  50. converse.roster.browserStorage._clear();
  51. test_utils.initConverse();
  52. test_utils.openControlBox();
  53. test_utils.openContactsPanel();
  54. });
  55. it("Mutual subscription between the users and a contact", $.proxy(function () {
  56. /* The process by which a user subscribes to a contact, including
  57. * the interaction between roster items and subscription states.
  58. */
  59. var contact, stanza, sentStanza, iq_id;
  60. runs($.proxy(function () {
  61. var panel = this.chatboxviews.get('controlbox').contactspanel;
  62. spyOn(panel, "addContactFromForm").andCallThrough();
  63. spyOn(this.roster, "addAndSubscribe").andCallThrough();
  64. spyOn(this.roster, "addContact").andCallThrough();
  65. spyOn(this.roster, "sendContactAddIQ").andCallThrough();
  66. spyOn(this, "getVCard").andCallThrough();
  67. var sendIQ = this.connection.sendIQ;
  68. spyOn(this.connection, 'sendIQ').andCallFake(function (iq, callback, errback) {
  69. sentStanza = iq;
  70. iq_id = sendIQ.bind(this)(iq, callback, errback);
  71. });
  72. panel.delegateEvents(); // Rebind all events so that our spy gets called
  73. /* Add a new contact through the UI */
  74. var $form = panel.$('form.add-xmpp-contact');
  75. expect($form.is(":visible")).toBeFalsy();
  76. // Click the "Add a contact" link.
  77. panel.$('.toggle-xmpp-contact-form').click();
  78. // Check that the $form appears
  79. expect($form.is(":visible")).toBeTruthy();
  80. // Fill in the form and submit
  81. $form.find('input').val('contact@example.org');
  82. $form.submit();
  83. /* In preparation for being able to render the contact in the
  84. * user's client interface and for the server to keep track of the
  85. * subscription, the user's client SHOULD perform a "roster set"
  86. * for the new roster item.
  87. */
  88. expect(panel.addContactFromForm).toHaveBeenCalled();
  89. expect(converse.roster.addAndSubscribe).toHaveBeenCalled();
  90. expect(converse.roster.addContact).toHaveBeenCalled();
  91. // The form should not be visible anymore.
  92. expect($form.is(":visible")).toBeFalsy();
  93. /* This request consists of sending an IQ
  94. * stanza of type='set' containing a <query/> element qualified by
  95. * the 'jabber:iq:roster' namespace, which in turn contains an
  96. * <item/> element that defines the new roster item; the <item/>
  97. * element MUST possess a 'jid' attribute, MAY possess a 'name'
  98. * attribute, MUST NOT possess a 'subscription' attribute, and MAY
  99. * contain one or more <group/> child elements:
  100. *
  101. * <iq type='set' id='set1'>
  102. * <query xmlns='jabber:iq:roster'>
  103. * <item
  104. * jid='contact@example.org'
  105. * name='MyContact'>
  106. * <group>MyBuddies</group>
  107. * </item>
  108. * </query>
  109. * </iq>
  110. */
  111. expect(converse.roster.sendContactAddIQ).toHaveBeenCalled();
  112. expect(sentStanza.toLocaleString()).toBe(
  113. "<iq type='set' xmlns='jabber:client' id='"+iq_id+"'>"+
  114. "<query xmlns='jabber:iq:roster'>"+
  115. "<item jid='contact@example.org' name='contact@example.org'/>"+
  116. "</query>"+
  117. "</iq>"
  118. );
  119. /* As a result, the user's server (1) MUST initiate a roster push
  120. * for the new roster item to all available resources associated
  121. * with this user that have requested the roster, setting the
  122. * 'subscription' attribute to a value of "none"; and (2) MUST
  123. * reply to the sending resource with an IQ result indicating the
  124. * success of the roster set:
  125. *
  126. * <iq type='set'>
  127. * <query xmlns='jabber:iq:roster'>
  128. * <item
  129. * jid='contact@example.org'
  130. * subscription='none'
  131. * name='MyContact'>
  132. * <group>MyBuddies</group>
  133. * </item>
  134. * </query>
  135. * </iq>
  136. */
  137. var create = converse.roster.create;
  138. spyOn(converse.connection, 'send').andCallFake(function (stanza) {
  139. sentStanza = stanza;
  140. });
  141. spyOn(converse.roster, 'create').andCallFake(function () {
  142. contact = create.apply(converse.roster, arguments);
  143. spyOn(contact, 'subscribe').andCallThrough();
  144. return contact;
  145. });
  146. stanza = $iq({'type': 'set'}).c('query', {'xmlns': 'jabber:iq:roster'})
  147. .c('item', {
  148. 'jid': 'contact@example.org',
  149. 'subscription': 'none',
  150. 'name': 'contact@example.org'});
  151. this.connection._dataRecv(test_utils.createRequest(stanza));
  152. /*
  153. * <iq type='result' id='set1'/>
  154. */
  155. stanza = $iq({'type': 'result', 'id':iq_id});
  156. this.connection._dataRecv(test_utils.createRequest(stanza));
  157. // A contact should now have been created
  158. expect(this.roster.get('contact@example.org') instanceof converse.RosterContact).toBeTruthy();
  159. expect(contact.get('jid')).toBe('contact@example.org');
  160. expect(this.getVCard).toHaveBeenCalled();
  161. /* To subscribe to the contact's presence information,
  162. * the user's client MUST send a presence stanza of
  163. * type='subscribe' to the contact:
  164. *
  165. * <presence to='contact@example.org' type='subscribe'/>
  166. */
  167. expect(contact.subscribe).toHaveBeenCalled();
  168. expect(sentStanza.toLocaleString()).toBe( // Strophe adds the xmlns attr (although not in spec)
  169. "<presence to='contact@example.org' type='subscribe' xmlns='jabber:client'/>"
  170. );
  171. /* As a result, the user's server MUST initiate a second roster
  172. * push to all of the user's available resources that have
  173. * requested the roster, setting the contact to the pending
  174. * sub-state of the 'none' subscription state; this pending
  175. * sub-state is denoted by the inclusion of the ask='subscribe'
  176. * attribute in the roster item:
  177. *
  178. * <iq type='set'>
  179. * <query xmlns='jabber:iq:roster'>
  180. * <item
  181. * jid='contact@example.org'
  182. * subscription='none'
  183. * ask='subscribe'
  184. * name='MyContact'>
  185. * <group>MyBuddies</group>
  186. * </item>
  187. * </query>
  188. * </iq>
  189. */
  190. spyOn(converse.roster, "updateContact").andCallThrough();
  191. stanza = $iq({'type': 'set'}).c('query', {'xmlns': 'jabber:iq:roster'})
  192. .c('item', {
  193. 'jid': 'contact@example.org',
  194. 'subscription': 'none',
  195. 'ask': 'subscribe',
  196. 'name': 'contact@example.org'});
  197. this.connection._dataRecv(test_utils.createRequest(stanza));
  198. expect(converse.roster.updateContact).toHaveBeenCalled();
  199. }, this));
  200. waits(50); // Needed, due to debounce
  201. runs($.proxy(function () {
  202. // Check that the user is now properly shown as a pending
  203. // contact in the roster.
  204. var $header = $('a:contains("Pending contacts")');
  205. expect($header.length).toBe(1);
  206. expect($header.is(":visible")).toBeTruthy();
  207. var $contacts = $header.parent().nextUntil('dt', 'dd');
  208. expect($contacts.length).toBe(1);
  209. spyOn(contact, "acknowledgeSubscription").andCallThrough();
  210. /* Here we assume the "happy path" that the contact
  211. * approves the subscription request
  212. *
  213. * <presence
  214. * to='user@example.com'
  215. * from='contact@example.org'
  216. * type='subscribed'/>
  217. */
  218. stanza = $pres({
  219. 'to': converse.bare_jid,
  220. 'from': 'contact@example.org',
  221. 'type': 'subscribed'
  222. });
  223. sentStanza = ""; // Reset
  224. this.connection._dataRecv(test_utils.createRequest(stanza));
  225. /* Upon receiving the presence stanza of type "subscribed",
  226. * the user SHOULD acknowledge receipt of that
  227. * subscription state notification by sending a presence
  228. * stanza of type "subscribe".
  229. */
  230. expect(contact.acknowledgeSubscription).toHaveBeenCalled();
  231. expect(sentStanza.toLocaleString()).toBe( // Strophe adds the xmlns attr (although not in spec)
  232. "<presence type='subscribe' to='contact@example.org' xmlns='jabber:client'/>"
  233. );
  234. /* The user's server MUST initiate a roster push to all of the user's
  235. * available resources that have requested the roster,
  236. * containing an updated roster item for the contact with
  237. * the 'subscription' attribute set to a value of "to";
  238. *
  239. * <iq type='set'>
  240. * <query xmlns='jabber:iq:roster'>
  241. * <item
  242. * jid='contact@example.org'
  243. * subscription='to'
  244. * name='MyContact'>
  245. * <group>MyBuddies</group>
  246. * </item>
  247. * </query>
  248. * </iq>
  249. */
  250. iq_id = converse.connection.getUniqueId('roster');
  251. stanza = $iq({'type': 'set', 'id': iq_id})
  252. .c('query', {'xmlns': 'jabber:iq:roster'})
  253. .c('item', {
  254. 'jid': 'contact@example.org',
  255. 'subscription': 'to',
  256. 'name': 'contact@example.org'});
  257. this.connection._dataRecv(test_utils.createRequest(stanza));
  258. // Check that the IQ set was acknowledged.
  259. expect(sentStanza.toLocaleString()).toBe( // Strophe adds the xmlns attr (although not in spec)
  260. "<iq type='result' id='"+iq_id+"' from='dummy@localhost/resource' xmlns='jabber:client'/>"
  261. );
  262. expect(converse.roster.updateContact).toHaveBeenCalled();
  263. // The contact should now be visible as an existing
  264. // contact (but still offline).
  265. $header = $('a:contains("My contacts")');
  266. expect($header.length).toBe(1);
  267. expect($header.is(":visible")).toBeTruthy();
  268. $contacts = $header.parent().nextUntil('dt', 'dd');
  269. expect($contacts.length).toBe(1);
  270. // Check that it has the right classes and text
  271. expect($contacts.hasClass('to')).toBeTruthy();
  272. expect($contacts.hasClass('both')).toBeFalsy();
  273. expect($contacts.hasClass('current-xmpp-contact')).toBeTruthy();
  274. expect($contacts.text().trim()).toBe('Contact');
  275. expect(contact.get('chat_status')).toBe('offline');
  276. /* <presence
  277. * from='contact@example.org/resource'
  278. * to='user@example.com/resource'/>
  279. */
  280. stanza = $pres({'to': converse.bare_jid, 'from': 'contact@example.org/resource'});
  281. this.connection._dataRecv(test_utils.createRequest(stanza));
  282. // Now the contact should also be online.
  283. expect(contact.get('chat_status')).toBe('online');
  284. /* Section 8.3. Creating a Mutual Subscription
  285. *
  286. * If the contact wants to create a mutual subscription,
  287. * the contact MUST send a subscription request to the
  288. * user.
  289. *
  290. * <presence from='contact@example.org' to='user@example.com' type='subscribe'/>
  291. */
  292. spyOn(contact, 'authorize').andCallThrough();
  293. spyOn(this.roster, 'handleIncomingSubscription').andCallThrough();
  294. stanza = $pres({
  295. 'to': converse.bare_jid,
  296. 'from': 'contact@example.org/resource',
  297. 'type': 'subscribe'});
  298. this.connection._dataRecv(test_utils.createRequest(stanza));
  299. expect(this.roster.handleIncomingSubscription).toHaveBeenCalled();
  300. /* The user's client MUST send a presence stanza of type
  301. * "subscribed" to the contact in order to approve the
  302. * subscription request.
  303. *
  304. * <presence to='contact@example.org' type='subscribed'/>
  305. */
  306. expect(contact.authorize).toHaveBeenCalled();
  307. expect(sentStanza.toLocaleString()).toBe(
  308. "<presence to='contact@example.org' type='subscribed' xmlns='jabber:client'/>"
  309. );
  310. /* As a result, the user's server MUST initiate a
  311. * roster push containing a roster item for the
  312. * contact with the 'subscription' attribute set to
  313. * a value of "both".
  314. *
  315. * <iq type='set'>
  316. * <query xmlns='jabber:iq:roster'>
  317. * <item
  318. * jid='contact@example.org'
  319. * subscription='both'
  320. * name='MyContact'>
  321. * <group>MyBuddies</group>
  322. * </item>
  323. * </query>
  324. * </iq>
  325. */
  326. stanza = $iq({'type': 'set'}).c('query', {'xmlns': 'jabber:iq:roster'})
  327. .c('item', {
  328. 'jid': 'contact@example.org',
  329. 'subscription': 'both',
  330. 'name': 'contact@example.org'});
  331. this.connection._dataRecv(test_utils.createRequest(stanza));
  332. expect(converse.roster.updateContact).toHaveBeenCalled();
  333. // The class on the contact will now have switched.
  334. expect($contacts.hasClass('to')).toBeFalsy();
  335. expect($contacts.hasClass('both')).toBeTruthy();
  336. }, this));
  337. }, converse));
  338. it("Alternate Flow: Contact Declines Subscription Request", $.proxy(function () {
  339. // TODO
  340. }, converse));
  341. it("Creating a Mutual Subscription", $.proxy(function () {
  342. // TODO
  343. }, converse));
  344. it("Alternate Flow: User Declines Subscription Request", $.proxy(function () {
  345. // TODO
  346. }, converse));
  347. }, converse, mock, test_utils));
  348. }, converse, mock, test_utils));
  349. }));