1
0
mirror of https://github.com/matrix-org/matrix-js-sdk.git synced 2025-11-25 05:23:13 +03:00
Files
matrix-js-sdk/spec/unit/webrtc/groupCallEventHandler.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

327 lines
11 KiB
TypeScript

/*
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.
*/
import { mocked } from "jest-mock";
import { ClientEvent } from "../../../src/client";
import { RoomMember } from "../../../src/models/room-member";
import { SyncState } from "../../../src/sync";
import {
GroupCallIntent,
GroupCallState,
GroupCallType,
GroupCallTerminationReason,
} from "../../../src/webrtc/groupCall";
import { IContent, MatrixEvent } from "../../../src/models/event";
import { Room } from "../../../src/models/room";
import { RoomState } from "../../../src/models/room-state";
import { GroupCallEventHandler, GroupCallEventHandlerEvent } from "../../../src/webrtc/groupCallEventHandler";
import { flushPromises } from "../../test-utils/flushPromises";
import { makeMockGroupCallStateEvent, MockCallMatrixClient } from "../../test-utils/webrtc";
const FAKE_USER_ID = "@alice:test.dummy";
const FAKE_DEVICE_ID = "AAAAAAA";
const FAKE_SESSION_ID = "session1";
const FAKE_ROOM_ID = "!roomid:test.dummy";
const FAKE_GROUP_CALL_ID = "fakegroupcallid";
describe("Group Call Event Handler", function () {
let groupCallEventHandler: GroupCallEventHandler;
let mockClient: MockCallMatrixClient;
let mockRoom: Room;
let mockMember: RoomMember;
beforeEach(() => {
mockClient = new MockCallMatrixClient(FAKE_USER_ID, FAKE_DEVICE_ID, FAKE_SESSION_ID);
groupCallEventHandler = new GroupCallEventHandler(mockClient.typed());
mockMember = {
userId: FAKE_USER_ID,
membership: "join",
} as unknown as RoomMember;
const mockEvent = makeMockGroupCallStateEvent(FAKE_ROOM_ID, FAKE_GROUP_CALL_ID);
mockRoom = {
on: () => {},
off: () => {},
roomId: FAKE_ROOM_ID,
currentState: {
getStateEvents: jest.fn((type, key) => {
if (type === mockEvent.getType()) {
return key === undefined ? [mockEvent] : mockEvent;
} else {
return key === undefined ? [] : null;
}
}),
},
getMember: (userId: string) => (userId === FAKE_USER_ID ? mockMember : null),
} as unknown as Room;
mockClient.getRoom = jest.fn().mockReturnValue(mockRoom);
mockClient.getFoci.mockReturnValue([{}]);
});
describe("reacts to state changes", () => {
it("terminates call", async () => {
await groupCallEventHandler.start();
mockClient.emitRoomState(makeMockGroupCallStateEvent(FAKE_ROOM_ID, FAKE_GROUP_CALL_ID), {
roomId: FAKE_ROOM_ID,
} as unknown as RoomState);
const groupCall = groupCallEventHandler.groupCalls.get(FAKE_ROOM_ID)!;
expect(groupCall.state).toBe(GroupCallState.LocalCallFeedUninitialized);
mockClient.emitRoomState(
makeMockGroupCallStateEvent(FAKE_ROOM_ID, FAKE_GROUP_CALL_ID, {
"m.type": GroupCallType.Video,
"m.intent": GroupCallIntent.Prompt,
"m.terminated": GroupCallTerminationReason.CallEnded,
}),
{
roomId: FAKE_ROOM_ID,
} as unknown as RoomState,
);
expect(groupCall.state).toBe(GroupCallState.Ended);
});
it("terminates call when redacted", async () => {
await groupCallEventHandler.start();
mockClient.emitRoomState(makeMockGroupCallStateEvent(FAKE_ROOM_ID, FAKE_GROUP_CALL_ID), {
roomId: FAKE_ROOM_ID,
} as unknown as RoomState);
const groupCall = groupCallEventHandler.groupCalls.get(FAKE_ROOM_ID)!;
expect(groupCall.state).toBe(GroupCallState.LocalCallFeedUninitialized);
mockClient.emitRoomState(makeMockGroupCallStateEvent(FAKE_ROOM_ID, FAKE_GROUP_CALL_ID, undefined, true), {
roomId: FAKE_ROOM_ID,
} as unknown as RoomState);
expect(groupCall.state).toBe(GroupCallState.Ended);
});
});
it("waits until client starts syncing", async () => {
mockClient.getSyncState.mockReturnValue(null);
let isStarted = false;
(async () => {
await groupCallEventHandler.start();
isStarted = true;
})();
const setSyncState = async (newState: SyncState) => {
const oldState = mockClient.getSyncState();
mockClient.getSyncState.mockReturnValue(newState);
mockClient.emit(ClientEvent.Sync, newState, oldState, undefined);
await flushPromises();
};
await flushPromises();
expect(isStarted).toEqual(false);
await setSyncState(SyncState.Prepared);
expect(isStarted).toEqual(false);
await setSyncState(SyncState.Syncing);
expect(isStarted).toEqual(true);
});
it("finds existing group calls when started", async () => {
const mockClientEmit = (mockClient.emit = jest.fn());
mockClient.getRooms.mockReturnValue([mockRoom]);
await groupCallEventHandler.start();
expect(mockClientEmit).toHaveBeenCalledWith(
GroupCallEventHandlerEvent.Incoming,
expect.objectContaining({
groupCallId: FAKE_GROUP_CALL_ID,
}),
);
groupCallEventHandler.stop();
});
it("can wait until a room is ready for group calls", async () => {
await groupCallEventHandler.start();
const prom = groupCallEventHandler.waitUntilRoomReadyForGroupCalls(FAKE_ROOM_ID);
let resolved = false;
(async () => {
await prom;
resolved = true;
})();
expect(resolved).toEqual(false);
mockClient.emit(ClientEvent.Room, mockRoom);
await prom;
expect(resolved).toEqual(true);
groupCallEventHandler.stop();
});
it("fires events for incoming calls", async () => {
const onIncomingGroupCall = jest.fn();
mockClient.on(GroupCallEventHandlerEvent.Incoming, onIncomingGroupCall);
await groupCallEventHandler.start();
mockClient.emitRoomState(makeMockGroupCallStateEvent(FAKE_ROOM_ID, FAKE_GROUP_CALL_ID), {
roomId: FAKE_ROOM_ID,
} as unknown as RoomState);
expect(onIncomingGroupCall).toHaveBeenCalledWith(
expect.objectContaining({
groupCallId: FAKE_GROUP_CALL_ID,
}),
);
mockClient.off(GroupCallEventHandlerEvent.Incoming, onIncomingGroupCall);
});
it("handles data channel", async () => {
await groupCallEventHandler.start();
const dataChannelOptions = {
maxPacketLifeTime: "life_time",
maxRetransmits: "retransmits",
ordered: "ordered",
protocol: "protocol",
};
mockClient.emitRoomState(
makeMockGroupCallStateEvent(FAKE_ROOM_ID, FAKE_GROUP_CALL_ID, {
"m.type": GroupCallType.Video,
"m.intent": GroupCallIntent.Prompt,
"dataChannelsEnabled": true,
dataChannelOptions,
}),
{
roomId: FAKE_ROOM_ID,
} as unknown as RoomState,
);
// @ts-ignore Mock dataChannelsEnabled is private
expect(groupCallEventHandler.groupCalls.get(FAKE_ROOM_ID)?.dataChannelsEnabled).toBe(true);
// @ts-ignore Mock dataChannelOptions is private
expect(groupCallEventHandler.groupCalls.get(FAKE_ROOM_ID)?.dataChannelOptions).toStrictEqual(
dataChannelOptions,
);
});
describe("ignoring invalid group call state events", () => {
let mockClientEmit: jest.Func;
beforeEach(() => {
mockClientEmit = mockClient.emit = jest.fn();
});
afterEach(() => {
groupCallEventHandler.stop();
jest.clearAllMocks();
});
const setupCallAndStart = async (content?: IContent, redacted?: boolean) => {
mocked(mockRoom.currentState.getStateEvents).mockReturnValue([
makeMockGroupCallStateEvent(FAKE_ROOM_ID, FAKE_GROUP_CALL_ID, content, redacted),
] as unknown as MatrixEvent);
mockClient.getRooms.mockReturnValue([mockRoom]);
await groupCallEventHandler.start();
};
it("ignores terminated calls", async () => {
await setupCallAndStart({
"m.type": GroupCallType.Video,
"m.intent": GroupCallIntent.Prompt,
"m.terminated": GroupCallTerminationReason.CallEnded,
});
expect(mockClientEmit).not.toHaveBeenCalledWith(
GroupCallEventHandlerEvent.Incoming,
expect.objectContaining({
groupCallId: FAKE_GROUP_CALL_ID,
}),
);
});
it("ignores calls with invalid type", async () => {
await setupCallAndStart({
"m.type": "fake_type",
"m.intent": GroupCallIntent.Prompt,
});
expect(mockClientEmit).not.toHaveBeenCalledWith(
GroupCallEventHandlerEvent.Incoming,
expect.objectContaining({
groupCallId: FAKE_GROUP_CALL_ID,
}),
);
});
it("ignores calls with invalid intent", async () => {
await setupCallAndStart({
"m.type": GroupCallType.Video,
"m.intent": "fake_intent",
});
expect(mockClientEmit).not.toHaveBeenCalledWith(
GroupCallEventHandlerEvent.Incoming,
expect.objectContaining({
groupCallId: FAKE_GROUP_CALL_ID,
}),
);
});
it("ignores calls without a room", async () => {
mockClient.getRoom.mockReturnValue(undefined);
await setupCallAndStart();
expect(mockClientEmit).not.toHaveBeenCalledWith(
GroupCallEventHandlerEvent.Incoming,
expect.objectContaining({
groupCallId: FAKE_GROUP_CALL_ID,
}),
);
});
it("ignores redacted calls", async () => {
await setupCallAndStart(
{
// Real event contents to make sure that it's specifically the
// event being redacted that causes it to be ignored
"m.type": GroupCallType.Video,
"m.intent": GroupCallIntent.Prompt,
},
true,
);
expect(mockClientEmit).not.toHaveBeenCalledWith(
GroupCallEventHandlerEvent.Incoming,
expect.objectContaining({
groupCallId: FAKE_GROUP_CALL_ID,
}),
);
});
});
});