Test that device lists can get updated via PEP

Fix various bugs in the process.

updates #497
This commit is contained in:
JC Brand 2018-05-20 13:41:16 +02:00
parent 41db49ffca
commit ddd0ef8e20
4 changed files with 254 additions and 78 deletions

View File

@ -320,52 +320,54 @@
[{'category': 'pubsub', 'type': 'pep'}], [{'category': 'pubsub', 'type': 'pep'}],
['http://jabber.org/protocol/pubsub#publish-options'] ['http://jabber.org/protocol/pubsub#publish-options']
).then(function () { ).then(function () {
test_utils.waitUntil(function () { return test_utils.waitUntil(() => _converse.bookmarks);
return _converse.bookmarks; }).then(function () {
}, 300).then(function () { // Emit here instead of mocking fetching of bookmarks.
/* The stored data is automatically pushed to all of the user's _converse.emit('bookmarksInitialized');
* connected resources.
*
* Publisher receives event notification
* -------------------------------------
* <message from='juliet@capulet.lit'
* to='juliet@capulet.lit/balcony'
* type='headline'
* id='rnfoo1'>
* <event xmlns='http://jabber.org/protocol/pubsub#event'>
* <items node='storage:bookmarks'>
* <item id='current'>
* <storage xmlns='storage:bookmarks'>
* <conference name='The Play&apos;s the Thing'
* autojoin='true'
* jid='theplay@conference.shakespeare.lit'>
* <nick>JC</nick>
* </conference>
* </storage>
* </item>
* </items>
* </event>
* </message>
*/
var stanza = $msg({
'from': 'dummy@localhost',
'to': 'dummy@localhost/resource',
'type': 'headline',
'id': 'rnfoo1'
}).c('event', {'xmlns': 'http://jabber.org/protocol/pubsub#event'})
.c('items', {'node': 'storage:bookmarks'})
.c('item', {'id': 'current'})
.c('storage', {'xmlns': 'storage:bookmarks'})
.c('conference', {'name': 'The Play&apos;s the Thing',
'autojoin': 'true',
'jid':'theplay@conference.shakespeare.lit'})
.c('nick').t('JC');
_converse.connection._dataRecv(test_utils.createRequest(stanza)); /* The stored data is automatically pushed to all of the user's
expect(_converse.bookmarks.length).toBe(1); * connected resources.
expect(_converse.chatboxviews.get('theplay@conference.shakespeare.lit')).not.toBeUndefined(); *
done(); * Publisher receives event notification
}); * -------------------------------------
* <message from='juliet@capulet.lit'
* to='juliet@capulet.lit/balcony'
* type='headline'
* id='rnfoo1'>
* <event xmlns='http://jabber.org/protocol/pubsub#event'>
* <items node='storage:bookmarks'>
* <item id='current'>
* <storage xmlns='storage:bookmarks'>
* <conference name='The Play&apos;s the Thing'
* autojoin='true'
* jid='theplay@conference.shakespeare.lit'>
* <nick>JC</nick>
* </conference>
* </storage>
* </item>
* </items>
* </event>
* </message>
*/
var stanza = $msg({
'from': 'dummy@localhost',
'to': 'dummy@localhost/resource',
'type': 'headline',
'id': 'rnfoo1'
}).c('event', {'xmlns': 'http://jabber.org/protocol/pubsub#event'})
.c('items', {'node': 'storage:bookmarks'})
.c('item', {'id': 'current'})
.c('storage', {'xmlns': 'storage:bookmarks'})
.c('conference', {'name': 'The Play&apos;s the Thing',
'autojoin': 'true',
'jid':'theplay@conference.shakespeare.lit'})
.c('nick').t('JC');
_converse.connection._dataRecv(test_utils.createRequest(stanza));
return test_utils.waitUntil(() => _converse.bookmarks.length);
}).then(function () {
expect(_converse.bookmarks.length).toBe(1);
expect(_converse.chatboxviews.get('theplay@conference.shakespeare.lit')).not.toBeUndefined();
done();
}); });
})); }));

View File

