1 | "use strict";
|
2 | import 'source-map-support/register';
|
3 | import Promise from 'bluebird';
|
4 | const sdk = require("../..");
|
5 | const MatrixClient = sdk.MatrixClient;
|
6 | const utils = require("../test-utils");
|
7 |
|
8 | import expect from 'expect';
|
9 | import lolex from 'lolex';
|
10 | import logger from '../../src/logger';
|
11 |
|
12 | describe("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 |
|
51 |
|
52 |
|
53 |
|
54 |
|
55 |
|
56 |
|
57 |
|
58 |
|
59 |
|
60 |
|
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) {
|
76 | if (pendingLookup) {
|
77 | if (pendingLookup.method === method && pendingLookup.path === path) {
|
78 | return pendingLookup.promise;
|
79 | }
|
80 |
|
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);
|
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);
|
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() {},
|
151 | store: store,
|
152 | scheduler: scheduler,
|
153 | userId: userId,
|
154 | });
|
155 |
|
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 |
|
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 |
|
174 |
|
175 |
|
176 |
|
177 |
|
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 |
|
235 |
|
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 |
|
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 |
|
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 |
|
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 = [];
|
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 | });
|