/* Copyright 2022 - 2023 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ import type HttpBackend from "matrix-mock-request"; import * as utils from "../test-utils/test-utils"; import { type IStoredClientOpts, MatrixClient } from "../../src/client"; import { MatrixEvent } from "../../src/models/event"; import { Filter, JoinRule, type KnockRoomOpts, MemoryStore, Method, Room, type RoomSummary, SERVICE_TYPES, } from "../../src/matrix"; import { TestClient } from "../TestClient"; import { THREAD_RELATION_TYPE } from "../../src/models/thread"; import { type IFilterDefinition } from "../../src/filter"; import { type ISearchResults } from "../../src/@types/search"; import { type IStore } from "../../src/store"; import { SetPresence } from "../../src/sync"; import { KnownMembership } from "../../src/@types/membership"; describe("MatrixClient", function () { const userId = "@alice:localhost"; const accessToken = "aseukfgwef"; const idServerDomain = "identity.localhost"; // not a real server const identityAccessToken = "woop-i-am-a-secret"; let client: MatrixClient; let httpBackend: HttpBackend; let store: MemoryStore; const defaultClientOpts: IStoredClientOpts = { threadSupport: false, }; const setupTests = (): [MatrixClient, HttpBackend, MemoryStore] => { const store = new MemoryStore(); const testClient = new TestClient(userId, "aliceDevice", accessToken, undefined, { store: store as IStore, identityServer: { getAccessToken: () => Promise.resolve(identityAccessToken), }, idBaseUrl: `https://${idServerDomain}`, }); return [testClient.client, testClient.httpBackend, store]; }; beforeEach(function () { [client, httpBackend, store] = setupTests(); }); afterEach(function () { httpBackend.verifyNoOutstandingExpectation(); return httpBackend.stop(); }); describe("uploadContent", function () { const buf = Buffer.from("hello world"); const file = buf; const opts = { type: "text/plain", name: "hi.txt", }; it("should upload the file", function () { httpBackend .when("POST", "/_matrix/media/v3/upload") .check(function (req) { expect(req.rawData).toEqual(buf); expect(req.queryParams?.filename).toEqual("hi.txt"); expect(req.headers["Authorization"]).toBe("Bearer " + accessToken); expect(req.headers["Content-Type"]).toEqual("text/plain"); // @ts-ignore private property expect(req.opts.json).toBeFalsy(); // @ts-ignore private property expect(req.opts.timeout).toBe(undefined); }) .respond(200, '{"content_uri": "content"}', true); const prom = client.uploadContent(file, opts); expect(prom).toBeTruthy(); const uploads = client.getCurrentUploads(); expect(uploads.length).toEqual(1); expect(uploads[0].promise).toBe(prom); expect(uploads[0].loaded).toEqual(0); const prom2 = prom.then(function (response) { expect(response.content_uri).toEqual("content"); const uploads = client.getCurrentUploads(); expect(uploads.length).toEqual(0); }); httpBackend.flush(""); return prom2; }); it("should parse errors into a MatrixError", function () { httpBackend .when("POST", "/_matrix/media/v3/upload") .check(function (req) { expect(req.rawData).toEqual(buf); // @ts-ignore private property expect(req.opts.json).toBeFalsy(); }) .respond(400, { errcode: "M_SNAFU", error: "broken", }); const prom = client.uploadContent(file, opts).then( function (response) { throw Error("request not failed"); }, function (error) { expect(error.httpStatus).toEqual(400); expect(error.errcode).toEqual("M_SNAFU"); expect(error.message).toEqual("MatrixError: [400] broken"); }, ); httpBackend.flush(""); return prom; }); it("should return a promise which can be cancelled", async () => { const prom = client.uploadContent(file, opts); const uploads = client.getCurrentUploads(); expect(uploads.length).toEqual(1); expect(uploads[0].promise).toBe(prom); expect(uploads[0].loaded).toEqual(0); const r = client.cancelUpload(prom); expect(r).toBe(true); await expect(prom).rejects.toThrow("Aborted"); expect(client.getCurrentUploads()).toHaveLength(0); }); }); describe("mediaConfig", function () { it("should get media config on unauthenticated media call", async () => { httpBackend.when("GET", "/_matrix/media/v3/config").respond(200, '{"m.upload.size": 50000000}', true); const prom = client.getMediaConfig(); httpBackend.flushAllExpected(); expect((await prom)["m.upload.size"]).toEqual(50000000); }); it("should get media config on authenticated media call", async () => { httpBackend .when("GET", "/_matrix/client/v1/media/config") .respond(200, '{"m.upload.size": 50000000}', true); const prom = client.getMediaConfig(true); httpBackend.flushAllExpected(); expect((await prom)["m.upload.size"]).toEqual(50000000); }); }); describe("joinRoom", function () { it("should no-op given the ID of a room you've already joined", async () => { const roomId = "!foo:bar"; const room = new Room(roomId, client, userId); client.fetchRoomEvent = () => Promise.resolve({ type: "test", content: {}, }); room.addLiveEvents( [ utils.mkMembership({ user: userId, room: roomId, mship: KnownMembership.Join, event: true, }), ], { addToState: true }, ); httpBackend.verifyNoOutstandingRequests(); store.storeRoom(room); const joinPromise = client.joinRoom(roomId); httpBackend.verifyNoOutstandingRequests(); expect(await joinPromise).toBe(room); }); it("should no-op given the alias of a room you've already joined", async () => { const roomId = "!roomId:server"; const roomAlias = "#my-fancy-room:server"; const room = new Room(roomId, client, userId); room.addLiveEvents( [ utils.mkMembership({ user: userId, room: roomId, mship: KnownMembership.Join, event: true, }), ], { addToState: true }, ); store.storeRoom(room); // The method makes a request to resolve the alias httpBackend.when("POST", "/join/" + encodeURIComponent(roomAlias)).respond(200, { room_id: roomId }); const joinPromise = client.joinRoom(roomAlias); await httpBackend.flushAllExpected(); expect(await joinPromise).toBe(room); }); it("should send request to inviteSignUrl if specified", async () => { const roomId = "!roomId:server"; const inviteSignUrl = "https://id.server/sign/this/for/me"; const viaServers = ["a", "b", "c"]; const signature = { sender: "sender", mxid: "@sender:foo", token: "token", signatures: {}, }; httpBackend .when("POST", inviteSignUrl) .check((request) => { expect(request.queryParams?.mxid).toEqual(client.getUserId()); }) .respond(200, signature); httpBackend .when("POST", "/join/" + encodeURIComponent(roomId)) .check((request) => { expect(request.data.third_party_signed).toEqual(signature); }) .respond(200, { room_id: roomId }); const prom = client.joinRoom(roomId, { inviteSignUrl, viaServers, }); await httpBackend.flushAllExpected(); expect((await prom).roomId).toBe(roomId); }); }); describe("knockRoom", function () { const roomId = "!some-room-id:example.org"; const reason = "some reason"; const viaServers = "example.com"; type TestCase = [string, KnockRoomOpts]; const testCases: TestCase[] = [ ["should knock a room", {}], ["should knock a room for a reason", { reason }], ["should knock a room via given servers", { viaServers }], ["should knock a room for a reason via given servers", { reason, viaServers }], ]; it.each(testCases)("%s", async (_, opts) => { httpBackend .when("POST", "/knock/" + encodeURIComponent(roomId)) .check((request) => { expect(request.data).toEqual({ reason: opts.reason }); expect(request.queryParams).toEqual({ server_name: opts.viaServers, via: opts.viaServers }); }) .respond(200, { room_id: roomId }); const prom = client.knockRoom(roomId, opts); await httpBackend.flushAllExpected(); expect((await prom).room_id).toBe(roomId); }); it("should no-op if you've already knocked a room", function () { const room = new Room(roomId, client, userId); client.fetchRoomEvent = () => Promise.resolve({ type: "test", content: {}, }); room.addLiveEvents( [ utils.mkMembership({ user: userId, room: roomId, mship: KnownMembership.Knock, event: true, }), ], { addToState: true }, ); httpBackend.verifyNoOutstandingRequests(); store.storeRoom(room); client.knockRoom(roomId); httpBackend.verifyNoOutstandingRequests(); }); describe("errors", function () { type TestCase = [number, { errcode: string; error?: string }, string]; const testCases: TestCase[] = [ [ 403, { errcode: "M_FORBIDDEN", error: "You don't have permission to knock" }, "[M_FORBIDDEN: MatrixError: [403] You don't have permission to knock]", ], [ 500, { errcode: "INTERNAL_SERVER_ERROR" }, "[INTERNAL_SERVER_ERROR: MatrixError: [500] Unknown message]", ], ]; it.each(testCases)("should handle %s error", async (code, { errcode, error }, snapshot) => { httpBackend.when("POST", "/knock/" + encodeURIComponent(roomId)).respond(code, { errcode, error }); const prom = client.knockRoom(roomId); await Promise.all([ httpBackend.flushAllExpected(), expect(prom).rejects.toMatchInlineSnapshot(snapshot), ]); }); }); }); describe("getFilter", function () { const filterId = "f1lt3r1d"; it("should return a filter from the store if allowCached", async () => { const filter = Filter.fromJson(userId, filterId, { event_format: "client", }); store.storeFilter(filter); const gotFilter = await client.getFilter(userId, filterId, true); expect(gotFilter).toEqual(filter); httpBackend.verifyNoOutstandingRequests(); }); it("should do an HTTP request if !allowCached even if one exists", async () => { const httpFilterDefinition = { event_format: "federation", }; httpBackend .when("GET", "/user/" + encodeURIComponent(userId) + "/filter/" + filterId) .respond(200, httpFilterDefinition); const storeFilter = Filter.fromJson(userId, filterId, { event_format: "client", }); store.storeFilter(storeFilter); const [gotFilter] = await Promise.all([client.getFilter(userId, filterId, false), httpBackend.flush("")]); expect(gotFilter.getDefinition()).toEqual(httpFilterDefinition); }); it("should do an HTTP request if nothing is in the cache and then store it", async () => { const httpFilterDefinition = { event_format: "federation", }; expect(store.getFilter(userId, filterId)).toBe(null); httpBackend .when("GET", "/user/" + encodeURIComponent(userId) + "/filter/" + filterId) .respond(200, httpFilterDefinition); const [gotFilter] = await Promise.all([client.getFilter(userId, filterId, true), httpBackend.flush("")]); expect(gotFilter.getDefinition()).toEqual(httpFilterDefinition); expect(store.getFilter(userId, filterId)).toBeTruthy(); }); }); describe("createFilter", function () { const filterId = "f1llllllerid"; it("should do an HTTP request and then store the filter", async () => { expect(store.getFilter(userId, filterId)).toBe(null); const filterDefinition = { event_format: "client" as IFilterDefinition["event_format"], }; httpBackend .when("POST", "/user/" + encodeURIComponent(userId) + "/filter") .check(function (req) { expect(req.data).toEqual(filterDefinition); }) .respond(200, { filter_id: filterId, }); const [gotFilter] = await Promise.all([client.createFilter(filterDefinition), httpBackend.flush("")]); expect(gotFilter.getDefinition()).toEqual(filterDefinition); expect(store.getFilter(userId, filterId)).toEqual(gotFilter); }); }); describe("searching", function () { it("searchMessageText should perform a /search for room_events", function () { const response = { search_categories: { room_events: { count: 24, results: [ { rank: 0.1, result: { event_id: "$flibble:localhost", type: "m.room.message", user_id: "@alice:localhost", room_id: "!feuiwhf:localhost", content: { body: "a result", msgtype: "m.text", }, }, }, ], }, }, }; client.searchMessageText({ query: "monkeys", }); httpBackend .when("POST", "/search") .check(function (req) { expect(req.data).toEqual({ search_categories: { room_events: { search_term: "monkeys", }, }, }); }) .respond(200, response); return httpBackend.flush(""); }); describe("should filter out context from different timelines (threads)", () => { it("filters out thread replies when result is in the main timeline", async () => { const response = { search_categories: { room_events: { count: 24, highlights: [], results: [ { rank: 0.1, result: { event_id: "$flibble:localhost", type: "m.room.message", sender: "@test:locahost", origin_server_ts: 123, user_id: "@alice:localhost", room_id: "!feuiwhf:localhost", content: { body: "main timeline", msgtype: "m.text", }, }, context: { profile_info: {}, events_after: [ { event_id: "$ev-after:server", type: "m.room.message", sender: "@test:locahost", origin_server_ts: 123, user_id: "@alice:localhost", room_id: "!feuiwhf:localhost", content: { "body": "thread reply", "msgtype": "m.text", "m.relates_to": { event_id: "$some-thread:server", rel_type: THREAD_RELATION_TYPE.name, }, }, }, ], events_before: [ { event_id: "$ev-before:server", type: "m.room.message", sender: "@test:locahost", origin_server_ts: 123, user_id: "@alice:localhost", room_id: "!feuiwhf:localhost", content: { body: "main timeline again", msgtype: "m.text", }, }, ], }, }, ], }, }, }; const data: ISearchResults = { results: [], highlights: [], }; client.processRoomEventsSearch(data, response); expect(data.results).toHaveLength(1); expect(data.results[0].context.getTimeline()).toHaveLength(2); expect(data.results[0].context.getTimeline().find((e) => e.getId() === "$ev-after:server")).toBeFalsy(); }); it("filters out thread replies from threads other than the thread the result replied to", () => { const response = { search_categories: { room_events: { count: 24, highlights: [], results: [ { rank: 0.1, result: { event_id: "$flibble:localhost", type: "m.room.message", sender: "@test:locahost", origin_server_ts: 123, user_id: "@alice:localhost", room_id: "!feuiwhf:localhost", content: { "body": "thread 1 reply 1", "msgtype": "m.text", "m.relates_to": { event_id: "$thread1:server", rel_type: THREAD_RELATION_TYPE.name, }, }, }, context: { profile_info: {}, events_after: [ { event_id: "$ev-after:server", type: "m.room.message", sender: "@test:locahost", origin_server_ts: 123, user_id: "@alice:localhost", room_id: "!feuiwhf:localhost", content: { "body": "thread 2 reply 2", "msgtype": "m.text", "m.relates_to": { event_id: "$thread2:server", rel_type: THREAD_RELATION_TYPE.name, }, }, }, ], events_before: [], }, }, ], }, }, }; const data: ISearchResults = { results: [], highlights: [], }; client.processRoomEventsSearch(data, response); expect(data.results).toHaveLength(1); expect(data.results[0].context.getTimeline()).toHaveLength(1); expect( data.results[0].context.getTimeline().find((e) => e.getId() === "$flibble:localhost"), ).toBeTruthy(); }); it("filters out main timeline events when result is a thread reply", () => { const response = { search_categories: { room_events: { count: 24, highlights: [], results: [ { rank: 0.1, result: { event_id: "$flibble:localhost", sender: "@test:locahost", origin_server_ts: 123, type: "m.room.message", user_id: "@alice:localhost", room_id: "!feuiwhf:localhost", content: { "body": "thread 1 reply 1", "msgtype": "m.text", "m.relates_to": { event_id: "$thread1:server", rel_type: THREAD_RELATION_TYPE.name, }, }, }, context: { events_after: [ { event_id: "$ev-after:server", sender: "@test:locahost", origin_server_ts: 123, type: "m.room.message", user_id: "@alice:localhost", room_id: "!feuiwhf:localhost", content: { body: "main timeline", msgtype: "m.text", }, }, ], events_before: [], profile_info: {}, }, }, ], }, }, }; const data: ISearchResults = { results: [], highlights: [], }; client.processRoomEventsSearch(data, response); expect(data.results).toHaveLength(1); expect(data.results[0].context.getTimeline()).toHaveLength(1); expect( data.results[0].context.getTimeline().find((e) => e.getId() === "$flibble:localhost"), ).toBeTruthy(); }); }); }); describe("partitionThreadedEvents", function () { let room: Room; beforeEach(() => { room = new Room("!STrMRsukXHtqQdSeHa:matrix.org", client, userId); }); it("returns empty arrays when given an empty arrays", function () { const events: MatrixEvent[] = []; const [timeline, threaded] = room.partitionThreadedEvents(events); expect(timeline).toEqual([]); expect(threaded).toEqual([]); }); it("should not copy pre-thread in-timeline vote events onto both timelines", function () { // @ts-ignore setting private property client.clientOpts = { ...defaultClientOpts, threadSupport: true, }; const eventPollResponseReference = buildEventPollResponseReference(); const eventPollStartThreadRoot = buildEventPollStartThreadRoot(); const eventMessageInThread = buildEventMessageInThread(eventPollStartThreadRoot); const events = [eventPollStartThreadRoot, eventMessageInThread, eventPollResponseReference]; // Vote has no threadId yet // @ts-ignore private property expect(eventPollResponseReference.threadId).toBeFalsy(); const [timeline, threaded] = room.partitionThreadedEvents(events); expect(timeline).toEqual([ // The message that was sent in a thread is missing eventPollStartThreadRoot, eventPollResponseReference, ]); // The vote event has been copied into the thread const eventRefWithThreadId = withThreadId(eventPollResponseReference, eventPollStartThreadRoot.getId()!); expect(eventRefWithThreadId.threadRootId).toBeTruthy(); expect(threaded).toEqual([eventPollStartThreadRoot, eventMessageInThread]); }); it("should not copy pre-thread in-timeline reactions onto both timelines", function () { // @ts-ignore setting private property client.clientOpts = { ...defaultClientOpts, threadSupport: true, }; const eventPollStartThreadRoot = buildEventPollStartThreadRoot(); const eventMessageInThread = buildEventMessageInThread(eventPollStartThreadRoot); const eventReaction = buildEventReaction(eventPollStartThreadRoot); const events = [eventPollStartThreadRoot, eventMessageInThread, eventReaction]; const [timeline, threaded] = room.partitionThreadedEvents(events); expect(timeline).toEqual([eventPollStartThreadRoot, eventReaction]); expect(threaded).toEqual([eventPollStartThreadRoot, eventMessageInThread]); }); it("should not copy post-thread in-timeline vote events onto both timelines", function () { // @ts-ignore setting private property client.clientOpts = { ...defaultClientOpts, threadSupport: true, }; const eventPollResponseReference = buildEventPollResponseReference(); const eventPollStartThreadRoot = buildEventPollStartThreadRoot(); const eventMessageInThread = buildEventMessageInThread(eventPollStartThreadRoot); const events = [eventPollStartThreadRoot, eventPollResponseReference, eventMessageInThread]; const [timeline, threaded] = room.partitionThreadedEvents(events); expect(timeline).toEqual([eventPollStartThreadRoot, eventPollResponseReference]); expect(threaded).toEqual([eventPollStartThreadRoot, eventMessageInThread]); }); it("should not copy post-thread in-timeline reactions onto both timelines", function () { // @ts-ignore setting private property client.clientOpts = { ...defaultClientOpts, threadSupport: true, }; const eventPollStartThreadRoot = buildEventPollStartThreadRoot(); const eventMessageInThread = buildEventMessageInThread(eventPollStartThreadRoot); const eventReaction = buildEventReaction(eventPollStartThreadRoot); const events = [eventPollStartThreadRoot, eventMessageInThread, eventReaction]; const [timeline, threaded] = room.partitionThreadedEvents(events); expect(timeline).toEqual([eventPollStartThreadRoot, eventReaction]); expect(threaded).toEqual([eventPollStartThreadRoot, eventMessageInThread]); }); it("sends room state events to the main timeline only", function () { // @ts-ignore setting private property client.clientOpts = { ...defaultClientOpts, threadSupport: true, }; // This is based on recording the events in a real room: const eventPollStartThreadRoot = buildEventPollStartThreadRoot(); const eventPollResponseReference = buildEventPollResponseReference(); const eventMessageInThread = buildEventMessageInThread(eventPollStartThreadRoot); const eventRoomName = buildEventRoomName(); const eventEncryption = buildEventEncryption(); const eventGuestAccess = buildEventGuestAccess(); const eventHistoryVisibility = buildEventHistoryVisibility(); const eventJoinRules = buildEventJoinRules(); const eventPowerLevels = buildEventPowerLevels(); const eventMember = buildEventMember(); const eventCreate = buildEventCreate(); const events = [ eventPollStartThreadRoot, eventPollResponseReference, eventMessageInThread, eventRoomName, eventEncryption, eventGuestAccess, eventHistoryVisibility, eventJoinRules, eventPowerLevels, eventMember, eventCreate, ]; const [timeline, threaded] = room.partitionThreadedEvents(events); expect(timeline).toEqual([ // The message that was sent in a thread is missing eventPollStartThreadRoot, eventPollResponseReference, eventRoomName, eventEncryption, eventGuestAccess, eventHistoryVisibility, eventJoinRules, eventPowerLevels, eventMember, eventCreate, ]); // Thread should contain only stuff that happened in the thread - no room state events expect(threaded).toEqual([eventPollStartThreadRoot, eventMessageInThread]); }); it("sends redactions of reactions to thread responses to thread timeline only", () => { // @ts-ignore setting private property client.clientOpts = { ...defaultClientOpts, threadSupport: true, }; const threadRootEvent = buildEventPollStartThreadRoot(); const eventMessageInThread = buildEventMessageInThread(threadRootEvent); const threadedReaction = buildEventReaction(eventMessageInThread); const threadedReactionRedaction = buildEventRedaction(threadedReaction); const events = [threadRootEvent, eventMessageInThread, threadedReaction, threadedReactionRedaction]; const [timeline, threaded] = room.partitionThreadedEvents(events); expect(timeline).toEqual([threadRootEvent]); expect(threaded).toEqual([ threadRootEvent, eventMessageInThread, threadedReaction, threadedReactionRedaction, ]); }); it("sends reply to reply to thread root outside of thread to main timeline only", () => { // @ts-ignore setting private property client.clientOpts = { ...defaultClientOpts, threadSupport: true, }; const threadRootEvent = buildEventPollStartThreadRoot(); const eventMessageInThread = buildEventMessageInThread(threadRootEvent); const directReplyToThreadRoot = buildEventReply(threadRootEvent); const replyToReply = buildEventReply(directReplyToThreadRoot); const events = [threadRootEvent, eventMessageInThread, directReplyToThreadRoot, replyToReply]; const [timeline, threaded] = room.partitionThreadedEvents(events); expect(timeline).toEqual([threadRootEvent, directReplyToThreadRoot, replyToReply]); expect(threaded).toEqual([threadRootEvent, eventMessageInThread]); }); it("sends reply to thread responses to main timeline only", () => { // @ts-ignore setting private property client.clientOpts = { ...defaultClientOpts, threadSupport: true, }; const threadRootEvent = buildEventPollStartThreadRoot(); const eventMessageInThread = buildEventMessageInThread(threadRootEvent); const replyToThreadResponse = buildEventReply(eventMessageInThread); const events = [threadRootEvent, eventMessageInThread, replyToThreadResponse]; const [timeline, threaded] = room.partitionThreadedEvents(events); expect(timeline).toEqual([threadRootEvent]); expect(threaded).toEqual([threadRootEvent, eventMessageInThread, replyToThreadResponse]); }); }); describe("getThirdpartyUser", () => { it("should hit the expected API endpoint", async () => { const response = [ { userid: "@Bob", protocol: "irc", fields: {}, }, ]; const prom = client.getThirdpartyUser("irc", {}); httpBackend.when("GET", "/thirdparty/user/irc").respond(200, response); await httpBackend.flush(""); expect(await prom).toStrictEqual(response); }); }); describe("getThirdpartyLocation", () => { it("should hit the expected API endpoint", async () => { const response = [ { alias: "#alias", protocol: "irc", fields: {}, }, ]; const prom = client.getThirdpartyLocation("irc", {}); httpBackend.when("GET", "/thirdparty/location/irc").respond(200, response); await httpBackend.flush(""); expect(await prom).toStrictEqual(response); }); }); describe("getPushers", () => { it("should hit the expected API endpoint", async () => { const response = { pushers: [], }; const prom = client.getPushers(); httpBackend.when("GET", "/_matrix/client/versions").respond(200, {}); httpBackend.when("GET", "/pushers").respond(200, response); await httpBackend.flush(""); expect(await prom).toStrictEqual(response); }); }); describe("getKeyChanges", () => { it("should hit the expected API endpoint", async () => { const response = { changed: [], left: [], }; const prom = client.getKeyChanges("old", "new"); httpBackend .when("GET", "/keys/changes") .check((req) => { expect(req.queryParams?.from).toEqual("old"); expect(req.queryParams?.to).toEqual("new"); }) .respond(200, response); await httpBackend.flush(""); expect(await prom).toStrictEqual(response); }); }); describe("getDevices", () => { it("should hit the expected API endpoint", async () => { const response = { devices: [], }; const prom = client.getDevices(); httpBackend.when("GET", "/devices").respond(200, response); await httpBackend.flush(""); expect(await prom).toStrictEqual(response); }); }); describe("getDevice", () => { it("should hit the expected API endpoint", async () => { const response = { device_id: "DEADBEEF", display_name: "NotAPhone", last_seen_ip: "127.0.0.1", last_seen_ts: 1, }; const prom = client.getDevice("DEADBEEF"); httpBackend.when("GET", "/devices/DEADBEEF").respond(200, response); await httpBackend.flush(""); expect(await prom).toStrictEqual(response); }); }); describe("getThreePids", () => { it("should hit the expected API endpoint", async () => { const response = { threepids: [], }; const prom = client.getThreePids(); httpBackend.when("GET", "/account/3pid").respond(200, response); await httpBackend.flush(""); expect(await prom).toStrictEqual(response); }); }); describe("deleteAlias", () => { it("should hit the expected API endpoint", async () => { const response = {}; const prom = client.deleteAlias("#foo:bar"); httpBackend.when("DELETE", "/directory/room/" + encodeURIComponent("#foo:bar")).respond(200, response); await httpBackend.flush(""); expect(await prom).toStrictEqual(response); }); }); describe("deleteRoomTag", () => { it("should hit the expected API endpoint", async () => { const response = {}; const prom = client.deleteRoomTag("!roomId:server", "u.tag"); const url = `/user/${encodeURIComponent(userId)}/rooms/${encodeURIComponent("!roomId:server")}/tags/u.tag`; httpBackend.when("DELETE", url).respond(200, response); await httpBackend.flush(""); expect(await prom).toStrictEqual(response); }); }); describe("getRoomTags", () => { it("should hit the expected API endpoint", async () => { const response = { tags: { "u.tag": { order: 0.5, }, }, }; const prom = client.getRoomTags("!roomId:server"); const url = `/user/${encodeURIComponent(userId)}/rooms/${encodeURIComponent("!roomId:server")}/tags`; httpBackend.when("GET", url).respond(200, response); await httpBackend.flush(""); expect(await prom).toStrictEqual(response); }); }); describe("requestRegisterEmailToken", () => { it("should hit the expected API endpoint", async () => { const response = { sid: "random_sid", submit_url: "https://foobar.matrix/_matrix/matrix", }; const prom = client.requestRegisterEmailToken("bob@email", "secret", 1); httpBackend .when("POST", "/register/email/requestToken") .check((req) => { expect(req.data).toStrictEqual({ email: "bob@email", client_secret: "secret", send_attempt: 1, }); }) .respond(200, response); await httpBackend.flush(""); expect(await prom).toStrictEqual(response); }); }); describe("inviteByThreePid", () => { it("should supply an id_access_token", async () => { const targetEmail = "gerald@example.org"; httpBackend .when("POST", "/invite") .check((req) => { expect(req.data).toStrictEqual({ id_server: idServerDomain, id_access_token: identityAccessToken, medium: "email", address: targetEmail, }); }) .respond(200, {}); const prom = client.inviteByThreePid("!room:example.org", "email", targetEmail); await httpBackend.flush(""); await prom; // returns empty object, so no validation needed }); }); describe("createRoom", () => { it("should populate id_access_token on 3pid invites", async () => { const targetEmail = "gerald@example.org"; const response = { room_id: "!room:localhost", }; const input = { invite_3pid: [ { // we intentionally exclude the access token here, so it can be populated for us id_server: idServerDomain, medium: "email", address: targetEmail, }, ], }; httpBackend .when("POST", "/createRoom") .check((req) => { expect(req.data).toMatchObject({ invite_3pid: expect.arrayContaining([ { ...input.invite_3pid[0], id_access_token: identityAccessToken, }, ]), }); expect(req.data.invite_3pid.length).toBe(1); }) .respond(200, response); const prom = client.createRoom(input); await httpBackend.flush(""); expect(await prom).toStrictEqual(response); }); }); describe("requestLoginToken", () => { it("should hit the expected API endpoint with UIA", async () => { const response = {}; const uiaData = {}; const prom = client.requestLoginToken(uiaData); httpBackend.when("POST", "/v1/login/get_token", { auth: uiaData }).respond(200, response); await httpBackend.flush(""); expect(await prom).toStrictEqual(response); }); it("should hit the expected API endpoint without UIA", async () => { const response = { login_token: "xyz", expires_in_ms: 5000 }; const prom = client.requestLoginToken(); httpBackend.when("POST", "/v1/login/get_token", {}).respond(200, response); await httpBackend.flush(""); expect(await prom).toStrictEqual(response); }); }); describe("logout", () => { it("should abort pending requests when called with stopClient=true", async () => { httpBackend.when("POST", "/logout").respond(200, {}); const fn = jest.fn(); client.http.request(Method.Get, "/test").catch(fn); client.logout(true); await httpBackend.flush(undefined); expect(fn).toHaveBeenCalled(); }); }); describe("sendHtmlEmote", () => { it("should send valid html emote", async () => { httpBackend .when("PUT", "/send") .check((req) => { expect(req.data).toStrictEqual({ msgtype: "m.emote", body: "Body", formatted_body: "