UNPKG

20.9 kBJavaScriptView Raw
1"use strict";
2import 'source-map-support/register';
3import Promise from 'bluebird';
4const sdk = require("../..");
5const MatrixClient = sdk.MatrixClient;
6const utils = require("../test-utils");
7
8import expect from 'expect';
9import lolex from 'lolex';
10import logger from '../../src/logger';
11
12describe("MatrixClient", function() {
13 const userId = "@alice:bar";
14 const identityServerUrl = "https://identity.server";
15 const identityServerDomain = "identity.server";
16 let client;
17 let store;
18 let scheduler;
19 let clock;
20
21 const KEEP_ALIVE_PATH = "/_matrix/client/versions";
22
23 const PUSH_RULES_RESPONSE = {
24 method: "GET",
25 path: "/pushrules/",
26 data: {},
27 };
28
29 const FILTER_PATH = "/user/" + encodeURIComponent(userId) + "/filter";
30
31 const FILTER_RESPONSE = {
32 method: "POST",
33 path: FILTER_PATH,
34 data: { filter_id: "f1lt3r" },
35 };
36
37 const SYNC_DATA = {
38 next_batch: "s_5_3",
39 presence: { events: [] },
40 rooms: {},
41 };
42
43 const SYNC_RESPONSE = {
44 method: "GET",
45 path: "/sync",
46 data: SYNC_DATA,
47 };
48
49 let httpLookups = [
50 // items are objects which look like:
51 // {
52 // method: "GET",
53 // path: "/initialSync",
54 // data: {},
55 // error: { errcode: M_FORBIDDEN } // if present will reject promise,
56 // expectBody: {} // additional expects on the body
57 // expectQueryParams: {} // additional expects on query params
58 // thenCall: function(){} // function to call *AFTER* returning response.
59 // }
60 // items are popped off when processed and block if no items left.
61 ];
62 let acceptKeepalives;
63 let pendingLookup = null;
64 function httpReq(cb, method, path, qp, data, prefix) {
65 if (path === KEEP_ALIVE_PATH && acceptKeepalives) {
66 return Promise.resolve();
67 }
68 const next = httpLookups.shift();
69 const logLine = (
70 "MatrixClient[UT] RECV " + method + " " + path + " " +
71 "EXPECT " + (next ? next.method : next) + " " + (next ? next.path : next)
72 );
73 logger.log(logLine);
74
75 if (!next) { // no more things to return
76 if (pendingLookup) {
77 if (pendingLookup.method === method && pendingLookup.path === path) {
78 return pendingLookup.promise;
79 }
80 // >1 pending thing, and they are different, whine.
81 expect(false).toBe(
82 true, ">1 pending request. You should probably handle them. " +
83 "PENDING: " + JSON.stringify(pendingLookup) + " JUST GOT: " +
84 method + " " + path,
85 );
86 }
87 pendingLookup = {
88 promise: Promise.defer().promise,
89 method: method,
90 path: path,
91 };
92 return pendingLookup.promise;
93 }
94 if (next.path === path && next.method === method) {
95 logger.log(
96 "MatrixClient[UT] Matched. Returning " +
97 (next.error ? "BAD" : "GOOD") + " response",
98 );
99 if (next.expectBody) {
100 expect(next.expectBody).toEqual(data);
101 }
102 if (next.expectQueryParams) {
103 Object.keys(next.expectQueryParams).forEach(function(k) {
104 expect(qp[k]).toEqual(next.expectQueryParams[k]);
105 });
106 }
107
108 if (next.thenCall) {
109 process.nextTick(next.thenCall, 0); // next tick so we return first.
110 }
111
112 if (next.error) {
113 return Promise.reject({
114 errcode: next.error.errcode,
115 httpStatus: next.error.httpStatus,
116 name: next.error.errcode,
117 message: "Expected testing error",
118 data: next.error,
119 });
120 }
121 return Promise.resolve(next.data);
122 }
123 expect(true).toBe(false, "Expected different request. " + logLine);
124 return Promise.defer().promise;
125 }
126
127 beforeEach(function() {
128 utils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
129 clock = lolex.install();
130 scheduler = [
131 "getQueueForEvent", "queueEvent", "removeEventFromQueue",
132 "setProcessFunction",
133 ].reduce((r, k) => { r[k] = expect.createSpy(); return r; }, {});
134 store = [
135 "getRoom", "getRooms", "getUser", "getSyncToken", "scrollback",
136 "save", "wantsSave", "setSyncToken", "storeEvents", "storeRoom", "storeUser",
137 "getFilterIdByName", "setFilterIdByName", "getFilter", "storeFilter",
138 "getSyncAccumulator", "startup", "deleteAllData",
139 ].reduce((r, k) => { r[k] = expect.createSpy(); return r; }, {});
140 store.getSavedSync = expect.createSpy().andReturn(Promise.resolve(null));
141 store.getSavedSyncToken = expect.createSpy().andReturn(Promise.resolve(null));
142 store.setSyncData = expect.createSpy().andReturn(Promise.resolve(null));
143 store.getClientOptions = expect.createSpy().andReturn(Promise.resolve(null));
144 store.storeClientOptions = expect.createSpy().andReturn(Promise.resolve(null));
145 store.isNewlyCreated = expect.createSpy().andReturn(Promise.resolve(true));
146 client = new MatrixClient({
147 baseUrl: "https://my.home.server",
148 idBaseUrl: identityServerUrl,
149 accessToken: "my.access.token",
150 request: function() {}, // NOP
151 store: store,
152 scheduler: scheduler,
153 userId: userId,
154 });
155 // FIXME: We shouldn't be yanking _http like this.
156 client._http = [
157 "authedRequest", "getContentUri", "request", "uploadContent",
158 ].reduce((r, k) => { r[k] = expect.createSpy(); return r; }, {});
159 client._http.authedRequest.andCall(httpReq);
160 client._http.request.andCall(httpReq);
161
162 // set reasonable working defaults
163 acceptKeepalives = true;
164 pendingLookup = null;
165 httpLookups = [];
166 httpLookups.push(PUSH_RULES_RESPONSE);
167 httpLookups.push(FILTER_RESPONSE);
168 httpLookups.push(SYNC_RESPONSE);
169 });
170
171 afterEach(function() {
172 clock.uninstall();
173 // need to re-stub the requests with NOPs because there are no guarantees
174 // clients from previous tests will be GC'd before the next test. This
175 // means they may call /events and then fail an expect() which will fail
176 // a DIFFERENT test (pollution between tests!) - we return unresolved
177 // promises to stop the client from continuing to run.
178 client._http.authedRequest.andCall(function() {
179 return Promise.defer().promise;
180 });
181 });
182
183 it("should not POST /filter if a matching filter already exists", async function() {
184 httpLookups = [];
185 httpLookups.push(PUSH_RULES_RESPONSE);
186 httpLookups.push(SYNC_RESPONSE);
187 const filterId = "ehfewf";
188 store.getFilterIdByName.andReturn(filterId);
189 const filter = new sdk.Filter(0, filterId);
190 filter.setDefinition({"room": {"timeline": {"limit": 8}}});
191 store.getFilter.andReturn(filter);
192 const syncPromise = new Promise((resolve, reject) => {
193 client.on("sync", function syncListener(state) {
194 if (state === "SYNCING") {
195 expect(httpLookups.length).toEqual(0);
196 client.removeListener("sync", syncListener);
197 resolve();
198 } else if (state === "ERROR") {
199 reject(new Error("sync error"));
200 }
201 });
202 });
203 await client.startClient();
204 await syncPromise;
205 });
206
207 describe("getSyncState", function() {
208 it("should return null if the client isn't started", function() {
209 expect(client.getSyncState()).toBe(null);
210 });
211
212 it("should return the same sync state as emitted sync events", async function() {
213 const syncingPromise = new Promise((resolve) => {
214 client.on("sync", function syncListener(state) {
215 expect(state).toEqual(client.getSyncState());
216 if (state === "SYNCING") {
217 client.removeListener("sync", syncListener);
218 resolve();
219 }
220 });
221 });
222 await client.startClient();
223 await syncingPromise;
224 });
225 });
226
227 describe("getOrCreateFilter", function() {
228 it("should POST createFilter if no id is present in localStorage", function() {
229 });
230 it("should use an existing filter if id is present in localStorage", function() {
231 });
232 it("should handle localStorage filterId missing from the server", function(done) {
233 function getFilterName(userId, suffix) {
234 // scope this on the user ID because people may login on many accounts
235 // and they all need to be stored!
236 return "FILTER_SYNC_" + userId + (suffix ? "_" + suffix : "");
237 }
238 const invalidFilterId = 'invalidF1lt3r';
239 httpLookups = [];
240 httpLookups.push({
241 method: "GET",
242 path: FILTER_PATH + '/' + invalidFilterId,
243 error: {
244 errcode: "M_UNKNOWN",
245 name: "M_UNKNOWN",
246 message: "No row found",
247 data: { errcode: "M_UNKNOWN", error: "No row found" },
248 httpStatus: 404,
249 },
250 });
251 httpLookups.push(FILTER_RESPONSE);
252 store.getFilterIdByName.andReturn(invalidFilterId);
253
254 const filterName = getFilterName(client.credentials.userId);
255 client.store.setFilterIdByName(filterName, invalidFilterId);
256 const filter = new sdk.Filter(client.credentials.userId);
257
258 client.getOrCreateFilter(filterName, filter).then(function(filterId) {
259 expect(filterId).toEqual(FILTER_RESPONSE.data.filter_id);
260 done();
261 });
262 });
263 });
264
265 describe("retryImmediately", function() {
266 it("should return false if there is no request waiting", async function() {
267 await client.startClient();
268 expect(client.retryImmediately()).toBe(false);
269 });
270
271 it("should work on /filter", function(done) {
272 httpLookups = [];
273 httpLookups.push(PUSH_RULES_RESPONSE);
274 httpLookups.push({
275 method: "POST", path: FILTER_PATH, error: { errcode: "NOPE_NOPE_NOPE" },
276 });
277 httpLookups.push(FILTER_RESPONSE);
278 httpLookups.push(SYNC_RESPONSE);
279
280 client.on("sync", function syncListener(state) {
281 if (state === "ERROR" && httpLookups.length > 0) {
282 expect(httpLookups.length).toEqual(2);
283 expect(client.retryImmediately()).toBe(true);
284 clock.tick(1);
285 } else if (state === "PREPARED" && httpLookups.length === 0) {
286 client.removeListener("sync", syncListener);
287 done();
288 } else {
289 // unexpected state transition!
290 expect(state).toEqual(null);
291 }
292 });
293 client.startClient();
294 });
295
296 it("should work on /sync", function(done) {
297 httpLookups.push({
298 method: "GET", path: "/sync", error: { errcode: "NOPE_NOPE_NOPE" },
299 });
300 httpLookups.push({
301 method: "GET", path: "/sync", data: SYNC_DATA,
302 });
303
304 client.on("sync", function syncListener(state) {
305 if (state === "ERROR" && httpLookups.length > 0) {
306 expect(httpLookups.length).toEqual(1);
307 expect(client.retryImmediately()).toBe(
308 true, "retryImmediately returned false",
309 );
310 clock.tick(1);
311 } else if (state === "RECONNECTING" && httpLookups.length > 0) {
312 clock.tick(10000);
313 } else if (state === "SYNCING" && httpLookups.length === 0) {
314 client.removeListener("sync", syncListener);
315 done();
316 }
317 });
318 client.startClient();
319 });
320
321 it("should work on /pushrules", function(done) {
322 httpLookups = [];
323 httpLookups.push({
324 method: "GET", path: "/pushrules/", error: { errcode: "NOPE_NOPE_NOPE" },
325 });
326 httpLookups.push(PUSH_RULES_RESPONSE);
327 httpLookups.push(FILTER_RESPONSE);
328 httpLookups.push(SYNC_RESPONSE);
329
330 client.on("sync", function syncListener(state) {
331 if (state === "ERROR" && httpLookups.length > 0) {
332 expect(httpLookups.length).toEqual(3);
333 expect(client.retryImmediately()).toBe(true);
334 clock.tick(1);
335 } else if (state === "PREPARED" && httpLookups.length === 0) {
336 client.removeListener("sync", syncListener);
337 done();
338 } else {
339 // unexpected state transition!
340 expect(state).toEqual(null);
341 }
342 });
343 client.startClient();
344 });
345 });
346
347 describe("emitted sync events", function() {
348 function syncChecker(expectedStates, done) {
349 return function syncListener(state, old) {
350 const expected = expectedStates.shift();
351 logger.log(
352 "'sync' curr=%s old=%s EXPECT=%s", state, old, expected,
353 );
354 if (!expected) {
355 done();
356 return;
357 }
358 expect(state).toEqual(expected[0]);
359 expect(old).toEqual(expected[1]);
360 if (expectedStates.length === 0) {
361 client.removeListener("sync", syncListener);
362 done();
363 }
364 // standard retry time is 5 to 10 seconds
365 clock.tick(10000);
366 };
367 }
368
369 it("should transition null -> PREPARED after the first /sync", function(done) {
370 const expectedStates = [];
371 expectedStates.push(["PREPARED", null]);
372 client.on("sync", syncChecker(expectedStates, done));
373 client.startClient();
374 });
375
376 it("should transition null -> ERROR after a failed /filter", function(done) {
377 const expectedStates = [];
378 httpLookups = [];
379 httpLookups.push(PUSH_RULES_RESPONSE);
380 httpLookups.push({
381 method: "POST", path: FILTER_PATH, error: { errcode: "NOPE_NOPE_NOPE" },
382 });
383 expectedStates.push(["ERROR", null]);
384 client.on("sync", syncChecker(expectedStates, done));
385 client.startClient();
386 });
387
388 it("should transition ERROR -> CATCHUP after /sync if prev failed",
389 function(done) {
390 const expectedStates = [];
391 acceptKeepalives = false;
392 httpLookups = [];
393 httpLookups.push(PUSH_RULES_RESPONSE);
394 httpLookups.push(FILTER_RESPONSE);
395 httpLookups.push({
396 method: "GET", path: "/sync", error: { errcode: "NOPE_NOPE_NOPE" },
397 });
398 httpLookups.push({
399 method: "GET", path: KEEP_ALIVE_PATH,
400 error: { errcode: "KEEPALIVE_FAIL" },
401 });
402 httpLookups.push({
403 method: "GET", path: KEEP_ALIVE_PATH, data: {},
404 });
405 httpLookups.push({
406 method: "GET", path: "/sync", data: SYNC_DATA,
407 });
408
409 expectedStates.push(["RECONNECTING", null]);
410 expectedStates.push(["ERROR", "RECONNECTING"]);
411 expectedStates.push(["CATCHUP", "ERROR"]);
412 client.on("sync", syncChecker(expectedStates, done));
413 client.startClient();
414 });
415
416 it("should transition PREPARED -> SYNCING after /sync", function(done) {
417 const expectedStates = [];
418 expectedStates.push(["PREPARED", null]);
419 expectedStates.push(["SYNCING", "PREPARED"]);
420 client.on("sync", syncChecker(expectedStates, done));
421 client.startClient();
422 });
423
424 it("should transition SYNCING -> ERROR after a failed /sync", function(done) {
425 acceptKeepalives = false;
426 const expectedStates = [];
427 httpLookups.push({
428 method: "GET", path: "/sync", error: { errcode: "NONONONONO" },
429 });
430 httpLookups.push({
431 method: "GET", path: KEEP_ALIVE_PATH,
432 error: { errcode: "KEEPALIVE_FAIL" },
433 });
434
435 expectedStates.push(["PREPARED", null]);
436 expectedStates.push(["SYNCING", "PREPARED"]);
437 expectedStates.push(["RECONNECTING", "SYNCING"]);
438 expectedStates.push(["ERROR", "RECONNECTING"]);
439 client.on("sync", syncChecker(expectedStates, done));
440 client.startClient();
441 });
442
443 xit("should transition ERROR -> SYNCING after /sync if prev failed",
444 function(done) {
445 const expectedStates = [];
446 httpLookups.push({
447 method: "GET", path: "/sync", error: { errcode: "NONONONONO" },
448 });
449 httpLookups.push(SYNC_RESPONSE);
450
451 expectedStates.push(["PREPARED", null]);
452 expectedStates.push(["SYNCING", "PREPARED"]);
453 expectedStates.push(["ERROR", "SYNCING"]);
454 client.on("sync", syncChecker(expectedStates, done));
455 client.startClient();
456 });
457
458 it("should transition SYNCING -> SYNCING on subsequent /sync successes",
459 function(done) {
460 const expectedStates = [];
461 httpLookups.push(SYNC_RESPONSE);
462 httpLookups.push(SYNC_RESPONSE);
463
464 expectedStates.push(["PREPARED", null]);
465 expectedStates.push(["SYNCING", "PREPARED"]);
466 expectedStates.push(["SYNCING", "SYNCING"]);
467 client.on("sync", syncChecker(expectedStates, done));
468 client.startClient();
469 });
470
471 it("should transition ERROR -> ERROR if keepalive keeps failing", function(done) {
472 acceptKeepalives = false;
473 const expectedStates = [];
474 httpLookups.push({
475 method: "GET", path: "/sync", error: { errcode: "NONONONONO" },
476 });
477 httpLookups.push({
478 method: "GET", path: KEEP_ALIVE_PATH,
479 error: { errcode: "KEEPALIVE_FAIL" },
480 });
481 httpLookups.push({
482 method: "GET", path: KEEP_ALIVE_PATH,
483 error: { errcode: "KEEPALIVE_FAIL" },
484 });
485
486 expectedStates.push(["PREPARED", null]);
487 expectedStates.push(["SYNCING", "PREPARED"]);
488 expectedStates.push(["RECONNECTING", "SYNCING"]);
489 expectedStates.push(["ERROR", "RECONNECTING"]);
490 expectedStates.push(["ERROR", "ERROR"]);
491 client.on("sync", syncChecker(expectedStates, done));
492 client.startClient();
493 });
494 });
495
496 describe("inviteByEmail", function() {
497 const roomId = "!foo:bar";
498
499 it("should send an invite HTTP POST", function() {
500 httpLookups = [{
501 method: "POST",
502 path: "/rooms/!foo%3Abar/invite",
503 data: {},
504 expectBody: {
505 id_server: identityServerDomain,
506 medium: "email",
507 address: "alice@gmail.com",
508 },
509 }];
510 client.inviteByEmail(roomId, "alice@gmail.com");
511 expect(httpLookups.length).toEqual(0);
512 });
513 });
514
515 describe("guest rooms", function() {
516 it("should only do /sync calls (without filter/pushrules)", function(done) {
517 httpLookups = []; // no /pushrules or /filter
518 httpLookups.push({
519 method: "GET",
520 path: "/sync",
521 data: SYNC_DATA,
522 thenCall: function() {
523 done();
524 },
525 });
526 client.setGuest(true);
527 client.startClient();
528 });
529
530 xit("should be able to peek into a room using peekInRoom", function(done) {
531 });
532 });
533});