import * as utils from "../test-utils"; import { EventTimeline } from "../../src/matrix"; import { logger } from "../../src/logger"; import { TestClient } from "../TestClient"; const userId = "@alice:localhost"; const userName = "Alice"; const accessToken = "aseukfgwef"; const roomId = "!foo:bar"; const otherUserId = "@bob:localhost"; const USER_MEMBERSHIP_EVENT = utils.mkMembership({ room: roomId, mship: "join", user: userId, name: userName, }); const ROOM_NAME_EVENT = utils.mkEvent({ type: "m.room.name", room: roomId, user: otherUserId, content: { name: "Old room name", }, }); const INITIAL_SYNC_DATA = { next_batch: "s_5_3", rooms: { join: { "!foo:bar": { // roomId timeline: { events: [ utils.mkMessage({ room: roomId, user: otherUserId, msg: "hello", }), ], prev_batch: "f_1_1", }, state: { events: [ ROOM_NAME_EVENT, utils.mkMembership({ room: roomId, mship: "join", user: otherUserId, name: "Bob", }), USER_MEMBERSHIP_EVENT, utils.mkEvent({ type: "m.room.create", room: roomId, user: userId, content: { creator: userId, }, }), ], }, }, }, }, }; const EVENTS = [ utils.mkMessage({ room: roomId, user: userId, msg: "we", }), utils.mkMessage({ room: roomId, user: userId, msg: "could", }), utils.mkMessage({ room: roomId, user: userId, msg: "be", }), utils.mkMessage({ room: roomId, user: userId, msg: "heroes", }), ]; // start the client, and wait for it to initialise function startClient(httpBackend, client) { httpBackend.when("GET", "/pushrules").respond(200, {}); httpBackend.when("POST", "/filter").respond(200, { filter_id: "fid" }); httpBackend.when("GET", "/sync").respond(200, INITIAL_SYNC_DATA); client.startClient(); // set up a promise which will resolve once the client is initialised const prom = new Promise((resolve) => { client.on("sync", function(state) { logger.log("sync", state); if (state != "SYNCING") { return; } resolve(); }); }); return Promise.all([ httpBackend.flushAllExpected(), prom, ]); } describe("getEventTimeline support", function() { let httpBackend; let client; beforeEach(function() { const testClient = new TestClient(userId, "DEVICE", accessToken); client = testClient.client; httpBackend = testClient.httpBackend; }); afterEach(function() { if (client) { client.stopClient(); } return httpBackend.stop(); }); it("timeline support must be enabled to work", function() { return startClient(httpBackend, client).then(function() { const room = client.getRoom(roomId); const timelineSet = room.getTimelineSets()[0]; expect(function() { client.getEventTimeline(timelineSet, "event"); }).toThrow(); }); }); it("timeline support works when enabled", function() { const testClient = new TestClient( userId, "DEVICE", accessToken, undefined, { timelineSupport: true }, ); client = testClient.client; httpBackend = testClient.httpBackend; return startClient(httpBackend, client).then(() => { const room = client.getRoom(roomId); const timelineSet = room.getTimelineSets()[0]; expect(function() { client.getEventTimeline(timelineSet, "event"); }).not.toThrow(); }); }); it("scrollback should be able to scroll back to before a gappy /sync", function() { // need a client with timelineSupport disabled to make this work let room; return startClient(httpBackend, client).then(function() { room = client.getRoom(roomId); httpBackend.when("GET", "/sync").respond(200, { next_batch: "s_5_4", rooms: { join: { "!foo:bar": { timeline: { events: [ EVENTS[0], ], prev_batch: "f_1_1", }, }, }, }, }); httpBackend.when("GET", "/sync").respond(200, { next_batch: "s_5_5", rooms: { join: { "!foo:bar": { timeline: { events: [ EVENTS[1], ], limited: true, prev_batch: "f_1_2", }, }, }, }, }); return Promise.all([ httpBackend.flushAllExpected(), utils.syncPromise(client, 2), ]); }).then(function() { expect(room.timeline.length).toEqual(1); expect(room.timeline[0].event).toEqual(EVENTS[1]); httpBackend.when("GET", "/messages").respond(200, { chunk: [EVENTS[0]], start: "pagin_start", end: "pagin_end", }); httpBackend.flush("/messages", 1); return client.scrollback(room); }).then(function() { expect(room.timeline.length).toEqual(2); expect(room.timeline[0].event).toEqual(EVENTS[0]); expect(room.timeline[1].event).toEqual(EVENTS[1]); expect(room.oldState.paginationToken).toEqual("pagin_end"); }); }); }); describe("MatrixClient event timelines", function() { let client = null; let httpBackend = null; beforeEach(function() { const testClient = new TestClient( userId, "DEVICE", accessToken, undefined, { timelineSupport: true }, ); client = testClient.client; httpBackend = testClient.httpBackend; return startClient(httpBackend, client); }); afterEach(function() { httpBackend.verifyNoOutstandingExpectation(); client.stopClient(); }); describe("getEventTimeline", function() { it("should create a new timeline for new events", function() { const room = client.getRoom(roomId); const timelineSet = room.getTimelineSets()[0]; httpBackend.when("GET", "/rooms/!foo%3Abar/context/event1%3Abar") .respond(200, function() { return { start: "start_token", events_before: [EVENTS[1], EVENTS[0]], event: EVENTS[2], events_after: [EVENTS[3]], state: [ ROOM_NAME_EVENT, USER_MEMBERSHIP_EVENT, ], end: "end_token", }; }); return Promise.all([ client.getEventTimeline(timelineSet, "event1:bar").then(function(tl) { expect(tl.getEvents().length).toEqual(4); for (let i = 0; i < 4; i++) { expect(tl.getEvents()[i].event).toEqual(EVENTS[i]); expect(tl.getEvents()[i].sender.name).toEqual(userName); } expect(tl.getPaginationToken(EventTimeline.BACKWARDS)) .toEqual("start_token"); expect(tl.getPaginationToken(EventTimeline.FORWARDS)) .toEqual("end_token"); }), httpBackend.flushAllExpected(), ]); }); it("should return existing timeline for known events", function() { const room = client.getRoom(roomId); const timelineSet = room.getTimelineSets()[0]; httpBackend.when("GET", "/sync").respond(200, { next_batch: "s_5_4", rooms: { join: { "!foo:bar": { timeline: { events: [ EVENTS[0], ], prev_batch: "f_1_2", }, }, }, }, }); return Promise.all([ httpBackend.flush("/sync"), utils.syncPromise(client), ]).then(function() { return client.getEventTimeline(timelineSet, EVENTS[0].event_id); }).then(function(tl) { expect(tl.getEvents().length).toEqual(2); expect(tl.getEvents()[1].event).toEqual(EVENTS[0]); expect(tl.getEvents()[1].sender.name).toEqual(userName); expect(tl.getPaginationToken(EventTimeline.BACKWARDS)) .toEqual("f_1_1"); // expect(tl.getPaginationToken(EventTimeline.FORWARDS)) // .toEqual("s_5_4"); }); }); it("should update timelines where they overlap a previous /sync", function() { const room = client.getRoom(roomId); const timelineSet = room.getTimelineSets()[0]; httpBackend.when("GET", "/sync").respond(200, { next_batch: "s_5_4", rooms: { join: { "!foo:bar": { timeline: { events: [ EVENTS[3], ], prev_batch: "f_1_2", }, }, }, }, }); httpBackend.when("GET", "/rooms/!foo%3Abar/context/" + encodeURIComponent(EVENTS[2].event_id)) .respond(200, function() { return { start: "start_token", events_before: [EVENTS[1]], event: EVENTS[2], events_after: [EVENTS[3]], end: "end_token", state: [], }; }); const prom = new Promise((resolve, reject) => { client.on("sync", function() { client.getEventTimeline(timelineSet, EVENTS[2].event_id, ).then(function(tl) { expect(tl.getEvents().length).toEqual(4); expect(tl.getEvents()[0].event).toEqual(EVENTS[1]); expect(tl.getEvents()[1].event).toEqual(EVENTS[2]); expect(tl.getEvents()[3].event).toEqual(EVENTS[3]); expect(tl.getPaginationToken(EventTimeline.BACKWARDS)) .toEqual("start_token"); // expect(tl.getPaginationToken(EventTimeline.FORWARDS)) // .toEqual("s_5_4"); }).then(resolve, reject); }); }); return Promise.all([ httpBackend.flushAllExpected(), prom, ]); }); it("should join timelines where they overlap a previous /context", function() { const room = client.getRoom(roomId); const timelineSet = room.getTimelineSets()[0]; // we fetch event 0, then 2, then 3, and finally 1. 1 is returned // with context which joins them all up. httpBackend.when("GET", "/rooms/!foo%3Abar/context/" + encodeURIComponent(EVENTS[0].event_id)) .respond(200, function() { return { start: "start_token0", events_before: [], event: EVENTS[0], events_after: [], end: "end_token0", state: [], }; }); httpBackend.when("GET", "/rooms/!foo%3Abar/context/" + encodeURIComponent(EVENTS[2].event_id)) .respond(200, function() { return { start: "start_token2", events_before: [], event: EVENTS[2], events_after: [], end: "end_token2", state: [], }; }); httpBackend.when("GET", "/rooms/!foo%3Abar/context/" + encodeURIComponent(EVENTS[3].event_id)) .respond(200, function() { return { start: "start_token3", events_before: [], event: EVENTS[3], events_after: [], end: "end_token3", state: [], }; }); httpBackend.when("GET", "/rooms/!foo%3Abar/context/" + encodeURIComponent(EVENTS[1].event_id)) .respond(200, function() { return { start: "start_token4", events_before: [EVENTS[0]], event: EVENTS[1], events_after: [EVENTS[2], EVENTS[3]], end: "end_token4", state: [], }; }); let tl0; let tl3; return Promise.all([ client.getEventTimeline(timelineSet, EVENTS[0].event_id, ).then(function(tl) { expect(tl.getEvents().length).toEqual(1); tl0 = tl; return client.getEventTimeline(timelineSet, EVENTS[2].event_id); }).then(function(tl) { expect(tl.getEvents().length).toEqual(1); return client.getEventTimeline(timelineSet, EVENTS[3].event_id); }).then(function(tl) { expect(tl.getEvents().length).toEqual(1); tl3 = tl; return client.getEventTimeline(timelineSet, EVENTS[1].event_id); }).then(function(tl) { // we expect it to get merged in with event 2 expect(tl.getEvents().length).toEqual(2); expect(tl.getEvents()[0].event).toEqual(EVENTS[1]); expect(tl.getEvents()[1].event).toEqual(EVENTS[2]); expect(tl.getNeighbouringTimeline(EventTimeline.BACKWARDS)) .toBe(tl0); expect(tl.getNeighbouringTimeline(EventTimeline.FORWARDS)) .toBe(tl3); expect(tl0.getPaginationToken(EventTimeline.BACKWARDS)) .toEqual("start_token0"); expect(tl0.getPaginationToken(EventTimeline.FORWARDS)) .toBe(null); expect(tl3.getPaginationToken(EventTimeline.BACKWARDS)) .toBe(null); expect(tl3.getPaginationToken(EventTimeline.FORWARDS)) .toEqual("end_token3"); }), httpBackend.flushAllExpected(), ]); }); it("should fail gracefully if there is no event field", function() { const room = client.getRoom(roomId); const timelineSet = room.getTimelineSets()[0]; // we fetch event 0, then 2, then 3, and finally 1. 1 is returned // with context which joins them all up. httpBackend.when("GET", "/rooms/!foo%3Abar/context/event1") .respond(200, function() { return { start: "start_token", events_before: [], events_after: [], end: "end_token", state: [], }; }); return Promise.all([ client.getEventTimeline(timelineSet, "event1", ).then(function(tl) { // could do with a fail() expect(true).toBeFalsy(); }, function(e) { expect(String(e)).toMatch(/'event'/); }), httpBackend.flushAllExpected(), ]); }); }); describe("paginateEventTimeline", function() { it("should allow you to paginate backwards", function() { const room = client.getRoom(roomId); const timelineSet = room.getTimelineSets()[0]; httpBackend.when("GET", "/rooms/!foo%3Abar/context/" + encodeURIComponent(EVENTS[0].event_id)) .respond(200, function() { return { start: "start_token0", events_before: [], event: EVENTS[0], events_after: [], end: "end_token0", state: [], }; }); httpBackend.when("GET", "/rooms/!foo%3Abar/messages") .check(function(req) { const params = req.queryParams; expect(params.dir).toEqual("b"); expect(params.from).toEqual("start_token0"); expect(params.limit).toEqual("30"); }).respond(200, function() { return { chunk: [EVENTS[1], EVENTS[2]], end: "start_token1", }; }); let tl; return Promise.all([ client.getEventTimeline(timelineSet, EVENTS[0].event_id, ).then(function(tl0) { tl = tl0; return client.paginateEventTimeline(tl, { backwards: true }); }).then(function(success) { expect(success).toBeTruthy(); expect(tl.getEvents().length).toEqual(3); expect(tl.getEvents()[0].event).toEqual(EVENTS[2]); expect(tl.getEvents()[1].event).toEqual(EVENTS[1]); expect(tl.getEvents()[2].event).toEqual(EVENTS[0]); expect(tl.getPaginationToken(EventTimeline.BACKWARDS)) .toEqual("start_token1"); expect(tl.getPaginationToken(EventTimeline.FORWARDS)) .toEqual("end_token0"); }), httpBackend.flushAllExpected(), ]); }); it("should allow you to paginate forwards", function() { const room = client.getRoom(roomId); const timelineSet = room.getTimelineSets()[0]; httpBackend.when("GET", "/rooms/!foo%3Abar/context/" + encodeURIComponent(EVENTS[0].event_id)) .respond(200, function() { return { start: "start_token0", events_before: [], event: EVENTS[0], events_after: [], end: "end_token0", state: [], }; }); httpBackend.when("GET", "/rooms/!foo%3Abar/messages") .check(function(req) { const params = req.queryParams; expect(params.dir).toEqual("f"); expect(params.from).toEqual("end_token0"); expect(params.limit).toEqual("20"); }).respond(200, function() { return { chunk: [EVENTS[1], EVENTS[2]], end: "end_token1", }; }); let tl; return Promise.all([ client.getEventTimeline(timelineSet, EVENTS[0].event_id, ).then(function(tl0) { tl = tl0; return client.paginateEventTimeline( tl, { backwards: false, limit: 20 }); }).then(function(success) { expect(success).toBeTruthy(); expect(tl.getEvents().length).toEqual(3); expect(tl.getEvents()[0].event).toEqual(EVENTS[0]); expect(tl.getEvents()[1].event).toEqual(EVENTS[1]); expect(tl.getEvents()[2].event).toEqual(EVENTS[2]); expect(tl.getPaginationToken(EventTimeline.BACKWARDS)) .toEqual("start_token0"); expect(tl.getPaginationToken(EventTimeline.FORWARDS)) .toEqual("end_token1"); }), httpBackend.flushAllExpected(), ]); }); }); describe("event timeline for sent events", function() { const TXN_ID = "txn1"; const event = utils.mkMessage({ room: roomId, user: userId, msg: "a body", }); event.unsigned = { transaction_id: TXN_ID }; beforeEach(function() { // set up handlers for both the message send, and the // /sync httpBackend.when("PUT", "/send/m.room.message/" + TXN_ID) .respond(200, { event_id: event.event_id, }); httpBackend.when("GET", "/sync").respond(200, { next_batch: "s_5_4", rooms: { join: { "!foo:bar": { timeline: { events: [ event, ], prev_batch: "f_1_1", }, }, }, }, }); }); it("should work when /send returns before /sync", function() { const room = client.getRoom(roomId); const timelineSet = room.getTimelineSets()[0]; return Promise.all([ client.sendTextMessage(roomId, "a body", TXN_ID).then(function(res) { expect(res.event_id).toEqual(event.event_id); return client.getEventTimeline(timelineSet, event.event_id); }).then(function(tl) { // 2 because the initial sync contained an event expect(tl.getEvents().length).toEqual(2); expect(tl.getEvents()[1].getContent().body).toEqual("a body"); // now let the sync complete, and check it again return Promise.all([ httpBackend.flush("/sync", 1), utils.syncPromise(client), ]); }).then(function() { return client.getEventTimeline(timelineSet, event.event_id); }).then(function(tl) { expect(tl.getEvents().length).toEqual(2); expect(tl.getEvents()[1].event).toEqual(event); }), httpBackend.flush("/send/m.room.message/" + TXN_ID, 1), ]); }); it("should work when /send returns after /sync", function() { const room = client.getRoom(roomId); const timelineSet = room.getTimelineSets()[0]; return Promise.all([ // initiate the send, and set up checks to be done when it completes // - but note that it won't complete until after the /sync does, below. client.sendTextMessage(roomId, "a body", TXN_ID).then(function(res) { logger.log("sendTextMessage completed"); expect(res.event_id).toEqual(event.event_id); return client.getEventTimeline(timelineSet, event.event_id); }).then(function(tl) { logger.log("getEventTimeline completed (2)"); expect(tl.getEvents().length).toEqual(2); expect(tl.getEvents()[1].getContent().body).toEqual("a body"); }), Promise.all([ httpBackend.flush("/sync", 1), utils.syncPromise(client), ]).then(function() { return client.getEventTimeline(timelineSet, event.event_id); }).then(function(tl) { logger.log("getEventTimeline completed (1)"); expect(tl.getEvents().length).toEqual(2); expect(tl.getEvents()[1].event).toEqual(event); // now let the send complete. return httpBackend.flush("/send/m.room.message/" + TXN_ID, 1); }), ]); }); }); it("should handle gappy syncs after redactions", function() { // https://github.com/vector-im/vector-web/issues/1389 // a state event, followed by a redaction thereof const event = utils.mkMembership({ room: roomId, mship: "join", user: otherUserId, }); const redaction = utils.mkEvent({ type: "m.room.redaction", room_id: roomId, sender: otherUserId, content: {}, }); redaction.redacts = event.event_id; const syncData = { next_batch: "batch1", rooms: { join: {}, }, }; syncData.rooms.join[roomId] = { timeline: { events: [ event, redaction, ], limited: false, }, }; httpBackend.when("GET", "/sync").respond(200, syncData); return Promise.all([ httpBackend.flushAllExpected(), utils.syncPromise(client), ]).then(function() { const room = client.getRoom(roomId); const tl = room.getLiveTimeline(); expect(tl.getEvents().length).toEqual(3); expect(tl.getEvents()[1].isRedacted()).toBe(true); const sync2 = { next_batch: "batch2", rooms: { join: {}, }, }; sync2.rooms.join[roomId] = { timeline: { events: [ utils.mkMessage({ room: roomId, user: otherUserId, msg: "world", }), ], limited: true, prev_batch: "newerTok", }, }; httpBackend.when("GET", "/sync").respond(200, sync2); return Promise.all([ httpBackend.flushAllExpected(), utils.syncPromise(client), ]); }).then(function() { const room = client.getRoom(roomId); const tl = room.getLiveTimeline(); expect(tl.getEvents().length).toEqual(1); }); }); });