(function (root, factory) { define([ "jasmine", "jquery", "converse-core", "mock", "test-utils"], factory); } (this, function (jasmine, $, converse, mock, test_utils) { "use strict"; var Strophe = converse.env.Strophe; var $iq = converse.env.$iq; var $pres = converse.env.$pres; var _ = converse.env._; // See: // https://xmpp.org/rfcs/rfc3921.html describe("The Protocol", function () { describe("Integration of Roster Items and Presence Subscriptions", function () { // Stub the trimChat method. It causes havoc when running with // phantomJS. /* Some level of integration between roster items and presence * subscriptions is normally expected by an instant messaging user * regarding the user's subscriptions to and from other contacts. This * section describes the level of integration that MUST be supported * within an XMPP instant messaging applications. * * There are four primary subscription states: * * None -- the user does not have a subscription to the contact's * presence information, and the contact does not have a subscription * to the user's presence information * To -- the user has a subscription to the contact's presence * information, but the contact does not have a subscription to the * user's presence information * From -- the contact has a subscription to the user's presence * information, but the user does not have a subscription to the * contact's presence information * Both -- both the user and the contact have subscriptions to each * other's presence information (i.e., the union of 'from' and 'to') * * Each of these states is reflected in the roster of both the user and * the contact, thus resulting in durable subscription states. * * The 'from' and 'to' addresses are OPTIONAL in roster pushes; if * included, their values SHOULD be the full JID of the resource for * that session. A client MUST acknowledge each roster push with an IQ * stanza of type "result". */ it("Subscribe to contact, contact accepts and subscribes back", mock.initConverseWithPromises( null, ['rosterGroupsFetched'], { roster_groups: false }, function (done, _converse) { var contact, sent_stanza, IQ_id, stanza; test_utils.waitUntilDiscoConfirmed(_converse, 'localhost', [], ['vcard-temp']) .then(function () { return test_utils.waitUntil(function () { return _converse.xmppstatus.get('fullname'); }, 300); }).then(function () { /* The process by which a user subscribes to a contact, including * the interaction between roster items and subscription states. */ test_utils.openControlBox(_converse); var panel = _converse.chatboxviews.get('controlbox').contactspanel; spyOn(panel, "addContactFromForm").and.callThrough(); spyOn(_converse.roster, "addAndSubscribe").and.callThrough(); spyOn(_converse.roster, "addContact").and.callThrough(); spyOn(_converse.roster, "sendContactAddIQ").and.callThrough(); spyOn(_converse.api.vcard, "get").and.callThrough(); var sendIQ = _converse.connection.sendIQ; spyOn(_converse.connection, 'sendIQ').and.callFake(function (iq, callback, errback) { sent_stanza = iq; IQ_id = sendIQ.bind(this)(iq, callback, errback); }); panel.delegateEvents(); // Rebind all events so that our spy gets called /* Add a new contact through the UI */ var form = panel.el.querySelector('form.add-xmpp-contact'); expect(_.isNull(form)).toBeTruthy(); // Click the "Add a contact" link. panel.el.querySelector('.toggle-xmpp-contact-form').click(); // Check that the form appears form = panel.el.querySelector('form.add-xmpp-contact'); expect(form.parentElement.offsetHeight).not.toBe(0); expect(_.includes(form.parentElement.classList, 'collapsed')).toBeFalsy(); // Fill in the form and submit form.querySelector('input').value = 'contact@example.org'; form.querySelector('[type="submit"]').click(); /* In preparation for being able to render the contact in the * user's client interface and for the server to keep track of the * subscription, the user's client SHOULD perform a "roster set" * for the new roster item. */ expect(panel.addContactFromForm).toHaveBeenCalled(); expect(_converse.roster.addAndSubscribe).toHaveBeenCalled(); expect(_converse.roster.addContact).toHaveBeenCalled(); // The form should not be visible anymore (by virtue of its // parent being collapsed) expect(form.parentElement.offsetHeight).toBe(0); expect(_.includes(form.parentElement.classList, 'collapsed')).toBeTrue; /* _converse request consists of sending an IQ * stanza of type='set' containing a element qualified by * the 'jabber:iq:roster' namespace, which in turn contains an * element that defines the new roster item; the * element MUST possess a 'jid' attribute, MAY possess a 'name' * attribute, MUST NOT possess a 'subscription' attribute, and MAY * contain one or more child elements: * * * * * MyBuddies * * * */ expect(_converse.roster.sendContactAddIQ).toHaveBeenCalled(); expect(sent_stanza.toLocaleString()).toBe( ""+ ""+ ""+ ""+ "" ); /* As a result, the user's server (1) MUST initiate a roster push * for the new roster item to all available resources associated * with _converse user that have requested the roster, setting the * 'subscription' attribute to a value of "none"; and (2) MUST * reply to the sending resource with an IQ result indicating the * success of the roster set: * * * * * MyBuddies * * * */ var create = _converse.roster.create; var sent_stanzas = []; spyOn(_converse.connection, 'send').and.callFake(function (stanza) { sent_stanza = stanza; sent_stanzas.push(stanza.toLocaleString()); }); spyOn(_converse.roster, 'create').and.callFake(function () { contact = create.apply(_converse.roster, arguments); spyOn(contact, 'subscribe').and.callThrough(); return contact; }); stanza = $iq({'type': 'set'}).c('query', {'xmlns': 'jabber:iq:roster'}) .c('item', { 'jid': 'contact@example.org', 'subscription': 'none', 'name': 'contact@example.org'}); _converse.connection._dataRecv(test_utils.createRequest(stanza)); /* * */ stanza = $iq({'type': 'result', 'id':IQ_id}); _converse.connection._dataRecv(test_utils.createRequest(stanza)); // A contact should now have been created expect(_converse.roster.get('contact@example.org') instanceof _converse.RosterContact).toBeTruthy(); expect(contact.get('jid')).toBe('contact@example.org'); expect(_converse.api.vcard.get).toHaveBeenCalled(); /* To subscribe to the contact's presence information, * the user's client MUST send a presence stanza of * type='subscribe' to the contact: * * */ return test_utils.waitUntil(function () { return sent_stanzas.length == 1; }, 300); }).then(function () { expect(contact.subscribe).toHaveBeenCalled(); expect(sent_stanza.toLocaleString()).toBe( // Strophe adds the xmlns attr (although not in spec) ""+ "Max Mustermann"+ "" ); /* As a result, the user's server MUST initiate a second roster * push to all of the user's available resources that have * requested the roster, setting the contact to the pending * sub-state of the 'none' subscription state; _converse pending * sub-state is denoted by the inclusion of the ask='subscribe' * attribute in the roster item: * * * * * MyBuddies * * * */ spyOn(_converse.roster, "updateContact").and.callThrough(); stanza = $iq({'type': 'set', 'from': 'dummy@localhost'}) .c('query', {'xmlns': 'jabber:iq:roster'}) .c('item', { 'jid': 'contact@example.org', 'subscription': 'none', 'ask': 'subscribe', 'name': 'contact@example.org'}); _converse.connection._dataRecv(test_utils.createRequest(stanza)); expect(_converse.roster.updateContact).toHaveBeenCalled(); // Check that the user is now properly shown as a pending // contact in the roster. return test_utils.waitUntil(function () { var $header = $('a:contains("Pending contacts")'); var $contacts = $header.parent().find('li:visible'); return $contacts.length; }, 600); }).then(function () { var $header = $('a:contains("Pending contacts")'); var $contacts = $header.parent().find('li'); expect($contacts.length).toBe(1); expect($contacts.is(':visible')).toBeTruthy(); spyOn(contact, "ackSubscribe").and.callThrough(); /* Here we assume the "happy path" that the contact * approves the subscription request * * */ stanza = $pres({ 'to': _converse.bare_jid, 'from': 'contact@example.org', 'type': 'subscribed' }); sent_stanza = ""; // Reset _converse.connection._dataRecv(test_utils.createRequest(stanza)); /* Upon receiving the presence stanza of type "subscribed", * the user SHOULD acknowledge receipt of that * subscription state notification by sending a presence * stanza of type "subscribe". */ expect(contact.ackSubscribe).toHaveBeenCalled(); expect(sent_stanza.toLocaleString()).toBe( // Strophe adds the xmlns attr (although not in spec) "" ); /* The user's server MUST initiate a roster push to all of the user's * available resources that have requested the roster, * containing an updated roster item for the contact with * the 'subscription' attribute set to a value of "to"; * * * * * MyBuddies * * * */ IQ_id = _converse.connection.getUniqueId('roster'); stanza = $iq({'type': 'set', 'id': IQ_id}) .c('query', {'xmlns': 'jabber:iq:roster'}) .c('item', { 'jid': 'contact@example.org', 'subscription': 'to', 'name': 'contact@example.org'}); _converse.connection._dataRecv(test_utils.createRequest(stanza)); // Check that the IQ set was acknowledged. expect(sent_stanza.toLocaleString()).toBe( // Strophe adds the xmlns attr (although not in spec) "" ); expect(_converse.roster.updateContact).toHaveBeenCalled(); // The contact should now be visible as an existing // contact (but still offline). return test_utils.waitUntil(function () { var $header = $('a:contains("My contacts")'); var $contacts = $header.parent().find('li:visible'); return $contacts.length; }, 600); }).then(function () { var $header = $('a:contains("My contacts")'); expect($header.length).toBe(1); expect($header.is(":visible")).toBeTruthy(); var $contacts = $header.parent().find('li'); expect($contacts.length).toBe(1); // Check that it has the right classes and text expect($contacts.hasClass('to')).toBeTruthy(); expect($contacts.hasClass('both')).toBeFalsy(); expect($contacts.hasClass('current-xmpp-contact')).toBeTruthy(); expect($contacts.text().trim()).toBe('Contact'); expect(contact.get('chat_status')).toBe('offline'); /* */ stanza = $pres({'to': _converse.bare_jid, 'from': 'contact@example.org/resource'}); _converse.connection._dataRecv(test_utils.createRequest(stanza)); // Now the contact should also be online. expect(contact.get('chat_status')).toBe('online'); /* Section 8.3. Creating a Mutual Subscription * * If the contact wants to create a mutual subscription, * the contact MUST send a subscription request to the * user. * * */ spyOn(contact, 'authorize').and.callThrough(); spyOn(_converse.roster, 'handleIncomingSubscription').and.callThrough(); stanza = $pres({ 'to': _converse.bare_jid, 'from': 'contact@example.org/resource', 'type': 'subscribe'}); _converse.connection._dataRecv(test_utils.createRequest(stanza)); expect(_converse.roster.handleIncomingSubscription).toHaveBeenCalled(); /* The user's client MUST send a presence stanza of type * "subscribed" to the contact in order to approve the * subscription request. * * */ expect(contact.authorize).toHaveBeenCalled(); expect(sent_stanza.toLocaleString()).toBe( "" ); /* As a result, the user's server MUST initiate a * roster push containing a roster item for the * contact with the 'subscription' attribute set to * a value of "both". * * * * * MyBuddies * * * */ stanza = $iq({'type': 'set'}).c('query', {'xmlns': 'jabber:iq:roster'}) .c('item', { 'jid': 'contact@example.org', 'subscription': 'both', 'name': 'contact@example.org'}); _converse.connection._dataRecv(test_utils.createRequest(stanza)); expect(_converse.roster.updateContact).toHaveBeenCalled(); // The class on the contact will now have switched. expect($contacts.hasClass('to')).toBeFalsy(); expect($contacts.hasClass('both')).toBeTruthy(); done(); }); })); it("Alternate Flow: Contact Declines Subscription Request", mock.initConverseWithPromises( null, ['rosterGroupsFetched'], {}, function (done, _converse) { /* The process by which a user subscribes to a contact, including * the interaction between roster items and subscription states. */ var contact, stanza, sent_stanza, sent_IQ; test_utils.openControlBox(_converse); // Add a new roster contact via roster push stanza = $iq({'type': 'set'}).c('query', {'xmlns': 'jabber:iq:roster'}) .c('item', { 'jid': 'contact@example.org', 'subscription': 'none', 'ask': 'subscribe', 'name': 'contact@example.org'}); _converse.connection._dataRecv(test_utils.createRequest(stanza)); // A pending contact should now exist. contact = _converse.roster.get('contact@example.org'); expect(_converse.roster.get('contact@example.org') instanceof _converse.RosterContact).toBeTruthy(); spyOn(contact, "ackUnsubscribe").and.callThrough(); spyOn(_converse.connection, 'send').and.callFake(function (stanza) { sent_stanza = stanza; }); spyOn(_converse.connection, 'sendIQ').and.callFake(function (iq, callback, errback) { sent_IQ = iq; }); /* We now assume the contact declines the subscription * requests. * * Upon receiving the presence stanza of type "unsubscribed" * addressed to the user, the user's server (1) MUST deliver * that presence stanza to the user and (2) MUST initiate a * roster push to all of the user's available resources that * have requested the roster, containing an updated roster * item for the contact with the 'subscription' attribute * set to a value of "none" and with no 'ask' attribute: * * * * * * * MyBuddies * * * */ // FIXME: also add the stanza = $pres({ 'to': _converse.bare_jid, 'from': 'contact@example.org', 'type': 'unsubscribed' }); _converse.connection._dataRecv(test_utils.createRequest(stanza)); /* Upon receiving the presence stanza of type "unsubscribed", * the user SHOULD acknowledge receipt of that subscription * state notification through either "affirming" it by * sending a presence stanza of type "unsubscribe */ expect(contact.ackUnsubscribe).toHaveBeenCalled(); expect(sent_stanza.toLocaleString()).toBe( "" ); /* _converse.js will then also automatically remove the * contact from the user's roster. */ expect(sent_IQ.toLocaleString()).toBe( ""+ ""+ ""+ ""+ "" ); done(); })); it("Unsubscribe to a contact when subscription is mutual", mock.initConverseWithPromises( null, ['rosterGroupsFetched'], { roster_groups: false }, function (done, _converse) { var sent_IQ, IQ_id, jid = 'annegreet.gomez@localhost'; test_utils.openControlBox(_converse); test_utils.createContacts(_converse, 'current'); spyOn(window, 'confirm').and.returnValue(true); // We now have a contact we want to remove expect(_converse.roster.get(jid) instanceof _converse.RosterContact).toBeTruthy(); var sendIQ = _converse.connection.sendIQ; spyOn(_converse.connection, 'sendIQ').and.callFake(function (iq, callback, errback) { sent_IQ = iq; IQ_id = sendIQ.bind(this)(iq, callback, errback); }); return test_utils.waitUntil(function () { var $header = $('a:contains("My contacts")'); var $contacts = $header.parent().find('li'); return $contacts.length; }, 600).then(function () { var $header = $('a:contains("My contacts")'); // remove the first user $header.parent().find('li .remove-xmpp-contact').get(0).click(); expect(window.confirm).toHaveBeenCalled(); /* Section 8.6 Removing a Roster Item and Cancelling All * Subscriptions * * First the user is removed from the roster * Because there may be many steps involved in completely * removing a roster item and cancelling subscriptions in * both directions, the roster management protocol includes * a "shortcut" method for doing so. The process may be * initiated no matter what the current subscription state * is by sending a roster set containing an item for the * contact with the 'subscription' attribute set to a value * of "remove": * * * * * * */ expect(sent_IQ.toLocaleString()).toBe( ""+ ""+ ""+ ""+ ""); // Receive confirmation from the contact's server // var stanza = $iq({'type': 'result', 'id':IQ_id}); _converse.connection._dataRecv(test_utils.createRequest(stanza)); // Our contact has now been removed expect(typeof _converse.roster.get(jid) === "undefined").toBeTruthy(); done(); }); })); it("Receiving a subscription request", mock.initConverseWithPromises( null, ['rosterGroupsFetched'], {}, function (done, _converse) { spyOn(_converse, "emit"); test_utils.openControlBox(_converse); test_utils.createContacts(_converse, 'current'); // Create some contacts so that we can test positioning /* */ var stanza = $pres({ 'to': _converse.bare_jid, 'from': 'contact@example.org', 'type': 'subscribe' }).c('nick', { 'xmlns': Strophe.NS.NICK, }).t('Clint Contact'); _converse.connection._dataRecv(test_utils.createRequest(stanza)); return test_utils.waitUntil(function () { var $header = $('a:contains("Contact requests")'); var $contacts = $header.parent().find('li:visible'); return $contacts.length; }, 600).then(function () { expect(_converse.emit).toHaveBeenCalledWith('contactRequest', jasmine.any(Object)); var $header = $('a:contains("Contact requests")'); expect($header.length).toBe(1); expect($header.is(":visible")).toBeTruthy(); var $contacts = $header.parent().find('li'); expect($contacts.length).toBe(1); done(); }); })); }); }); }));