(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.waitUntilFeatureSupportConfirmed(_converse, '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.$('.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).find('input').val('contact@example.org');
$(form).submit();
/* 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 () {
return $('a:contains("Pending contacts")').length;
}, 300);
}).then(function () {
var $header = $('a:contains("Pending contacts")');
expect($header.length).toBe(1);
expect($header.is(":visible")).toBeTruthy();
var $contacts = $header.parent().nextUntil('dt', 'dd');
expect($contacts.length).toBe(1);
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).
$header = $('a:contains("My contacts")');
expect($header.length).toBe(1);
expect($header.is(":visible")).toBeTruthy();
$contacts = $header.parent().nextUntil('dt', 'dd');
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);
});
test_utils.waitUntil(function () {
return $('a:contains("My contacts")').length;
}).then(function () {
var $header = $('a:contains("My contacts")');
// remove the first user
$($header.parent().nextUntil('dt', 'dd').find('.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));
test_utils.waitUntil(function () {
return $('a:contains("Contact requests")').length;
}).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().nextUntil('dt', 'dd');
expect($contacts.length).toBe(1);
done();
});
}));
});
});
}));