Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
41a2f47
WIP
robintown Nov 19, 2024
209eecd
temp
toger5 Aug 27, 2025
6156d4c
Fix imports
robintown Aug 27, 2025
b61e39a
Fix checkSessionsMembershipData thinking foci_preferred is required
robintown Aug 27, 2025
ca4a9c6
Merge branch 'develop' into voip-team/multi-SFU
robintown Sep 25, 2025
29879e8
incorporate CallMembership changes
toger5 Sep 30, 2025
86f33f9
use correct event type
toger5 Sep 30, 2025
bb7c23d
fix sonar cube conerns
toger5 Sep 30, 2025
8a5a8cd
callMembership tests
toger5 Sep 30, 2025
25f4d6f
make test correct
toger5 Sep 30, 2025
84a3d56
make sonar cube happy (it does not know about the type constraints...)
toger5 Sep 30, 2025
5bc970c
remove created_ts from RtcMembership
toger5 Sep 30, 2025
d94d02d
fix imports
toger5 Sep 30, 2025
74b793c
Update src/matrixrtc/IMembershipManager.ts
toger5 Oct 1, 2025
e829a7b
rename LivekitFocus.ts -> LivekitTransport.ts
toger5 Oct 1, 2025
f70cb14
add details to `getTransport`
toger5 Oct 1, 2025
a343e8c
Merge branch 'develop' into voip-team/multi-SFU
toger5 Oct 1, 2025
11f610d
review
toger5 Oct 7, 2025
66f202a
use DEFAULT_EXPIRE_DURATION in tests
toger5 Oct 7, 2025
4643844
fix test `does not provide focus if the selection method is unknown`
toger5 Oct 7, 2025
8a21ff6
Add the parent event to the CallMembership.
toger5 Oct 2, 2025
4bbb240
fix lints
toger5 Oct 6, 2025
aa1cbe9
correct usage of methods that now became async
toger5 Oct 6, 2025
65a3461
fix lints
toger5 Oct 7, 2025
e9dafb5
fix tests
toger5 Oct 7, 2025
383b219
add test
toger5 Oct 7, 2025
3c2f9b4
sonar cubes coaching ;)
toger5 Oct 7, 2025
9946143
fix mocking issues
toger5 Oct 7, 2025
fb23833
Merge branch 'develop' into toger5/use-relation-based-CallMembership-…
toger5 Oct 9, 2025
b4abbfc
introduce updateTs and connectedTs to fix expiration logic (+ fix tests)
toger5 Oct 9, 2025
61b05a0
review valere
toger5 Oct 9, 2025
06a46ac
add sticky_key to makeMyMembership
toger5 Oct 9, 2025
4608506
fix ts tests
toger5 Oct 9, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
345 changes: 198 additions & 147 deletions spec/unit/matrixrtc/CallMembership.spec.ts

Large diffs are not rendered by default.

244 changes: 158 additions & 86 deletions spec/unit/matrixrtc/MatrixRTCSession.spec.ts

Large diffs are not rendered by default.

67 changes: 44 additions & 23 deletions spec/unit/matrixrtc/MatrixRTCSessionManager.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,31 +17,36 @@ limitations under the License.
import { ClientEvent, EventTimeline, MatrixClient } from "../../../src";
import { RoomStateEvent } from "../../../src/models/room-state";
import { MatrixRTCSessionManager, MatrixRTCSessionManagerEvents } from "../../../src/matrixrtc/MatrixRTCSessionManager";
import { makeMockRoom, membershipTemplate, mockRoomState } from "./mocks";
import { makeMockRoom, sessionMembershipTemplate, mockRoomState } from "./mocks";
import { logger } from "../../../src/logger";

