Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix race in device list updates #431

Merged
merged 1 commit into from
May 5, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
128 changes: 128 additions & 0 deletions spec/unit/crypto/DeviceList.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import DeviceList from '../../../lib/crypto/DeviceList';
import MockStorageApi from '../../MockStorageApi';
import WebStorageSessionStore from '../../../lib/store/session/webstorage';
import testUtils from '../../test-utils';
import utils from '../../../lib/utils';

import expect from 'expect';
import q from 'q';

const signedDeviceList = {
"failures": {},
"device_keys": {
"@test1:sw1v.org": {
"HGKAWHRVJQ": {
"signatures": {
"@test1:sw1v.org": {
"ed25519:HGKAWHRVJQ":
"8PB450fxKDn5s8IiRZ2N2t6MiueQYVRLHFEzqIi1eLdxx1w" +
"XEPC1/1Uz9T4gwnKlMVAKkhB5hXQA/3kjaeLABw",
},
},
"user_id": "@test1:sw1v.org",
"keys": {
"ed25519:HGKAWHRVJQ":
"0gI/T6C+mn1pjtvnnW2yB2l1IIBb/5ULlBXi/LXFSEQ",
"curve25519:HGKAWHRVJQ":
"mbIZED1dBsgIgkgzxDpxKkJmsr4hiWlGzQTvUnQe3RY",
},
"algorithms": [
"m.olm.v1.curve25519-aes-sha2",
"m.megolm.v1.aes-sha2",
],
"device_id": "HGKAWHRVJQ",
"unsigned": {},
},
},
},
};

describe('DeviceList', function() {
let downloadSpy;
let sessionStore;

beforeEach(function() {
testUtils.beforeEach(this); // eslint-disable-line no-invalid-this

downloadSpy = expect.createSpy();
const mockStorage = new MockStorageApi();
sessionStore = new WebStorageSessionStore(mockStorage);
});

function createTestDeviceList() {
const baseApis = {
downloadKeysForUsers: downloadSpy,
};
const mockOlm = {
verifySignature: function(key, message, signature) {},
};
return new DeviceList(baseApis, sessionStore, mockOlm);
}

it("should successfully download and store device keys", function() {
const dl = createTestDeviceList();

dl.startTrackingDeviceList('@test1:sw1v.org');

const queryDefer1 = q.defer();
downloadSpy.andReturn(queryDefer1.promise);

const prom1 = dl.refreshOutdatedDeviceLists();
expect(downloadSpy).toHaveBeenCalledWith(['@test1:sw1v.org'], {});
queryDefer1.resolve(utils.deepCopy(signedDeviceList));

return prom1.then(() => {
const storedKeys = sessionStore.getEndToEndDevicesForUser('@test1:sw1v.org');
expect(Object.keys(storedKeys)).toEqual(['HGKAWHRVJQ']);
});
});

it("should have an outdated devicelist on an invalidation while an " +
"update is in progress", function() {
const dl = createTestDeviceList();

dl.startTrackingDeviceList('@test1:sw1v.org');

const queryDefer1 = q.defer();
downloadSpy.andReturn(queryDefer1.promise);

const prom1 = dl.refreshOutdatedDeviceLists();
expect(downloadSpy).toHaveBeenCalledWith(['@test1:sw1v.org'], {});
downloadSpy.reset();

// outdated notif arrives while the request is in flight.
const queryDefer2 = q.defer();
downloadSpy.andReturn(queryDefer2.promise);

dl.invalidateUserDeviceList('@test1:sw1v.org');
dl.refreshOutdatedDeviceLists();

// the first request completes
queryDefer1.resolve({
device_keys: {
'@test1:sw1v.org': {},
},
});

return prom1.then(() => {
// uh-oh; user restarts before second request completes. The new instance
// should know we never got a complete device list.
console.log("Creating new devicelist to simulate app reload");
downloadSpy.reset();
const dl2 = createTestDeviceList();
const queryDefer3 = q.defer();
downloadSpy.andReturn(queryDefer3.promise);

const prom3 = dl2.refreshOutdatedDeviceLists();
expect(downloadSpy).toHaveBeenCalledWith(['@test1:sw1v.org'], {});

queryDefer3.resolve(utils.deepCopy(signedDeviceList));

// allow promise chain to complete
return prom3;
}).then(() => {
const storedKeys = sessionStore.getEndToEndDevicesForUser('@test1:sw1v.org');
expect(Object.keys(storedKeys)).toEqual(['HGKAWHRVJQ']);
});
});
});
13 changes: 12 additions & 1 deletion src/crypto/DeviceList.js
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,9 @@ export default class DeviceList {

/**
* If we have users who have outdated device lists, start key downloads for them
*
* @returns {Promise} which completes when the download completes; normally there
* is no need to wait for this (it's mostly for the unit tests).
*/
refreshOutdatedDeviceLists() {
const usersToDownload = [];
Expand All @@ -280,7 +283,7 @@ export default class DeviceList {
// invalidateUserDeviceList, so do it now.
this._persistDeviceTrackingStatus();

this._doKeyDownload(usersToDownload);
return this._doKeyDownload(usersToDownload);
}


Expand Down Expand Up @@ -323,6 +326,14 @@ export default class DeviceList {

const finished = (success) => {
users.forEach((u) => {
// we may have queued up another download request for this user
// since we started this request. If that happens, we should
// ignore the completion of the first one.
if (this._keyDownloadsInProgressByUser[u] !== prom) {
console.log('Another update in the queue for', u,
'- not marking up-to-date');
return;
}
delete this._keyDownloadsInProgressByUser[u];
const stat = this._deviceTrackingStatus[u];
if (stat == TRACKING_STATUS_DOWNLOAD_IN_PROGRESS) {
Expand Down