diff --git a/spec/unit/matrixrtc/MatrixRTCSession.spec.ts b/spec/unit/matrixrtc/MatrixRTCSession.spec.ts index 06f409a3b..6c0e1693b 100644 --- a/spec/unit/matrixrtc/MatrixRTCSession.spec.ts +++ b/spec/unit/matrixrtc/MatrixRTCSession.spec.ts @@ -16,11 +16,10 @@ limitations under the License. import { encodeBase64, EventType, MatrixClient, type MatrixError, type MatrixEvent, type Room } from "../../../src"; import { KnownMembership } from "../../../src/@types/membership"; -import { type SessionMembershipData } from "../../../src/matrixrtc/CallMembership"; import { MatrixRTCSession, MatrixRTCSessionEvent } from "../../../src/matrixrtc/MatrixRTCSession"; import { type EncryptionKeysEventContent } from "../../../src/matrixrtc/types"; import { secureRandomString } from "../../../src/randomstring"; -import { makeMockEvent, makeMockRoom, makeMockRoomState, membershipTemplate, makeKey } from "./mocks"; +import { makeMockEvent, makeMockRoom, membershipTemplate, makeKey, type MembershipData, mockRoomState } from "./mocks"; import { RTCEncryptionManager } from "../../../src/matrixrtc/RTCEncryptionManager.ts"; const mockFocus = { type: "mock" }; @@ -48,7 +47,7 @@ describe("MatrixRTCSession", () => { describe("roomSessionForRoom", () => { it("creates a room-scoped session from room state", () => { - const mockRoom = makeMockRoom(membershipTemplate); + const mockRoom = makeMockRoom([membershipTemplate]); sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom); expect(sess?.memberships.length).toEqual(1); @@ -75,7 +74,7 @@ describe("MatrixRTCSession", () => { }); it("ignores memberships events of members not in the room", () => { - const mockRoom = makeMockRoom(membershipTemplate); + const mockRoom = makeMockRoom([membershipTemplate]); mockRoom.hasMembershipState = (state) => state === KnownMembership.Join; sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom); expect(sess?.memberships.length).toEqual(0); @@ -202,6 +201,59 @@ describe("MatrixRTCSession", () => { }); }); + describe("updateCallMembershipEvent", () => { + const mockFocus = { type: "livekit", livekit_service_url: "https://test.org" }; + const joinSessionConfig = {}; + + const sessionMembershipData: MembershipData = { + call_id: "", + scope: "m.room", + application: "m.call", + user_id: "@mock:user.example", + device_id: "AAAAAAA_session", + focus_active: mockFocus, + foci_preferred: [mockFocus], + }; + + let sendStateEventMock: jest.Mock; + let sendDelayedStateMock: jest.Mock; + + let sentStateEvent: Promise; + let sentDelayedState: Promise; + + beforeEach(() => { + sentStateEvent = new Promise((resolve) => { + sendStateEventMock = jest.fn(resolve); + }); + sentDelayedState = new Promise((resolve) => { + sendDelayedStateMock = jest.fn(() => { + resolve(); + return { + delay_id: "id", + }; + }); + }); + client.sendStateEvent = sendStateEventMock; + client._unstable_sendDelayedStateEvent = sendDelayedStateMock; + }); + + async function testSession(membershipData: MembershipData): Promise { + sess = MatrixRTCSession.roomSessionForRoom(client, makeMockRoom([membershipData])); + + sess.joinRoomSession([mockFocus], mockFocus, joinSessionConfig); + await Promise.race([sentStateEvent, new Promise((resolve) => setTimeout(resolve, 500))]); + + expect(sendStateEventMock).toHaveBeenCalledTimes(1); + + await Promise.race([sentDelayedState, new Promise((resolve) => setTimeout(resolve, 500))]); + expect(sendDelayedStateMock).toHaveBeenCalledTimes(1); + } + + it("sends events", async () => { + await testSession(sessionMembershipData); + }); + }); + describe("getOldestMembership", () => { it("returns the oldest membership event", () => { jest.useFakeTimers(); @@ -302,7 +354,7 @@ describe("MatrixRTCSession", () => { describe("onMembershipsChanged", () => { it("does not emit if no membership changes", () => { - const mockRoom = makeMockRoom(membershipTemplate); + const mockRoom = makeMockRoom([membershipTemplate]); sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom); const onMembershipsChanged = jest.fn(); @@ -313,13 +365,13 @@ describe("MatrixRTCSession", () => { }); it("emits on membership changes", () => { - const mockRoom = makeMockRoom(membershipTemplate); + const mockRoom = makeMockRoom([membershipTemplate]); sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom); const onMembershipsChanged = jest.fn(); sess.on(MatrixRTCSessionEvent.MembershipsChanged, onMembershipsChanged); - mockRoom.getLiveTimeline().getState = jest.fn().mockReturnValue(makeMockRoomState([], mockRoom.roomId)); + mockRoomState(mockRoom, []); sess.onRTCSessionMemberUpdate(); expect(onMembershipsChanged).toHaveBeenCalled(); @@ -503,18 +555,14 @@ describe("MatrixRTCSession", () => { expect(sess!.statistics.counters.roomEventEncryptionKeysSent).toEqual(1); // member2 leaves triggering key rotation - mockRoom.getLiveTimeline().getState = jest - .fn() - .mockReturnValue(makeMockRoomState([membershipTemplate], mockRoom.roomId)); + mockRoomState(mockRoom, [membershipTemplate]); sess.onRTCSessionMemberUpdate(); // member2 re-joins which should trigger an immediate re-send const keysSentPromise2 = new Promise((resolve) => { sendEventMock.mockImplementation((_roomId, _evType, payload) => resolve(payload)); }); - mockRoom.getLiveTimeline().getState = jest - .fn() - .mockReturnValue(makeMockRoomState([membershipTemplate, member2], mockRoom.roomId)); + mockRoomState(mockRoom, [membershipTemplate, member2]); sess.onRTCSessionMemberUpdate(); // but, that immediate resend is throttled so we need to wait a bit jest.advanceTimersByTime(1000); @@ -565,9 +613,7 @@ describe("MatrixRTCSession", () => { device_id: "BBBBBBB", }); - mockRoom.getLiveTimeline().getState = jest - .fn() - .mockReturnValue(makeMockRoomState([membershipTemplate, member2], mockRoom.roomId)); + mockRoomState(mockRoom, [membershipTemplate, member2]); sess.onRTCSessionMemberUpdate(); await keysSentPromise2; @@ -592,9 +638,7 @@ describe("MatrixRTCSession", () => { }); const mockRoom = makeMockRoom([member1, member2]); - mockRoom.getLiveTimeline().getState = jest - .fn() - .mockReturnValue(makeMockRoomState([member1, member2], mockRoom.roomId)); + mockRoomState(mockRoom, [member1, member2]); sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom); sess.joinRoomSession([mockFocus], mockFocus, { manageMediaKeys: true }); @@ -641,10 +685,6 @@ describe("MatrixRTCSession", () => { }; const mockRoom = makeMockRoom([member1, member2]); - mockRoom.getLiveTimeline().getState = jest - .fn() - .mockReturnValue(makeMockRoomState([member1, member2], mockRoom.roomId)); - sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom); sess.joinRoomSession([mockFocus], mockFocus, { manageMediaKeys: true }); @@ -674,6 +714,7 @@ describe("MatrixRTCSession", () => { // update created_ts member2.created_ts = 5000; + mockRoomState(mockRoom, [member1, member2]); const keysSentPromise2 = new Promise((resolve) => { sendEventMock.mockImplementation(resolve); @@ -737,9 +778,7 @@ describe("MatrixRTCSession", () => { sendEventMock.mockImplementation((_roomId, _evType, payload) => resolve(payload)); }); - mockRoom.getLiveTimeline().getState = jest - .fn() - .mockReturnValue(makeMockRoomState([membershipTemplate], mockRoom.roomId)); + mockRoomState(mockRoom, [membershipTemplate]); sess.onRTCSessionMemberUpdate(); jest.advanceTimersByTime(KEY_DELAY); @@ -784,7 +823,7 @@ describe("MatrixRTCSession", () => { it("wraps key index around to 0 when it reaches the maximum", async () => { // this should give us keys with index [0...255, 0, 1] const membersToTest = 258; - const members: SessionMembershipData[] = []; + const members: MembershipData[] = []; for (let i = 0; i < membersToTest; i++) { members.push(Object.assign({}, membershipTemplate, { device_id: `DEVICE${i}` })); } @@ -804,11 +843,7 @@ describe("MatrixRTCSession", () => { sess.joinRoomSession([mockFocus], mockFocus, { manageMediaKeys: true }); } else { // otherwise update the state reducing the membership each time in order to trigger key rotation - mockRoom.getLiveTimeline().getState = jest - .fn() - .mockReturnValue( - makeMockRoomState(members.slice(0, membersToTest - i), mockRoom.roomId), - ); + mockRoomState(mockRoom, members.slice(0, membersToTest - i)); } sess!.onRTCSessionMemberUpdate(); @@ -849,9 +884,7 @@ describe("MatrixRTCSession", () => { device_id: "BBBBBBB", }); - mockRoom.getLiveTimeline().getState = jest - .fn() - .mockReturnValue(makeMockRoomState([membershipTemplate, member2], mockRoom.roomId)); + mockRoomState(mockRoom, [membershipTemplate, member2]); sess.onRTCSessionMemberUpdate(); await new Promise((resolve) => { diff --git a/spec/unit/matrixrtc/MatrixRTCSessionManager.spec.ts b/spec/unit/matrixrtc/MatrixRTCSessionManager.spec.ts index 5cdb9278f..377e0eaf0 100644 --- a/spec/unit/matrixrtc/MatrixRTCSessionManager.spec.ts +++ b/spec/unit/matrixrtc/MatrixRTCSessionManager.spec.ts @@ -14,12 +14,10 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { type Mock } from "jest-mock"; - import { ClientEvent, EventTimeline, MatrixClient } from "../../../src"; import { RoomStateEvent } from "../../../src/models/room-state"; import { MatrixRTCSessionManagerEvents } from "../../../src/matrixrtc/MatrixRTCSessionManager"; -import { makeMockRoom, makeMockRoomState, membershipTemplate } from "./mocks"; +import { makeMockRoom, membershipTemplate, mockRoomState } from "./mocks"; describe("MatrixRTCSessionManager", () => { let client: MatrixClient; @@ -52,19 +50,16 @@ describe("MatrixRTCSessionManager", () => { it("Fires event when session ends", () => { const onEnded = jest.fn(); client.matrixRTC.on(MatrixRTCSessionManagerEvents.SessionEnded, onEnded); - const room1 = makeMockRoom(membershipTemplate); + const room1 = makeMockRoom([membershipTemplate]); jest.spyOn(client, "getRooms").mockReturnValue([room1]); jest.spyOn(client, "getRoom").mockReturnValue(room1); client.emit(ClientEvent.Room, room1); - (room1.getLiveTimeline as Mock).mockReturnValue({ - getState: jest.fn().mockReturnValue(makeMockRoomState([{}], room1.roomId)), - }); + mockRoomState(room1, [{ user_id: membershipTemplate.user_id }]); const roomState = room1.getLiveTimeline().getState(EventTimeline.FORWARDS)!; const membEvent = roomState.getStateEvents("")[0]; - client.emit(RoomStateEvent.Events, membEvent, roomState, null); expect(onEnded).toHaveBeenCalledWith(room1.roomId, client.matrixRTC.getActiveRoomSession(room1)); diff --git a/spec/unit/matrixrtc/MembershipManager.spec.ts b/spec/unit/matrixrtc/MembershipManager.spec.ts index 18b2fb630..0d1b35171 100644 --- a/spec/unit/matrixrtc/MembershipManager.spec.ts +++ b/spec/unit/matrixrtc/MembershipManager.spec.ts @@ -81,7 +81,7 @@ describe("MembershipManager", () => { // Default to fake timers. jest.useFakeTimers(); client = makeMockClient("@alice:example.org", "AAAAAAA"); - room = makeMockRoom(membershipTemplate); + room = makeMockRoom([membershipTemplate]); // Provide a default mock that is like the default "non error" server behaviour. (client._unstable_sendDelayedStateEvent as Mock).mockResolvedValue({ delay_id: "id" }); (client._unstable_updateDelayedEvent as Mock).mockResolvedValue(undefined); @@ -436,11 +436,11 @@ describe("MembershipManager", () => { type: "livekit", }, ], - device_id: client.getDeviceId(), + user_id: client.getUserId()!, + device_id: client.getDeviceId()!, created_ts: 1000, }, room.roomId, - client.getUserId()!, ), ); expect(manager.getActiveFocus()).toStrictEqual(focus); @@ -482,7 +482,7 @@ describe("MembershipManager", () => { await manager.onRTCSessionMemberUpdate([ mockCallMembership(membershipTemplate, room.roomId), - mockCallMembership(myMembership as SessionMembershipData, room.roomId, client.getUserId() ?? undefined), + mockCallMembership({ ...myMembership as SessionMembershipData, user_id: client.getUserId()! }, room.roomId), ]); await jest.advanceTimersByTimeAsync(1); @@ -797,7 +797,7 @@ describe("MembershipManager", () => { it("Should prefix log with MembershipManager used", () => { const client = makeMockClient("@alice:example.org", "AAAAAAA"); - const room = makeMockRoom(membershipTemplate); + const room = makeMockRoom([membershipTemplate]); const membershipManager = new MembershipManager(undefined, room, client, () => undefined, logger); diff --git a/spec/unit/matrixrtc/mocks.ts b/spec/unit/matrixrtc/mocks.ts index f20a9364e..d61670d79 100644 --- a/spec/unit/matrixrtc/mocks.ts +++ b/spec/unit/matrixrtc/mocks.ts @@ -20,11 +20,12 @@ import { EventType, type Room, RoomEvent, type MatrixClient, type MatrixEvent } import { CallMembership, type SessionMembershipData } from "../../../src/matrixrtc/CallMembership"; import { secureRandomString } from "../../../src/randomstring"; -type MembershipData = SessionMembershipData[] | SessionMembershipData | {}; +export type MembershipData = (SessionMembershipData | {}) & { user_id: string }; -export const membershipTemplate: SessionMembershipData = { +export const membershipTemplate: SessionMembershipData & { user_id: string } = { application: "m.call", call_id: "", + user_id: "@mock:user.example", device_id: "AAAAAAA", scope: "m.room", focus_active: { type: "livekit", focus_selection: "oldest_membership" }, @@ -68,7 +69,7 @@ export function makeMockClient(userId: string, deviceId: string): MockClient { } export function makeMockRoom( - membershipData: MembershipData, + membershipData: MembershipData[], ): Room & { emitTimelineEvent: (event: MatrixEvent) => void } { const roomId = secureRandomString(8); // Caching roomState here so it does not get recreated when calling `getLiveTimeline.getState()` @@ -87,10 +88,8 @@ export function makeMockRoom( }); } -export function makeMockRoomState(membershipData: MembershipData, roomId: string) { - const events = Array.isArray(membershipData) - ? membershipData.map((m) => mockRTCEvent(m, roomId)) - : [mockRTCEvent(membershipData, roomId)]; +function makeMockRoomState(membershipData: MembershipData[], roomId: string) { + const events = membershipData.map((m) => mockRTCEvent(m, roomId)); const keysAndEvents = events.map((e) => { const data = e.getContent() as SessionMembershipData; return [`_${e.sender?.userId}_${data.device_id}`]; @@ -120,6 +119,10 @@ export function makeMockRoomState(membershipData: MembershipData, roomId: string }; } +export function mockRoomState(room: Room, membershipData: MembershipData[]): void { + room.getLiveTimeline().getState = jest.fn().mockReturnValue(makeMockRoomState(membershipData, room.roomId)); +} + export function makeMockEvent( type: string, sender: string, @@ -138,13 +141,12 @@ export function makeMockEvent( } as unknown as MatrixEvent; } -export function mockRTCEvent(membershipData: MembershipData, roomId: string, customSender?: string): MatrixEvent { - const sender = customSender ?? "@mock:user.example"; +export function mockRTCEvent({ user_id: sender, ...membershipData }: MembershipData, roomId: string): MatrixEvent { return makeMockEvent(EventType.GroupCallMemberPrefix, sender, roomId, membershipData); } -export function mockCallMembership(membershipData: MembershipData, roomId: string, sender?: string): CallMembership { - return new CallMembership(mockRTCEvent(membershipData, roomId, sender), membershipData); +export function mockCallMembership(membershipData: MembershipData, roomId: string): CallMembership { + return new CallMembership(mockRTCEvent(membershipData, roomId), membershipData); } export function makeKey(id: number, key: string): { key: string; index: number } {