@ -4,6 +4,7 @@
var Strophe = converse.env.Strophe; var Strophe = converse.env.Strophe;
var b64_sha1 = converse.env.b64_sha1; var b64_sha1 = converse.env.b64_sha1;
var $iq = converse.env.$iq; var $iq = converse.env.$iq;
var $msg = converse.env.$msg;
var _ = converse.env._; var _ = converse.env._;
var u = converse.env.utils; var u = converse.env.utils;
@ -17,6 +18,165 @@
done(); done();
})); }));
it("updates the user's device list based on PEP messages",
mock.initConverseWithPromises(
null, ['rosterGroupsFetched'], {},
function (done, _converse) {
let iq_stanza;
test_utils.createContacts(_converse, 'current');
const contact_jid = mock.cur_names[3].replace(/ /g,'.').toLowerCase() + '@localhost';
test_utils.waitUntil(function () {
return _.filter(
_converse.connection.IQ_stanzas,
(iq) => {
const node = iq.nodeTree.querySelector('iq[to="'+_converse.bare_jid+'"] query[node="eu.siacs.conversations.axolotl.devicelist"]');
if (node) { iq_stanza = iq.nodeTree;}
return node;
}).length;
}).then(function () {
expect(iq_stanza.outerHTML).toBe(
'<iq type="get" from="dummy@localhost" to="dummy@localhost" xmlns="jabber:client" id="'+iq_stanza.getAttribute("id")+'">'+
'<query xmlns="http://jabber.org/protocol/disco#items" '+
'node="eu.siacs.conversations.axolotl.devicelist"/>'+
'</iq>');
const stanza = $iq({
'from': contact_jid,
'id': iq_stanza.getAttribute('id'),
'to': _converse.bare_jid,
'type': 'result',
}).c('query', {
'xmlns': 'http://jabber.org/protocol/disco#items',
'node': 'eu.siacs.conversations.axolotl.devicelist'
}).c('device', {'id': '555'}).up()
_converse.connection._dataRecv(test_utils.createRequest(stanza));
expect(_converse.devicelists.length).toBe(1);
const devicelist = _converse.devicelists.get(_converse.bare_jid);
expect(devicelist.devices.length).toBe(1);
expect(devicelist.devices.at(0).get('id')).toBe('555');
return test_utils.waitUntil(() => _converse.devicelists);
}).then(function () {
// We simply emit, to avoid doing all the setup work
_converse.emit('OMEMOInitialized');
let stanza = $msg({
'from': contact_jid,
'to': _converse.bare_jid,
'type': 'headline',
'id': 'update_01',
}).c('event', {'xmlns': 'http://jabber.org/protocol/pubsub#event'})
.c('items', {'node': 'eu.siacs.conversations.axolotl.devicelist'})
.c('item')
.c('list', {'xmlns': 'eu.siacs.conversations.axolotl'})
.c('device', {'id': '1234'})
.c('device', {'id': '4223'})
_converse.connection._dataRecv(test_utils.createRequest(stanza));
expect(_converse.devicelists.length).toBe(2);
let devices = _converse.devicelists.get(contact_jid).devices;
expect(devices.length).toBe(2);
expect(_.map(devices.models, 'attributes.id').sort().join()).toBe('1234,4223');
expect(devices.get('1234').get('active')).toBe(true);
expect(devices.get('4223').get('active')).toBe(true);
stanza = $msg({
'from': contact_jid,
'to': _converse.bare_jid,
'type': 'headline',
'id': 'update_02',
}).c('event', {'xmlns': 'http://jabber.org/protocol/pubsub#event'})
.c('items', {'node': 'eu.siacs.conversations.axolotl.devicelist'})
.c('item')
.c('list', {'xmlns': 'eu.siacs.conversations.axolotl'})
.c('device', {'id': '4223'})
.c('device', {'id': '4224'})
_converse.connection._dataRecv(test_utils.createRequest(stanza));
expect(_converse.devicelists.length).toBe(2);
expect(devices.length).toBe(3);
expect(_.map(devices.models, 'attributes.id').sort().join()).toBe('1234,4223,4224');
expect(devices.get('1234').get('active')).toBe(false);
expect(devices.get('4223').get('active')).toBe(true);
expect(devices.get('4224').get('active')).toBe(true);
// Check that own devicelist gets updated
stanza = $msg({
'from': _converse.bare_jid,
'to': _converse.bare_jid,
'type': 'headline',
'id': 'update_03',
}).c('event', {'xmlns': 'http://jabber.org/protocol/pubsub#event'})
.c('items', {'node': 'eu.siacs.conversations.axolotl.devicelist'})
.c('item')
.c('list', {'xmlns': 'eu.siacs.conversations.axolotl'})
.c('device', {'id': '555'})
.c('device', {'id': '777'})
_converse.connection._dataRecv(test_utils.createRequest(stanza));
expect(_converse.devicelists.length).toBe(2);
devices = _converse.devicelists.get(_converse.bare_jid).devices;
expect(devices.length).toBe(3);
expect(_.map(devices.models, 'attributes.id').sort().join()).toBe('123456789,555,777');
expect(devices.get('123456789').get('active')).toBe(true);
expect(devices.get('555').get('active')).toBe(true);
expect(devices.get('777').get('active')).toBe(true);
_converse.connection.IQ_stanzas = [];
// Check that own device gets re-added
stanza = $msg({
'from': _converse.bare_jid,
'to': _converse.bare_jid,
'type': 'headline',
'id': 'update_03',
}).c('event', {'xmlns': 'http://jabber.org/protocol/pubsub#event'})
.c('items', {'node': 'eu.siacs.conversations.axolotl.devicelist'})
.c('item')
.c('list', {'xmlns': 'eu.siacs.conversations.axolotl'})
.c('device', {'id': '444'})
_converse.connection._dataRecv(test_utils.createRequest(stanza));
return test_utils.waitUntil(function () {
return _.filter(
_converse.connection.IQ_stanzas,
(iq) => {
const node = iq.nodeTree.querySelector('iq[from="'+_converse.bare_jid+'"] publish[node="eu.siacs.conversations.axolotl.devicelist"]');
if (node) { iq_stanza = iq.nodeTree;}
return node;
}).length;
});
}).then(function () {
// Check that our own device is added again, but that removed
// devices are not added.
expect(iq_stanza.outerHTML).toBe(
'<iq from="dummy@localhost" type="set" xmlns="jabber:client" id="'+iq_stanza.getAttribute('id')+'">'+
'<pubsub xmlns="http://jabber.org/protocol/pubsub">'+
'<publish node="eu.siacs.conversations.axolotl.devicelist">'+
'<item>'+
'<list xmlns="eu.siacs.conversations.axolotl"/>'+
'<device id="123456789"/>'+
'<device id="444"/>'+
'</item>'+
'</publish>'+
'</pubsub>'+
'</iq>');
expect(_converse.devicelists.length).toBe(2);
const devices = _converse.devicelists.get(_converse.bare_jid).devices;
// The device id for this device (123456789) was also generated and added to the list,
// which is why we have 4 devices now.
expect(devices.length).toBe(4);
expect(_.map(devices.models, 'attributes.id').sort().join()).toBe('123456789,444,555,777');
expect(devices.get('123456789').get('active')).toBe(true);
expect(devices.get('444').get('active')).toBe(true);
expect(devices.get('555').get('active')).toBe(false);
expect(devices.get('777').get('active')).toBe(false);
done();
});
}));
it("adds a toolbar button for starting an encrypted chat session", it("adds a toolbar button for starting an encrypted chat session",
mock.initConverseWithPromises( mock.initConverseWithPromises(
null, ['rosterGroupsFetched'], {}, null, ['rosterGroupsFetched'], {},
@ -60,7 +220,7 @@
test_utils.openChatBoxFor(_converse, contact_jid); test_utils.openChatBoxFor(_converse, contact_jid);
return test_utils.waitUntil(() => { return test_utils.waitUntil(() => {
return _.filter(_converse.connection.IQ_stanzas, function (iq) { return _.filter(_converse.connection.IQ_stanzas, function (iq) {
const node = iq.nodeTree.querySelector('publish[xmlns="eu.siacs.conversations.axolotl.devicelist"]'); const node = iq.nodeTree.querySelector('publish[node="eu.siacs.conversations.axolotl.devicelist"]');
if (node) { iq_stanza = iq.nodeTree; } if (node) { iq_stanza = iq.nodeTree; }
return node; return node;
}).length; }).length;
@ -69,7 +229,7 @@
expect(iq_stanza.outerHTML).toBe( expect(iq_stanza.outerHTML).toBe(
'<iq from="dummy@localhost" type="set" xmlns="jabber:client" id="'+iq_stanza.getAttribute('id')+'">'+ '<iq from="dummy@localhost" type="set" xmlns="jabber:client" id="'+iq_stanza.getAttribute('id')+'">'+
'<pubsub xmlns="http://jabber.org/protocol/pubsub">'+ '<pubsub xmlns="http://jabber.org/protocol/pubsub">'+
'<publish xmlns="eu.siacs.conversations.axolotl.devicelist">'+ '<publish node="eu.siacs.conversations.axolotl.devicelist">'+
'<item>'+ '<item>'+
'<list xmlns="eu.siacs.conversations.axolotl"/>'+ '<list xmlns="eu.siacs.conversations.axolotl"/>'+
'<device id="482886413b977930064a5888b92134fe"/>'+ '<device id="482886413b977930064a5888b92134fe"/>'+

View File

@ -579,8 +579,10 @@
// Add a handler for bookmarks pushed from other connected clients // Add a handler for bookmarks pushed from other connected clients
// (from the same user obviously) // (from the same user obviously)
_converse.connection.addHandler((message) => { _converse.connection.addHandler((message) => {
if (message.querySelector('event[xmlns="'+Strophe.NS.PUBSUB+'#event"]')) { if (sizzle('event[xmlns="'+Strophe.NS.PUBSUB+'#event"] items[node="storage:bookmarks"]', message).length) {
_converse.bookmarks.createBookmarksFromStanza(message); _converse.api.waitUntil('bookmarksInitialized')
.then(() => _converse.bookmarks.createBookmarksFromStanza(message))
.catch(_.partial(_converse.log, _, Strophe.LogLevel.FATAL));
} }
}, null, 'message', 'headline', null, _converse.bare_jid); }, null, 'message', 'headline', null, _converse.bare_jid);
}); });

View File

@ -519,11 +519,11 @@
'from': _converse.bare_jid, 'from': _converse.bare_jid,
'type': 'set' 'type': 'set'
}).c('pubsub', {'xmlns': Strophe.NS.PUBSUB}) }).c('pubsub', {'xmlns': Strophe.NS.PUBSUB})
.c('publish', {'xmlns': Strophe.NS.OMEMO_DEVICELIST}) .c('publish', {'node': Strophe.NS.OMEMO_DEVICELIST})
.c('item') .c('item')
.c('list', {'xmlns': Strophe.NS.OMEMO}).up() .c('list', {'xmlns': Strophe.NS.OMEMO}).up()
this.devices.each((device) => { _.each(this.devices.where({'active': true}), (device) => {
stanza.c('device', {'id': device.get('id')}).up(); stanza.c('device', {'id': device.get('id')}).up();
}); });
_converse.connection.sendIQ(stanza, resolve, reject, _converse.IQ_TIMEOUT); _converse.connection.sendIQ(stanza, resolve, reject, _converse.IQ_TIMEOUT);
@ -584,48 +584,59 @@
/* If our own device is not on the list, add it. /* If our own device is not on the list, add it.
* Also, deduplicate devices if necessary. * Also, deduplicate devices if necessary.
*/ */
return new Promise((resolve, reject) => { const devicelist = _converse.devicelists.get(_converse.bare_jid),
restoreOMEMOSession().then(() => { device_id = _converse.omemo_store.get('device_id'),
const devicelist = _converse.devicelists.get(_converse.bare_jid); own_device = devicelist.devices.findWhere({'id': device_id});
const device_id = _converse.omemo_store.get('device_id');
if (!devicelist.devices.findWhere({'id': device_id})) { if (!own_device) {
return devicelist.addDeviceToList(device_id).then(resolve).catch(reject); return devicelist.addDeviceToList(device_id);
} } else if (!own_device.get('active')) {
resolve(); own_device.set('active', true, {'silent': true});
}).catch(_.partial(_converse.log, _, Strophe.LogLevel.ERROR)); return devicelist.addDeviceToList(device_id);
}); } else {
return Promise.resolve();
}
} }
function updateBundleFromStanza (stanza) { function updateBundleFromStanza (stanza) {
const items_el = sizzle(`items[node="${Strophe.NS.OMEMO_BUNDLES}"]`, stanza), const items_el = sizzle(`items[node="${Strophe.NS.OMEMO_BUNDLES}"]`, stanza).pop();
device_id = items_el.getAttribute('node').split(':')[1], if (!items_el) {
return;
}
const device_id = items_el.getAttribute('node').split(':')[1],
from = stanza.getAttribute('from'), from = stanza.getAttribute('from'),
bundle_el = sizzle(`item list[xmlns="${Strophe.NS.OMEMO}"] bundle`, items_el).pop(), bundle_el = sizzle(`item list[xmlns="${Strophe.NS.OMEMO}"] bundle`, items_el).pop(),
bundle = parseBundle(bundle_el); device = _converse.devicelists.get(from).devices.get(device_id);
device.save({'bundle': parseBundle(bundle_el)});
const device = _converse.devicelists.get(from).devices.get(device_id);
device.save({'bundle': bundle});
} }
function updateDevicesFromStanza (stanza) { function updateDevicesFromStanza (stanza) {
// TODO: check whether our own device_id is still on the list, const items_el = sizzle(`items[node="${Strophe.NS.OMEMO_DEVICELIST}"]`, stanza).pop();
// otherwise we need to update it. if (!items_el) {
return;
}
const device_ids = _.map( const device_ids = _.map(
sizzle(`items[node="${Strophe.NS.OMEMO_DEVICELIST}"] item list[xmlns="${Strophe.NS.OMEMO}"] device`, stanza), sizzle(`item list[xmlns="${Strophe.NS.OMEMO}"] device`, items_el),
(device) => device.getAttribute('id')); (device) => device.getAttribute('id')
);
const removed_ids = _.difference(_converse.devices.pluck('id'), device_ids); const jid = stanza.getAttribute('from'),
_.forEach(removed_ids, (removed_id) => _converse.devices.get(removed_id).set('active', false)); devicelist = _converse.devicelists.get(jid) || _converse.devicelists.create({'jid': jid}),
devices = devicelist.devices,
removed_ids = _.difference(devices.pluck('id'), device_ids);
_.forEach(removed_ids, (removed_id) => devices.get(removed_id).set('active', false));
_.forEach(device_ids, (device_id) => { _.forEach(device_ids, (device_id) => {
const dev = _converse.devices.get(device_id); const dev = devices.get(device_id);
if (dev) { if (dev) {
dev.save({'active': true}); dev.save({'active': true});
} else { } else {
_converse.devices.create({'id': device_id}) devices.create({'id': device_id})
} }
}); });
// Make sure our own device is on the list (i.e. if it was
// removed, add it again.
updateOwnDeviceList();
} }
function registerPEPPushHandler () { function registerPEPPushHandler () {
@ -634,9 +645,9 @@
if (message.querySelector('event[xmlns="'+Strophe.NS.PUBSUB+'#event"]')) { if (message.querySelector('event[xmlns="'+Strophe.NS.PUBSUB+'#event"]')) {
updateDevicesFromStanza(message); updateDevicesFromStanza(message);
updateBundleFromStanza(message); updateBundleFromStanza(message);
updateOwnDeviceList();
} }
}, null, 'message', 'headline', null, _converse.bare_jid); return true;
}, null, 'message', 'headline');
} }
function restoreOMEMOSession () { function restoreOMEMOSession () {
@ -655,6 +666,7 @@
b64_sha1(`converse.devicelists-${_converse.bare_jid}`) b64_sha1(`converse.devicelists-${_converse.bare_jid}`)
); );
fetchOwnDevices() fetchOwnDevices()
.then(() => restoreOMEMOSession())
.then(() => updateOwnDeviceList()) .then(() => updateOwnDeviceList())
.then(() => publishBundle()) .then(() => publishBundle())
.then(() => _converse.emit('OMEMOInitialized')) .then(() => _converse.emit('OMEMOInitialized'))