1
0
mirror of https://github.com/matrix-org/matrix-js-sdk.git synced 2025-08-09 10:22:46 +03:00
Files
matrix-js-sdk/spec/unit/embedded.spec.ts
David Baker 6836720e1e Introduce MatrixRTCSession lower level group call primitive (#3663)
* Add hacky option to disable the actual calling part of group calls.

So we can try using livekit instead.

* Put LiveKit info into the `m.call` state event (#3522)

* Put LK info into state

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>

* Update to the new way the LK service works

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>

---------

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>

* Send 'contentLoaded' event

As per comment, so we can start digging ourselves out of the widget
API hole we're currently in.

* Add comment on updating the livekit service URL

* Appease CI on `livekit` branch (#3566)

* Update codeowners on `livekit` branch (#3567)

* add getOpenIdToken to embedded client backend

Signed-off-by: Timo K <toger5@hotmail.de>

* add test and update comment

Signed-off-by: Timo K <toger5@hotmail.de>

* Merge `develop` into `livekit` (#3569)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: RiotRobot <releases@riot.im>
Co-authored-by: Florian Duros <florianduros@element.io>
Co-authored-by: Kerry <kerrya@element.io>
Co-authored-by: David Baker <dbkr@users.noreply.github.com>
Co-authored-by: Erik Johnston <erik@matrix.org>
Co-authored-by: Valere <bill.carson@valrsoft.com>
Co-authored-by: Hubert Chathi <hubertc@matrix.org>
Close IDB database before deleting it to prevent spurious unexpected close errors (#3478)
Fix export type `GeneratedSecretStorageKey` (#3479)
Fix order of things in `crypto-api.ts` (#3491)
Fix bug where switching media caused media in subsequent calls to fail (#3489)
fixes (#3515)
fix the integ tests, where #3509 etc fix the unit tests.
fix breakage on node 16 (#3527)
Fix an instance of failed to decrypt error when an in flight `/keys/query` fails. (#3486)
Fix `TypedEventEmitter::removeAllListeners(void)` not working (#3561)

* Revert "Merge `develop` into `livekit`" (#3572)

* Don't update calls with no livekit URL & expose method to update it instead

and generally simplify a bit: change it to a single string rather than
an array of structs.

* Fix other instances of passing focusInfo / livekit url

* Add temporary setter

* WIP refactor for removing m.call events

* Always remember rtcsessions since we need to only have one instance

* Fix tests

* Fix import loop

* Fix more cyclic imports & tests

* Test session joining

* Attempt to make tests happy

* Always leave calls in the tests to clean up

* comment + desperate attempt to work out what's failing

* More test debugging

* Okay, so these ones are fine?

* Stop more timers and hopefully have happy tests

* Test no rejoin

* Test malformed m.call.member events

* Test event emitting

and also move some code to a more sensible place in the file

* Test getActiveFoci()

* Test event emitting (and also fix it)

* Test membership updating & pruning on join

* Test getOldestMembership()

* Test member event renewal

* Don't start the rtc manager until the client has synced

Then we can initialise from the state once it's completed.

* Fix type

* Remove listeners added in constructor

* Stop the client here too

* Stop the client here also also

* ARGH. Disable tests to work out which one is causing the exception

* Disable everything

* Re-jig to avoid setting listeners in the constructor

and re-enable tests

* No need to rename this anymore

* argh, remove the right listener

* Is it this test???

* Re-enable some tests

* Try mocking getRooms to return something valid

* Re-enable other tests

* Give up trying to get the tests to work sensibly and deal with getRooms() returning nothing

* Oops, don't enable the ones that were skipped before

* One more try at the sensible way

* Didn't work, go back to the hack way.

* Log when we manage to send the member event update

* Support `getOpenIdToken()` in embedded mode (#3676)

* Call `sendContentLoaded()` (#3677)

* Start MatrixRTC in embedded mode (#3679)

* Reschedule the membership event check

* Bump widget api version

* Add mock for sendContentLoaded()

* More log detail

* Fix tests

and also better assert because the tests were passing undefined which
was considered fine because we were only checking for null.

* Simplify updateCallMembershipEvent a bit

* Split up updateCallMembershipEvent some more

* Typo

Co-authored-by: Daniel Abramov <inetcrack2@gmail.com>

* Expand comment

* Add comment

* More comments

* Better comment

* Sesson

* Rename some variables

* Comment

* Remove unused method

* Wrap updatecallMembershipEvent so it only runs one at a time

* Do another update if another one is triggered while the update happens

* Make triggerCallMembershipEventUpdate async

* Fix test & some missed timer removals

* Mark session manager as unstable

---------

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
Signed-off-by: Timo K <toger5@hotmail.de>
Co-authored-by: Šimon Brandner <simon.bra.ag@gmail.com>
Co-authored-by: Timo K <toger5@hotmail.de>
Co-authored-by: Timo <16718859+toger5@users.noreply.github.com>
Co-authored-by: Daniel Abramov <inetcrack2@gmail.com>
2023-09-12 15:08:15 +00:00

366 lines
16 KiB
TypeScript

/**
* @jest-environment jsdom
*/
/*
Copyright 2022 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.
*/
// We have to use EventEmitter here to mock part of the matrix-widget-api
// project, which doesn't know about our TypeEventEmitter implementation at all
// eslint-disable-next-line no-restricted-imports
import { EventEmitter } from "events";
import { MockedObject } from "jest-mock";
import {
WidgetApi,
WidgetApiToWidgetAction,
MatrixCapabilities,
ITurnServer,
IRoomEvent,
IOpenIDCredentials,
} from "matrix-widget-api";
import { createRoomWidgetClient, MsgType } from "../../src/matrix";
import { MatrixClient, ClientEvent, ITurnServer as IClientTurnServer } from "../../src/client";
import { SyncState } from "../../src/sync";
import { ICapabilities } from "../../src/embedded";
import { MatrixEvent } from "../../src/models/event";
import { ToDeviceBatch } from "../../src/models/ToDeviceMessage";
import { DeviceInfo } from "../../src/crypto/deviceinfo";
const testOIDCToken = {
access_token: "12345678",
expires_in: "10",
matrix_server_name: "homeserver.oabc",
token_type: "Bearer",
};
class MockWidgetApi extends EventEmitter {
public start = jest.fn();
public requestCapability = jest.fn();
public requestCapabilities = jest.fn();
public requestCapabilityForRoomTimeline = jest.fn();
public requestCapabilityToSendEvent = jest.fn();
public requestCapabilityToReceiveEvent = jest.fn();
public requestCapabilityToSendMessage = jest.fn();
public requestCapabilityToReceiveMessage = jest.fn();
public requestCapabilityToSendState = jest.fn();
public requestCapabilityToReceiveState = jest.fn();
public requestCapabilityToSendToDevice = jest.fn();
public requestCapabilityToReceiveToDevice = jest.fn();
public sendRoomEvent = jest.fn(() => ({ event_id: `$${Math.random()}` }));
public sendStateEvent = jest.fn();
public sendToDevice = jest.fn();
public requestOpenIDConnectToken = jest.fn(() => {
return testOIDCToken;
return new Promise<IOpenIDCredentials>(() => {
return testOIDCToken;
});
});
public readStateEvents = jest.fn(() => []);
public getTurnServers = jest.fn(() => []);
public sendContentLoaded = jest.fn();
public transport = { reply: jest.fn() };
}
describe("RoomWidgetClient", () => {
let widgetApi: MockedObject<WidgetApi>;
let client: MatrixClient;
beforeEach(() => {
widgetApi = new MockWidgetApi() as unknown as MockedObject<WidgetApi>;
});
afterEach(() => {
client.stopClient();
});
const makeClient = async (capabilities: ICapabilities): Promise<void> => {
const baseUrl = "https://example.org";
client = createRoomWidgetClient(widgetApi, capabilities, "!1:example.org", { baseUrl });
expect(widgetApi.start).toHaveBeenCalled(); // needs to have been called early in order to not miss messages
widgetApi.emit("ready");
await client.startClient();
};
describe("events", () => {
it("sends", async () => {
await makeClient({ sendEvent: ["org.matrix.rageshake_request"] });
expect(widgetApi.requestCapabilityForRoomTimeline).toHaveBeenCalledWith("!1:example.org");
expect(widgetApi.requestCapabilityToSendEvent).toHaveBeenCalledWith("org.matrix.rageshake_request");
await client.sendEvent("!1:example.org", "org.matrix.rageshake_request", { request_id: 123 });
expect(widgetApi.sendRoomEvent).toHaveBeenCalledWith(
"org.matrix.rageshake_request",
{ request_id: 123 },
"!1:example.org",
);
});
it("receives", async () => {
const event = new MatrixEvent({
type: "org.matrix.rageshake_request",
event_id: "$pduhfiidph",
room_id: "!1:example.org",
sender: "@alice:example.org",
content: { request_id: 123 },
}).getEffectiveEvent();
await makeClient({ receiveEvent: ["org.matrix.rageshake_request"] });
expect(widgetApi.requestCapabilityForRoomTimeline).toHaveBeenCalledWith("!1:example.org");
expect(widgetApi.requestCapabilityToReceiveEvent).toHaveBeenCalledWith("org.matrix.rageshake_request");
const emittedEvent = new Promise<MatrixEvent>((resolve) => client.once(ClientEvent.Event, resolve));
const emittedSync = new Promise<SyncState>((resolve) => client.once(ClientEvent.Sync, resolve));
widgetApi.emit(
`action:${WidgetApiToWidgetAction.SendEvent}`,
new CustomEvent(`action:${WidgetApiToWidgetAction.SendEvent}`, { detail: { data: event } }),
);
// The client should've emitted about the received event
expect((await emittedEvent).getEffectiveEvent()).toEqual(event);
expect(await emittedSync).toEqual(SyncState.Syncing);
// It should've also inserted the event into the room object
const room = client.getRoom("!1:example.org");
expect(room).not.toBeNull();
expect(
room!
.getLiveTimeline()
.getEvents()
.map((e) => e.getEffectiveEvent()),
).toEqual([event]);
});
});
describe("messages", () => {
it("requests permissions for specific message types", async () => {
await makeClient({ sendMessage: [MsgType.Text], receiveMessage: [MsgType.Text] });
expect(widgetApi.requestCapabilityForRoomTimeline).toHaveBeenCalledWith("!1:example.org");
expect(widgetApi.requestCapabilityToSendMessage).toHaveBeenCalledWith(MsgType.Text);
expect(widgetApi.requestCapabilityToReceiveMessage).toHaveBeenCalledWith(MsgType.Text);
});
it("requests permissions for all message types", async () => {
await makeClient({ sendMessage: true, receiveMessage: true });
expect(widgetApi.requestCapabilityForRoomTimeline).toHaveBeenCalledWith("!1:example.org");
expect(widgetApi.requestCapabilityToSendMessage).toHaveBeenCalledWith();
expect(widgetApi.requestCapabilityToReceiveMessage).toHaveBeenCalledWith();
});
// No point in testing sending and receiving since it's done exactly the
// same way as non-message events
});
describe("state events", () => {
const event = new MatrixEvent({
type: "org.example.foo",
event_id: "$sfkjfsksdkfsd",
room_id: "!1:example.org",
sender: "@alice:example.org",
state_key: "bar",
content: { hello: "world" },
}).getEffectiveEvent();
it("sends", async () => {
await makeClient({ sendState: [{ eventType: "org.example.foo", stateKey: "bar" }] });
expect(widgetApi.requestCapabilityForRoomTimeline).toHaveBeenCalledWith("!1:example.org");
expect(widgetApi.requestCapabilityToSendState).toHaveBeenCalledWith("org.example.foo", "bar");
await client.sendStateEvent("!1:example.org", "org.example.foo", { hello: "world" }, "bar");
expect(widgetApi.sendStateEvent).toHaveBeenCalledWith(
"org.example.foo",
"bar",
{ hello: "world" },
"!1:example.org",
);
});
it("receives", async () => {
await makeClient({ receiveState: [{ eventType: "org.example.foo", stateKey: "bar" }] });
expect(widgetApi.requestCapabilityForRoomTimeline).toHaveBeenCalledWith("!1:example.org");
expect(widgetApi.requestCapabilityToReceiveState).toHaveBeenCalledWith("org.example.foo", "bar");
const emittedEvent = new Promise<MatrixEvent>((resolve) => client.once(ClientEvent.Event, resolve));
const emittedSync = new Promise<SyncState>((resolve) => client.once(ClientEvent.Sync, resolve));
widgetApi.emit(
`action:${WidgetApiToWidgetAction.SendEvent}`,
new CustomEvent(`action:${WidgetApiToWidgetAction.SendEvent}`, { detail: { data: event } }),
);
// The client should've emitted about the received event
expect((await emittedEvent).getEffectiveEvent()).toEqual(event);
expect(await emittedSync).toEqual(SyncState.Syncing);
// It should've also inserted the event into the room object
const room = client.getRoom("!1:example.org");
expect(room).not.toBeNull();
expect(room!.currentState.getStateEvents("org.example.foo", "bar")?.getEffectiveEvent()).toEqual(event);
});
it("backfills", async () => {
widgetApi.readStateEvents.mockImplementation(async (eventType, limit, stateKey) =>
eventType === "org.example.foo" && (limit ?? Infinity) > 0 && stateKey === "bar"
? [event as IRoomEvent]
: [],
);
await makeClient({ receiveState: [{ eventType: "org.example.foo", stateKey: "bar" }] });
expect(widgetApi.requestCapabilityForRoomTimeline).toHaveBeenCalledWith("!1:example.org");
expect(widgetApi.requestCapabilityToReceiveState).toHaveBeenCalledWith("org.example.foo", "bar");
const room = client.getRoom("!1:example.org");
expect(room).not.toBeNull();
expect(room!.currentState.getStateEvents("org.example.foo", "bar")?.getEffectiveEvent()).toEqual(event);
});
});
describe("to-device messages", () => {
const unencryptedContentMap = new Map([
["@alice:example.org", new Map([["*", { hello: "alice!" }]])],
["@bob:example.org", new Map([["bobDesktop", { hello: "bob!" }]])],
]);
const expectedRequestData = {
["@alice:example.org"]: { ["*"]: { hello: "alice!" } },
["@bob:example.org"]: { ["bobDesktop"]: { hello: "bob!" } },
};
it("sends unencrypted (sendToDevice)", async () => {
await makeClient({ sendToDevice: ["org.example.foo"] });
expect(widgetApi.requestCapabilityToSendToDevice).toHaveBeenCalledWith("org.example.foo");
await client.sendToDevice("org.example.foo", unencryptedContentMap);
expect(widgetApi.sendToDevice).toHaveBeenCalledWith("org.example.foo", false, expectedRequestData);
});
it("sends unencrypted (queueToDevice)", async () => {
await makeClient({ sendToDevice: ["org.example.foo"] });
expect(widgetApi.requestCapabilityToSendToDevice).toHaveBeenCalledWith("org.example.foo");
const batch: ToDeviceBatch = {
eventType: "org.example.foo",
batch: [
{ userId: "@alice:example.org", deviceId: "*", payload: { hello: "alice!" } },
{ userId: "@bob:example.org", deviceId: "bobDesktop", payload: { hello: "bob!" } },
],
};
await client.queueToDevice(batch);
expect(widgetApi.sendToDevice).toHaveBeenCalledWith("org.example.foo", false, expectedRequestData);
});
it("sends encrypted (encryptAndSendToDevices)", async () => {
await makeClient({ sendToDevice: ["org.example.foo"] });
expect(widgetApi.requestCapabilityToSendToDevice).toHaveBeenCalledWith("org.example.foo");
const payload = { type: "org.example.foo", hello: "world" };
await client.encryptAndSendToDevices(
[
{ userId: "@alice:example.org", deviceInfo: new DeviceInfo("aliceWeb") },
{ userId: "@bob:example.org", deviceInfo: new DeviceInfo("bobDesktop") },
],
payload,
);
expect(widgetApi.sendToDevice).toHaveBeenCalledWith("org.example.foo", true, {
"@alice:example.org": { aliceWeb: payload },
"@bob:example.org": { bobDesktop: payload },
});
});
it.each([
{ encrypted: false, title: "unencrypted" },
{ encrypted: true, title: "encrypted" },
])("receives $title", async ({ encrypted }) => {
await makeClient({ receiveToDevice: ["org.example.foo"] });
expect(widgetApi.requestCapabilityToReceiveToDevice).toHaveBeenCalledWith("org.example.foo");
const event = {
type: "org.example.foo",
sender: "@alice:example.org",
encrypted,
content: { hello: "world" },
};
const emittedEvent = new Promise<MatrixEvent>((resolve) => client.once(ClientEvent.ToDeviceEvent, resolve));
const emittedSync = new Promise<SyncState>((resolve) => client.once(ClientEvent.Sync, resolve));
widgetApi.emit(
`action:${WidgetApiToWidgetAction.SendToDevice}`,
new CustomEvent(`action:${WidgetApiToWidgetAction.SendToDevice}`, { detail: { data: event } }),
);
expect((await emittedEvent).getEffectiveEvent()).toEqual({
type: event.type,
sender: event.sender,
content: event.content,
});
expect((await emittedEvent).isEncrypted()).toEqual(encrypted);
expect(await emittedSync).toEqual(SyncState.Syncing);
});
});
describe("oidc token", () => {
it("requests an oidc token", async () => {
await makeClient({});
expect(await client.getOpenIdToken()).toStrictEqual(testOIDCToken);
});
});
it("gets TURN servers", async () => {
const server1: ITurnServer = {
uris: [
"turn:turn.example.com:3478?transport=udp",
"turn:10.20.30.40:3478?transport=tcp",
"turns:10.20.30.40:443?transport=tcp",
],
username: "1443779631:@user:example.com",
password: "JlKfBy1QwLrO20385QyAtEyIv0=",
};
const server2: ITurnServer = {
uris: [
"turn:turn.example.com:3478?transport=udp",
"turn:10.20.30.40:3478?transport=tcp",
"turns:10.20.30.40:443?transport=tcp",
],
username: "1448999322:@user:example.com",
password: "hunter2",
};
const clientServer1: IClientTurnServer = {
urls: server1.uris,
username: server1.username,
credential: server1.password,
};
const clientServer2: IClientTurnServer = {
urls: server2.uris,
username: server2.username,
credential: server2.password,
};
let emitServer2: () => void;
const getServer2 = new Promise<ITurnServer>((resolve) => (emitServer2 = () => resolve(server2)));
widgetApi.getTurnServers.mockImplementation(async function* () {
yield server1;
yield await getServer2;
});
await makeClient({ turnServers: true });
expect(widgetApi.requestCapability).toHaveBeenCalledWith(MatrixCapabilities.MSC3846TurnServers);
// The first server should've arrived immediately
expect(client.getTurnServers()).toEqual([clientServer1]);
// Subsequent servers arrive asynchronously and should emit an event
const emittedServer = new Promise<IClientTurnServer[]>((resolve) =>
client.once(ClientEvent.TurnServers, resolve),
);
emitServer2!();
expect(await emittedServer).toEqual([clientServer2]);
expect(client.getTurnServers()).toEqual([clientServer2]);
});
});