UNPKG

14.9 kBJavaScriptView Raw
1/*
2Copyright 2017 Vector Creations Ltd
3Copyright 2018 New Vector Ltd
4
5Licensed under the Apache License, Version 2.0 (the "License");
6you may not use this file except in compliance with the License.
7You may obtain a copy of the License at
8
9 http://www.apache.org/licenses/LICENSE-2.0
10
11Unless required by applicable law or agreed to in writing, software
12distributed under the License is distributed on an "AS IS" BASIS,
13WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14See the License for the specific language governing permissions and
15limitations under the License.
16*/
17
18import expect from 'expect';
19import Promise from 'bluebird';
20
21import TestClient from '../TestClient';
22import testUtils from '../test-utils';
23import logger from '../../src/logger';
24
25const ROOM_ID = "!room:id";
26
27/**
28 * get a /sync response which contains a single e2e room (ROOM_ID), with the
29 * members given
30 *
31 * @param {string[]} roomMembers
32 *
33 * @return {object} sync response
34 */
35function getSyncResponse(roomMembers) {
36 const stateEvents = [
37 testUtils.mkEvent({
38 type: 'm.room.encryption',
39 skey: '',
40 content: {
41 algorithm: 'm.megolm.v1.aes-sha2',
42 },
43 }),
44 ];
45
46 Array.prototype.push.apply(
47 stateEvents,
48 roomMembers.map(
49 (m) => testUtils.mkMembership({
50 mship: 'join',
51 sender: m,
52 }),
53 ),
54 );
55
56 const syncResponse = {
57 next_batch: 1,
58 rooms: {
59 join: {
60 [ROOM_ID]: {
61 state: {
62 events: stateEvents,
63 },
64 },
65 },
66 },
67 };
68
69 return syncResponse;
70}
71
72
73describe("DeviceList management:", function() {
74 if (!global.Olm) {
75 logger.warn('not running deviceList tests: Olm not present');
76 return;
77 }
78
79 let sessionStoreBackend;
80 let aliceTestClient;
81
82 async function createTestClient() {
83 const testClient = new TestClient(
84 "@alice:localhost", "xzcvb", "akjgkrgjs", sessionStoreBackend,
85 );
86 await testClient.client.initCrypto();
87 return testClient;
88 }
89
90 beforeEach(async function() {
91 testUtils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
92
93 // we create our own sessionStoreBackend so that we can use it for
94 // another TestClient.
95 sessionStoreBackend = new testUtils.MockStorageApi();
96
97 aliceTestClient = await createTestClient();
98 });
99
100 afterEach(function() {
101 return aliceTestClient.stop();
102 });
103
104 it("Alice shouldn't do a second /query for non-e2e-capable devices", function() {
105 aliceTestClient.expectKeyQuery({device_keys: {'@alice:localhost': {}}});
106 return aliceTestClient.start().then(function() {
107 const syncResponse = getSyncResponse(['@bob:xyz']);
108 aliceTestClient.httpBackend.when('GET', '/sync').respond(200, syncResponse);
109
110 return aliceTestClient.flushSync();
111 }).then(function() {
112 logger.log("Forcing alice to download our device keys");
113
114 aliceTestClient.httpBackend.when('POST', '/keys/query').respond(200, {
115 device_keys: {
116 '@bob:xyz': {},
117 },
118 });
119
120 return Promise.all([
121 aliceTestClient.client.downloadKeys(['@bob:xyz']),
122 aliceTestClient.httpBackend.flush('/keys/query', 1),
123 ]);
124 }).then(function() {
125 logger.log("Telling alice to send a megolm message");
126
127 aliceTestClient.httpBackend.when(
128 'PUT', '/send/',
129 ).respond(200, {
130 event_id: '$event_id',
131 });
132
133 return Promise.all([
134 aliceTestClient.client.sendTextMessage(ROOM_ID, 'test'),
135
136 // the crypto stuff can take a while, so give the requests a whole second.
137 aliceTestClient.httpBackend.flushAllExpected({
138 timeout: 1000,
139 }),
140 ]);
141 });
142 });
143
144
145 it("We should not get confused by out-of-order device query responses",
146 () => {
147 // https://github.com/vector-im/riot-web/issues/3126
148 aliceTestClient.expectKeyQuery({device_keys: {'@alice:localhost': {}}});
149 return aliceTestClient.start().then(() => {
150 aliceTestClient.httpBackend.when('GET', '/sync').respond(
151 200, getSyncResponse(['@bob:xyz', '@chris:abc']));
152 return aliceTestClient.flushSync();
153 }).then(() => {
154 // to make sure the initial device queries are flushed out, we
155 // attempt to send a message.
156
157 aliceTestClient.httpBackend.when('POST', '/keys/query').respond(
158 200, {
159 device_keys: {
160 '@bob:xyz': {},
161 '@chris:abc': {},
162 },
163 },
164 );
165
166 aliceTestClient.httpBackend.when('PUT', '/send/').respond(
167 200, {event_id: '$event1'});
168
169 return Promise.all([
170 aliceTestClient.client.sendTextMessage(ROOM_ID, 'test'),
171 aliceTestClient.httpBackend.flush('/keys/query', 1).then(
172 () => aliceTestClient.httpBackend.flush('/send/', 1),
173 ),
174 aliceTestClient.client._crypto._deviceList.saveIfDirty(),
175 ]);
176 }).then(() => {
177 aliceTestClient.cryptoStore.getEndToEndDeviceData(null, (data) => {
178 expect(data.syncToken).toEqual(1);
179 });
180
181 // invalidate bob's and chris's device lists in separate syncs
182 aliceTestClient.httpBackend.when('GET', '/sync').respond(200, {
183 next_batch: '2',
184 device_lists: {
185 changed: ['@bob:xyz'],
186 },
187 });
188 aliceTestClient.httpBackend.when('GET', '/sync').respond(200, {
189 next_batch: '3',
190 device_lists: {
191 changed: ['@chris:abc'],
192 },
193 });
194 // flush both syncs
195 return aliceTestClient.flushSync().then(() => {
196 return aliceTestClient.flushSync();
197 });
198 }).then(() => {
199 // check that we don't yet have a request for chris's devices.
200 aliceTestClient.httpBackend.when('POST', '/keys/query', {
201 device_keys: {
202 '@chris:abc': {},
203 },
204 token: '3',
205 }).respond(200, {
206 device_keys: {'@chris:abc': {}},
207 });
208 return aliceTestClient.httpBackend.flush('/keys/query', 1);
209 }).then((flushed) => {
210 expect(flushed).toEqual(0);
211 return aliceTestClient.client._crypto._deviceList.saveIfDirty();
212 }).then(() => {
213 aliceTestClient.cryptoStore.getEndToEndDeviceData(null, (data) => {
214 const bobStat = data.trackingStatus['@bob:xyz'];
215 if (bobStat != 1 && bobStat != 2) {
216 throw new Error('Unexpected status for bob: wanted 1 or 2, got ' +
217 bobStat);
218 }
219 const chrisStat = data.trackingStatus['@chris:abc'];
220 if (chrisStat != 1 && chrisStat != 2) {
221 throw new Error(
222 'Unexpected status for chris: wanted 1 or 2, got ' + chrisStat,
223 );
224 }
225 });
226
227 // now add an expectation for a query for bob's devices, and let
228 // it complete.
229 aliceTestClient.httpBackend.when('POST', '/keys/query', {
230 device_keys: {
231 '@bob:xyz': {},
232 },
233 token: '2',
234 }).respond(200, {
235 device_keys: {'@bob:xyz': {}},
236 });
237 return aliceTestClient.httpBackend.flush('/keys/query', 1);
238 }).then((flushed) => {
239 expect(flushed).toEqual(1);
240
241 // wait for the client to stop processing the response
242 return aliceTestClient.client.downloadKeys(['@bob:xyz']);
243 }).then(() => {
244 return aliceTestClient.client._crypto._deviceList.saveIfDirty();
245 }).then(() => {
246 aliceTestClient.cryptoStore.getEndToEndDeviceData(null, (data) => {
247 const bobStat = data.trackingStatus['@bob:xyz'];
248 expect(bobStat).toEqual(3);
249 const chrisStat = data.trackingStatus['@chris:abc'];
250 if (chrisStat != 1 && chrisStat != 2) {
251 throw new Error(
252 'Unexpected status for chris: wanted 1 or 2, got ' + bobStat,
253 );
254 }
255 });
256
257 // now let the query for chris's devices complete.
258 return aliceTestClient.httpBackend.flush('/keys/query', 1);
259 }).then((flushed) => {
260 expect(flushed).toEqual(1);
261
262 // wait for the client to stop processing the response
263 return aliceTestClient.client.downloadKeys(['@chris:abc']);
264 }).then(() => {
265 return aliceTestClient.client._crypto._deviceList.saveIfDirty();
266 }).then(() => {
267 aliceTestClient.cryptoStore.getEndToEndDeviceData(null, (data) => {
268 const bobStat = data.trackingStatus['@bob:xyz'];
269 const chrisStat = data.trackingStatus['@bob:xyz'];
270
271 expect(bobStat).toEqual(3);
272 expect(chrisStat).toEqual(3);
273 expect(data.syncToken).toEqual(3);
274 });
275 });
276 }).timeout(3000);
277
278 // https://github.com/vector-im/riot-web/issues/4983
279 describe("Alice should know she has stale device lists", () => {
280 beforeEach(async function() {
281 await aliceTestClient.start();
282
283 aliceTestClient.httpBackend.when('GET', '/sync').respond(
284 200, getSyncResponse(['@bob:xyz']));
285 await aliceTestClient.flushSync();
286
287 aliceTestClient.httpBackend.when('POST', '/keys/query').respond(
288 200, {
289 device_keys: {
290 '@bob:xyz': {},
291 },
292 },
293 );
294 await aliceTestClient.httpBackend.flush('/keys/query', 1);
295 await aliceTestClient.client._crypto._deviceList.saveIfDirty();
296
297 aliceTestClient.cryptoStore.getEndToEndDeviceData(null, (data) => {
298 const bobStat = data.trackingStatus['@bob:xyz'];
299
300 expect(bobStat).toBeGreaterThan(
301 0, "Alice should be tracking bob's device list",
302 );
303 });
304 });
305
306 it("when Bob leaves", async function() {
307 aliceTestClient.httpBackend.when('GET', '/sync').respond(
308 200, {
309 next_batch: 2,
310 device_lists: {
311 left: ['@bob:xyz'],
312 },
313 rooms: {
314 join: {
315 [ROOM_ID]: {
316 timeline: {
317 events: [
318 testUtils.mkMembership({
319 mship: 'leave',
320 sender: '@bob:xyz',
321 }),
322 ],
323 },
324 },
325 },
326 },
327 },
328 );
329
330
331 await aliceTestClient.flushSync();
332 await aliceTestClient.client._crypto._deviceList.saveIfDirty();
333
334 aliceTestClient.cryptoStore.getEndToEndDeviceData(null, (data) => {
335 const bobStat = data.trackingStatus['@bob:xyz'];
336
337 expect(bobStat).toEqual(
338 0, "Alice should have marked bob's device list as untracked",
339 );
340 });
341 });
342
343 it("when Alice leaves", async function() {
344 aliceTestClient.httpBackend.when('GET', '/sync').respond(
345 200, {
346 next_batch: 2,
347 device_lists: {
348 left: ['@bob:xyz'],
349 },
350 rooms: {
351 leave: {
352 [ROOM_ID]: {
353 timeline: {
354 events: [
355 testUtils.mkMembership({
356 mship: 'leave',
357 sender: '@bob:xyz',
358 }),
359 ],
360 },
361 },
362 },
363 },
364 },
365 );
366
367 await aliceTestClient.flushSync();
368 await aliceTestClient.client._crypto._deviceList.saveIfDirty();
369
370 aliceTestClient.cryptoStore.getEndToEndDeviceData(null, (data) => {
371 const bobStat = data.trackingStatus['@bob:xyz'];
372
373 expect(bobStat).toEqual(
374 0, "Alice should have marked bob's device list as untracked",
375 );
376 });
377 });
378
379 it("when Bob leaves whilst Alice is offline", async function() {
380 aliceTestClient.stop();
381
382 const anotherTestClient = await createTestClient();
383
384 try {
385 await anotherTestClient.start();
386 anotherTestClient.httpBackend.when('GET', '/sync').respond(
387 200, getSyncResponse([]));
388 await anotherTestClient.flushSync();
389 await anotherTestClient.client._crypto._deviceList.saveIfDirty();
390
391 anotherTestClient.cryptoStore.getEndToEndDeviceData(null, (data) => {
392 const bobStat = data.trackingStatus['@bob:xyz'];
393
394 expect(bobStat).toEqual(
395 0, "Alice should have marked bob's device list as untracked",
396 );
397 });
398 } finally {
399 anotherTestClient.stop();
400 }
401 });
402 });
403});