describe("MatrixRTCSessionManager", () => {
let client: MatrixClient;

beforeEach(() => {
beforeEach(async () => {
client = new MatrixClient({ baseUrl: "base_url" });
client.matrixRTC.start();
await client.matrixRTC.start();
});

afterEach(() => {
client.stopClient();
client.matrixRTC.stop();
});

it("Fires event when session starts", () => {
it("Fires event when session starts", async () => {
const onStarted = jest.fn();
client.matrixRTC.on(MatrixRTCSessionManagerEvents.SessionStarted, onStarted);
const { promise, resolve } = Promise.withResolvers<void>();
client.matrixRTC.on(MatrixRTCSessionManagerEvents.SessionStarted, (...v) => {
onStarted(...v);
resolve();
});

try {
const room1 = makeMockRoom([membershipTemplate]);
const room1 = makeMockRoom([sessionMembershipTemplate]);
jest.spyOn(client, "getRooms").mockReturnValue([room1]);

client.emit(ClientEvent.Room, room1);
await promise;
expect(onStarted).toHaveBeenCalledWith(room1.roomId, client.matrixRTC.getActiveRoomSession(room1));
} finally {
client.matrixRTC.off(MatrixRTCSessionManagerEvents.SessionStarted, onStarted);
Expand All @@ -53,7 +58,7 @@ describe("MatrixRTCSessionManager", () => {
client.matrixRTC.on(MatrixRTCSessionManagerEvents.SessionStarted, onStarted);

try {
const room1 = makeMockRoom([{ ...membershipTemplate, application: "m.other" }]);
const room1 = makeMockRoom([{ ...sessionMembershipTemplate, application: "m.other" }]);
jest.spyOn(client, "getRooms").mockReturnValue([room1]);

client.emit(ClientEvent.Room, room1);
Expand All @@ -63,60 +68,76 @@ describe("MatrixRTCSessionManager", () => {
}
});

it("Fires event when session ends", () => {
it("Fires event when session ends", async () => {
const onEnded = jest.fn();
client.matrixRTC.on(MatrixRTCSessionManagerEvents.SessionEnded, onEnded);
const room1 = makeMockRoom([membershipTemplate]);
const { promise: endPromise, resolve: rEnd } = Promise.withResolvers();
client.matrixRTC.once(MatrixRTCSessionManagerEvents.SessionEnded, onEnded);
const { promise: startPromise, resolve: rStart } = Promise.withResolvers();
client.matrixRTC.once(MatrixRTCSessionManagerEvents.SessionEnded, rEnd);
client.matrixRTC.once(MatrixRTCSessionManagerEvents.SessionStarted, rStart);

const room1 = makeMockRoom([sessionMembershipTemplate]);
jest.spyOn(client, "getRooms").mockReturnValue([room1]);
jest.spyOn(client, "getRoom").mockReturnValue(room1);

client.emit(ClientEvent.Room, room1);
await startPromise;

mockRoomState(room1, [{ user_id: membershipTemplate.user_id }]);

mockRoomState(room1, [{ user_id: sessionMembershipTemplate.user_id }]);
const roomState = room1.getLiveTimeline().getState(EventTimeline.FORWARDS)!;
const membEvent = roomState.getStateEvents("org.matrix.msc3401.call.member")[0];
client.emit(RoomStateEvent.Events, membEvent, roomState, null);

await endPromise;
expect(onEnded).toHaveBeenCalledWith(room1.roomId, client.matrixRTC.getActiveRoomSession(room1));
});

it("Fires correctly with for with custom sessionDescription", () => {
it("Fires correctly with for with custom sessionDescription", async () => {
const onStarted = jest.fn();
const onEnded = jest.fn();
// create a session manager with a custom session description
const sessionManager = new MatrixRTCSessionManager(logger, client, { id: "test", application: "m.notCall" });

// manually start the session manager (its not the default one started by the client)
sessionManager.start();
sessionManager.on(MatrixRTCSessionManagerEvents.SessionEnded, onEnded);
sessionManager.on(MatrixRTCSessionManagerEvents.SessionStarted, onStarted);
await sessionManager.start();
const { promise: startPromise, resolve: rStart } = Promise.withResolvers<void>();
const { promise: endPromise, resolve: rEnd } = Promise.withResolvers<void>();

sessionManager.on(MatrixRTCSessionManagerEvents.SessionEnded, (v) => {
onEnded(v);
rEnd();
});
sessionManager.on(MatrixRTCSessionManagerEvents.SessionStarted, (v) => {
onStarted(v);
rStart();
});

try {
const room1 = makeMockRoom([{ ...membershipTemplate, application: "m.other" }]);
const room1 = makeMockRoom([{ ...sessionMembershipTemplate, application: "m.other" }]);
jest.spyOn(client, "getRooms").mockReturnValue([room1]);

client.emit(ClientEvent.Room, room1);
expect(onStarted).not.toHaveBeenCalled();
onStarted.mockClear();

const room2 = makeMockRoom([{ ...membershipTemplate, application: "m.notCall", call_id: "test" }]);
const room2 = makeMockRoom([{ ...sessionMembershipTemplate, application: "m.notCall", call_id: "test" }]);
jest.spyOn(client, "getRooms").mockReturnValue([room1, room2]);

client.emit(ClientEvent.Room, room2);
await startPromise;
expect(onStarted).toHaveBeenCalled();
onStarted.mockClear();

mockRoomState(room2, [{ user_id: membershipTemplate.user_id }]);
mockRoomState(room2, [{ user_id: sessionMembershipTemplate.user_id }]);
jest.spyOn(client, "getRoom").mockReturnValue(room2);

const roomState = room2.getLiveTimeline().getState(EventTimeline.FORWARDS)!;
const membEvent = roomState.getStateEvents("org.matrix.msc3401.call.member")[0];
client.emit(RoomStateEvent.Events, membEvent, roomState, null);
await endPromise;
expect(onEnded).toHaveBeenCalled();
onEnded.mockClear();

mockRoomState(room1, [{ user_id: membershipTemplate.user_id }]);
mockRoomState(room1, [{ user_id: sessionMembershipTemplate.user_id }]);
jest.spyOn(client, "getRoom").mockReturnValue(room1);

const roomStateOther = room1.getLiveTimeline().getState(EventTimeline.FORWARDS)!;
Expand All @@ -132,13 +153,13 @@ describe("MatrixRTCSessionManager", () => {
it("Doesn't fire event if unrelated sessions ends", () => {
const onEnded = jest.fn();
client.matrixRTC.on(MatrixRTCSessionManagerEvents.SessionEnded, onEnded);
const room1 = makeMockRoom([{ ...membershipTemplate, application: "m.other_app" }]);
const room1 = makeMockRoom([{ ...sessionMembershipTemplate, application: "m.other_app" }]);
jest.spyOn(client, "getRooms").mockReturnValue([room1]);
jest.spyOn(client, "getRoom").mockReturnValue(room1);

client.emit(ClientEvent.Room, room1);

mockRoomState(room1, [{ user_id: membershipTemplate.user_id }]);
mockRoomState(room1, [{ user_id: sessionMembershipTemplate.user_id }]);

const roomState = room1.getLiveTimeline().getState(EventTimeline.FORWARDS)!;
const membEvent = roomState.getStateEvents("org.matrix.msc3401.call.member")[0];
Expand Down
32 changes: 19 additions & 13 deletions spec/unit/matrixrtc/MembershipManager.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,9 @@ import {
type SessionMembershipData,
type LivekitFocusSelection,
} from "../../../src/matrixrtc";
import { makeMockClient, makeMockRoom, membershipTemplate, mockCallMembership, type MockClient } from "./mocks";
import { makeMockClient, makeMockRoom, sessionMembershipTemplate, mockCallMembership, type MockClient } from "./mocks";
import { MembershipManager } from "../../../src/matrixrtc/MembershipManager.ts";
import { waitFor } from "../../test-utils/test-utils.ts";

/**
* Create a promise that will resolve once a mocked method is called.
Expand Down Expand Up @@ -89,7 +90,7 @@ describe("MembershipManager", () => {
// Default to fake timers.
jest.useFakeTimers();
client = makeMockClient("@alice:example.org", "AAAAAAA");
room = makeMockRoom([membershipTemplate]);
room = makeMockRoom([sessionMembershipTemplate]);
// Provide a default mock that is like the default "non error" server behaviour.
(client._unstable_sendDelayedStateEvent as Mock<any>).mockResolvedValue({ delay_id: "id" });
(client._unstable_updateDelayedEvent as Mock<any>).mockResolvedValue(undefined);
Expand Down Expand Up @@ -161,6 +162,7 @@ describe("MembershipManager", () => {
memberManager.join([], focus);
// expects
await waitForMockCall(client.sendStateEvent, Promise.resolve({ event_id: "id" }));
// This should check for send sticky once we merge with the sticky matrixRTC branch.
expect(client.sendStateEvent).toHaveBeenCalledWith(
room.roomId,
"org.matrix.msc4143.rtc.member",
Expand All @@ -174,6 +176,7 @@ describe("MembershipManager", () => {
slot_id: "m.call#",
rtc_transports: [focus],
versions: [],
msc4354_sticky_key: "_@alice:example.org_AAAAAAA_m.call",
},
"_@alice:example.org_AAAAAAA_m.call",
);
Expand Down Expand Up @@ -384,7 +387,7 @@ describe("MembershipManager", () => {
const { resolve } = createAsyncHandle(client._unstable_sendDelayedStateEvent);
await jest.advanceTimersByTimeAsync(RESTART_DELAY);
// first simulate the sync, then resolve sending the delayed event.
await manager.onRTCSessionMemberUpdate([mockCallMembership(membershipTemplate, room.roomId)]);
await manager.onRTCSessionMemberUpdate([mockCallMembership(sessionMembershipTemplate, room.roomId)]);
resolve({ delay_id: "id" });
// Let the scheduler run one iteration so that the new join gets sent
await jest.runOnlyPendingTimersAsync();
Expand Down Expand Up @@ -467,7 +470,7 @@ describe("MembershipManager", () => {
describe("onRTCSessionMemberUpdate()", () => {
it("does nothing if not joined", async () => {
const manager = new MembershipManager({}, room, client, callSession);
await manager.onRTCSessionMemberUpdate([mockCallMembership(membershipTemplate, room.roomId)]);
await manager.onRTCSessionMemberUpdate([mockCallMembership(sessionMembershipTemplate, room.roomId)]);
await jest.advanceTimersToNextTimerAsync();
expect(client.sendStateEvent).not.toHaveBeenCalled();
expect(client._unstable_sendDelayedStateEvent).not.toHaveBeenCalled();
Expand All @@ -484,7 +487,7 @@ describe("MembershipManager", () => {
(client._unstable_sendDelayedStateEvent as Mock).mockClear();

await manager.onRTCSessionMemberUpdate([
mockCallMembership(membershipTemplate, room.roomId),
mockCallMembership(sessionMembershipTemplate, room.roomId),
mockCallMembership(
{ ...(myMembership as SessionMembershipData), user_id: client.getUserId()! },
room.roomId,
Expand All @@ -507,7 +510,7 @@ describe("MembershipManager", () => {
(client._unstable_sendDelayedStateEvent as Mock).mockClear();

// Our own membership is removed:
await manager.onRTCSessionMemberUpdate([mockCallMembership(membershipTemplate, room.roomId)]);
await manager.onRTCSessionMemberUpdate([mockCallMembership(sessionMembershipTemplate, room.roomId)]);
await jest.advanceTimersByTimeAsync(1);
expect(client.sendStateEvent).toHaveBeenCalled();
expect(client._unstable_sendDelayedStateEvent).toHaveBeenCalled();
Expand All @@ -530,7 +533,7 @@ describe("MembershipManager", () => {

const { resolve } = createAsyncHandle(client._unstable_sendDelayedStateEvent);
await jest.advanceTimersByTimeAsync(10_000);
await manager.onRTCSessionMemberUpdate([mockCallMembership(membershipTemplate, room.roomId)]);
await manager.onRTCSessionMemberUpdate([mockCallMembership(sessionMembershipTemplate, room.roomId)]);
resolve({ delay_id: "id" });
await jest.advanceTimersByTimeAsync(10_000);

Expand Down Expand Up @@ -899,7 +902,10 @@ describe("MembershipManager", () => {
const manager = new MembershipManager({}, room, client, callSession);
manager.join([]);
expect(manager.isActivated()).toEqual(true);
const membership = mockCallMembership({ ...membershipTemplate, user_id: client.getUserId()! }, room.roomId);
const membership = mockCallMembership(
{ ...sessionMembershipTemplate, user_id: client.getUserId()! },
room.roomId,
);
await manager.onRTCSessionMemberUpdate([membership]);
await manager.updateCallIntent("video");
expect(client.sendStateEvent).toHaveBeenCalledTimes(2);
Expand All @@ -913,7 +919,7 @@ describe("MembershipManager", () => {
manager.join([]);
expect(manager.isActivated()).toEqual(true);
const membership = mockCallMembership(
{ ...membershipTemplate, "user_id": client.getUserId()!, "m.call.intent": "video" },
{ ...sessionMembershipTemplate, "user_id": client.getUserId()!, "m.call.intent": "video" },
room.roomId,
);
await manager.onRTCSessionMemberUpdate([membership]);
Expand All @@ -923,18 +929,18 @@ describe("MembershipManager", () => {
});
});

it("Should prefix log with MembershipManager used", () => {
it("Should prefix log with MembershipManager used", async () => {
const spy = jest.spyOn(console, "error");
const client = makeMockClient("@alice:example.org", "AAAAAAA");
const room = makeMockRoom([membershipTemplate]);
const room = makeMockRoom([sessionMembershipTemplate]);

const membershipManager = new MembershipManager(undefined, room, client, callSession);

const spy = jest.spyOn(console, "error");
// Double join
membershipManager.join([]);
membershipManager.join([]);

expect(spy).toHaveBeenCalled();
await waitFor(() => expect(spy).toHaveBeenCalled());
const logline: string = spy.mock.calls[0][0];
expect(logline.startsWith("[MembershipManager]")).toBe(true);
});
4 changes: 2 additions & 2 deletions spec/unit/matrixrtc/RTCEncryptionManager.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import { RTCEncryptionManager } from "../../../src/matrixrtc/RTCEncryptionManage
import { type CallMembership, type Statistics } from "../../../src/matrixrtc";
import { type ToDeviceKeyTransport } from "../../../src/matrixrtc/ToDeviceKeyTransport.ts";
import { KeyTransportEvents, type KeyTransportEventsHandlerMap } from "../../../src/matrixrtc/IKeyTransport.ts";
import { membershipTemplate, mockCallMembership } from "./mocks.ts";
import { sessionMembershipTemplate, mockCallMembership } from "./mocks.ts";
import { decodeBase64, TypedEventEmitter } from "../../../src";
import { RoomAndToDeviceTransport } from "../../../src/matrixrtc/RoomAndToDeviceKeyTransport.ts";
import { type RoomKeyTransport } from "../../../src/matrixrtc/RoomKeyTransport.ts";
Expand Down Expand Up @@ -864,7 +864,7 @@ describe("RTCEncryptionManager", () => {

function aCallMembership(userId: string, deviceId: string, ts: number = 1000): CallMembership {
return mockCallMembership(
{ ...membershipTemplate, user_id: userId, device_id: deviceId, created_ts: ts },
{ ...sessionMembershipTemplate, user_id: userId, device_id: deviceId, created_ts: ts },
"!room:id",
);
}
Expand Down
8 changes: 4 additions & 4 deletions spec/unit/matrixrtc/RoomKeyTransport.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/

import { makeMockEvent, makeMockRoom, membershipTemplate, makeKey } from "./mocks";
import { makeMockEvent, makeMockRoom, sessionMembershipTemplate, makeKey } from "./mocks";
import { RoomKeyTransport } from "../../../src/matrixrtc/RoomKeyTransport";
import { KeyTransportEvents } from "../../../src/matrixrtc/IKeyTransport";
import { EventType, MatrixClient, RoomEvent } from "../../../src";
Expand All @@ -31,7 +31,7 @@ describe("RoomKeyTransport", () => {
let mockLogger: Mocked<Logger>;

const onCallEncryptionMock = jest.fn();
beforeEach(() => {
beforeEach(async () => {
onCallEncryptionMock.mockReset();
mockLogger = {
debug: jest.fn(),
Expand All @@ -48,9 +48,9 @@ describe("RoomKeyTransport", () => {
roomEventEncryptionKeysReceivedTotalAge: 0,
},
};
room = makeMockRoom([membershipTemplate]);
room = makeMockRoom([sessionMembershipTemplate]);
client = new MatrixClient({ baseUrl: "base_url" });
client.matrixRTC.start();
await client.matrixRTC.start();
transport = new RoomKeyTransport(room, client, statistics, {
getChild: jest.fn().mockReturnValue(mockLogger),
} as unknown as Mocked<Logger>);
Expand Down
Loading