From c332955b4128e7e56a909b066f915ef49fb32e2e Mon Sep 17 00:00:00 2001 From: Half-Shot Date: Tue, 30 Sep 2025 13:23:12 +0100 Subject: [PATCH] more lint --- spec/unit/matrixrtc/MatrixRTCSession.spec.ts | 93 ++++++++++++++++++-- spec/unit/matrixrtc/mocks.ts | 20 +++-- src/matrixrtc/CallMembership.ts | 2 +- src/matrixrtc/MatrixRTCSession.ts | 2 +- 4 files changed, 103 insertions(+), 14 deletions(-) diff --git a/spec/unit/matrixrtc/MatrixRTCSession.spec.ts b/spec/unit/matrixrtc/MatrixRTCSession.spec.ts index 72ef9d8f8..891189b4f 100644 --- a/spec/unit/matrixrtc/MatrixRTCSession.spec.ts +++ b/spec/unit/matrixrtc/MatrixRTCSession.spec.ts @@ -14,11 +14,27 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { encodeBase64, EventType, MatrixClient, type MatrixError, type MatrixEvent, type Room } from "../../../src"; +import { + encodeBase64, + type EventTimeline, + EventType, + MatrixClient, + type MatrixError, + type MatrixEvent, + type Room, +} from "../../../src"; import { KnownMembership } from "../../../src/@types/membership"; import { MatrixRTCSession, MatrixRTCSessionEvent } from "../../../src/matrixrtc/MatrixRTCSession"; import { Status, type EncryptionKeysEventContent } from "../../../src/matrixrtc/types"; -import { makeMockEvent, makeMockRoom, membershipTemplate, makeKey, type MembershipData, mockRoomState } from "./mocks"; +import { + makeMockEvent, + makeMockRoom, + membershipTemplate, + makeKey, + type MembershipData, + mockRoomState, + mockRTCEvent, +} from "./mocks"; import { RTCEncryptionManager } from "../../../src/matrixrtc/RTCEncryptionManager.ts"; const mockFocus = { type: "mock" }; @@ -118,7 +134,7 @@ describe("MatrixRTCSession", () => { it("ignores memberships events of members not in the room", () => { const mockRoom = makeMockRoom([membershipTemplate], testConfig.testCreateSticky); - mockRoom.hasMembershipState = (state) => state === KnownMembership.Join; + mockRoom.hasMembershipState.mockImplementation((state) => state === KnownMembership.Join); sess = MatrixRTCSession.sessionForRoom(client, mockRoom, callSession, testConfig); expect(sess?.memberships.length).toEqual(0); }); @@ -150,7 +166,7 @@ describe("MatrixRTCSession", () => { getLocalAge: jest.fn().mockReturnValue(0), }; const mockRoom = makeMockRoom([]); - mockRoom.getLiveTimeline = jest.fn().mockReturnValue({ + mockRoom.getLiveTimeline.mockReturnValue({ getState: jest.fn().mockReturnValue({ on: jest.fn(), off: jest.fn(), @@ -167,7 +183,7 @@ describe("MatrixRTCSession", () => { ], ]), }), - }); + } as unknown as EventTimeline); sess = MatrixRTCSession.sessionForRoom(client, mockRoom, callSession, testConfig); expect(sess.memberships).toHaveLength(0); }); @@ -181,7 +197,7 @@ describe("MatrixRTCSession", () => { getLocalAge: jest.fn().mockReturnValue(0), }; const mockRoom = makeMockRoom([]); - mockRoom.getLiveTimeline = jest.fn().mockReturnValue({ + mockRoom.getLiveTimeline.mockReturnValue({ getState: jest.fn().mockReturnValue({ on: jest.fn(), off: jest.fn(), @@ -198,7 +214,7 @@ describe("MatrixRTCSession", () => { ], ]), }), - }); + } as unknown as EventTimeline); sess = MatrixRTCSession.sessionForRoom(client, mockRoom, callSession, testConfig); expect(sess.memberships).toHaveLength(0); }); @@ -221,6 +237,69 @@ describe("MatrixRTCSession", () => { }, ); + describe("roomSessionForRoom combined state", () => { + it("perfers sticky events when both membership and sticky events appear for the same user", () => { + // Create a room with identical member state and sticky state for the same user. + const mockRoom = makeMockRoom([membershipTemplate]); + mockRoom.unstableGetStickyEvents.mockImplementation(() => { + const ev = mockRTCEvent( + { + ...membershipTemplate, + msc4354_sticky_key: `_${membershipTemplate.user_id}_${membershipTemplate.device_id}`, + }, + mockRoom.roomId, + ); + return [ev]; + }); + + // Expect for there to be one membership as the state has been merged down. + sess = MatrixRTCSession.sessionForRoom(client, mockRoom, callSession, { + listenForStickyEvents: true, + listenForMemberStateEvents: true, + }); + expect(sess?.memberships.length).toEqual(1); + expect(sess?.memberships[0].sessionDescription.id).toEqual(""); + expect(sess?.memberships[0].scope).toEqual("m.room"); + expect(sess?.memberships[0].application).toEqual("m.call"); + expect(sess?.memberships[0].deviceId).toEqual("AAAAAAA"); + expect(sess?.memberships[0].isExpired()).toEqual(false); + expect(sess?.sessionDescription.id).toEqual(""); + }); + it("combines sticky and membership events when both exist", () => { + // Create a room with identical member state and sticky state for the same user. + const mockRoom = makeMockRoom([membershipTemplate]); + const otherUserId = "@othermock:user.example"; + mockRoom.unstableGetStickyEvents.mockImplementation(() => { + const ev = mockRTCEvent( + { + ...membershipTemplate, + user_id: otherUserId, + msc4354_sticky_key: `_${otherUserId}_${membershipTemplate.device_id}`, + }, + mockRoom.roomId, + ); + return [ev]; + }); + + // Expect two membership events, sticky events always coming first. + sess = MatrixRTCSession.sessionForRoom(client, mockRoom, callSession, { + listenForStickyEvents: true, + listenForMemberStateEvents: true, + }); + expect(sess?.memberships.length).toEqual(2); + expect(sess?.memberships[0].sender).toEqual(otherUserId); + expect(sess?.memberships[0].sessionDescription.id).toEqual(""); + expect(sess?.memberships[0].scope).toEqual("m.room"); + expect(sess?.memberships[0].application).toEqual("m.call"); + expect(sess?.memberships[0].deviceId).toEqual("AAAAAAA"); + expect(sess?.memberships[0].isExpired()).toEqual(false); + + expect(sess?.memberships[1].sender).toEqual(membershipTemplate.user_id); + + expect(sess?.sessionDescription.id).toEqual(""); + }); + }); + describe("getOldestMembership", () => { it("returns the oldest membership event", () => { jest.useFakeTimers(); diff --git a/spec/unit/matrixrtc/mocks.ts b/spec/unit/matrixrtc/mocks.ts index ed5c680e5..5b6641cf3 100644 --- a/spec/unit/matrixrtc/mocks.ts +++ b/spec/unit/matrixrtc/mocks.ts @@ -15,6 +15,7 @@ limitations under the License. */ import { EventEmitter } from "stream"; +import { type Mocked } from "jest-mock"; import { EventType, type Room, RoomEvent, type MatrixClient, type MatrixEvent } from "../../../src"; import { CallMembership, type SessionMembershipData } from "../../../src/matrixrtc/CallMembership"; @@ -75,7 +76,7 @@ export function makeMockClient(userId: string, deviceId: string): MockClient { export function makeMockRoom( membershipData: MembershipData[], useStickyEvents = false, -): Room & { emitTimelineEvent: (event: MatrixEvent) => void } { +): Mocked void }> { const roomId = secureRandomString(8); // Caching roomState here so it does not get recreated when calling `getLiveTimeline.getState()` const roomState = makeMockRoomState(useStickyEvents ? [] : membershipData, roomId); @@ -91,12 +92,12 @@ export function makeMockRoom( .fn() .mockImplementation(() => useStickyEvents ? membershipData.map((m) => mockRTCEvent(m, roomId, 10000, ts)) : [], - ), - }) as unknown as Room; + ) as any, + }); return Object.assign(room, { emitTimelineEvent: (event: MatrixEvent) => room.emit(RoomEvent.Timeline, event, room, undefined, false, {} as any), - }); + }) as unknown as Mocked void }>; } function makeMockRoomState(membershipData: MembershipData[], roomId: string) { @@ -140,6 +141,7 @@ export function makeMockEvent( roomId: string | undefined, content: any, timestamp?: number, + stateKey?: string, ): MatrixEvent { return { getType: jest.fn().mockReturnValue(type), @@ -148,6 +150,7 @@ export function makeMockEvent( getTs: jest.fn().mockReturnValue(timestamp ?? Date.now()), getRoomId: jest.fn().mockReturnValue(roomId), getId: jest.fn().mockReturnValue(secureRandomString(8)), + getStateKey: jest.fn().mockReturnValue(stateKey), isDecryptionFailure: jest.fn().mockReturnValue(false), } as unknown as MatrixEvent; } @@ -159,7 +162,14 @@ export function mockRTCEvent( timestamp?: number, ): MatrixEvent { return { - ...makeMockEvent(EventType.GroupCallMemberPrefix, sender, roomId, membershipData, timestamp), + ...makeMockEvent( + EventType.GroupCallMemberPrefix, + sender, + roomId, + membershipData, + timestamp, + !stickyDuration && "device_id" in membershipData ? `_${sender}_${membershipData.device_id}` : "", + ), unstableStickyContent: { duration_ms: stickyDuration, }, diff --git a/src/matrixrtc/CallMembership.ts b/src/matrixrtc/CallMembership.ts index e225a57d3..37c96c38b 100644 --- a/src/matrixrtc/CallMembership.ts +++ b/src/matrixrtc/CallMembership.ts @@ -96,7 +96,7 @@ export type SessionMembershipData = { /** * the sticky key for sticky events packed application + device_id making up the used slot + device. */ - "sticky_key"?: string; + "msc4354_sticky_key"?: string; }; const checkSessionsMembershipData = ( diff --git a/src/matrixrtc/MatrixRTCSession.ts b/src/matrixrtc/MatrixRTCSession.ts index 58d2eb9e5..b1272744c 100644 --- a/src/matrixrtc/MatrixRTCSession.ts +++ b/src/matrixrtc/MatrixRTCSession.ts @@ -322,7 +322,7 @@ export class MatrixRTCSession extends TypedEventEmitter< let callMemberEvents = [] as MatrixEvent[]; if (listenForStickyEvents) { // prefill with sticky events - callMemberEvents = Array.from(room.unstableGetStickyEvents()).filter( + callMemberEvents = [...room.unstableGetStickyEvents()].filter( (e) => e.getType() === EventType.GroupCallMemberPrefix, ); }