Skip to content

Allow sending notification events when starting a call #4826

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 2 commits into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
105 changes: 66 additions & 39 deletions spec/unit/matrixrtc/MatrixRTCSession.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,11 @@ limitations under the License.

import { encodeBase64, EventType, MatrixClient, type MatrixError, type MatrixEvent, type Room } from "../../../src";
import { KnownMembership } from "../../../src/@types/membership";
import { DEFAULT_EXPIRE_DURATION, type SessionMembershipData } from "../../../src/matrixrtc/CallMembership";
import { DEFAULT_EXPIRE_DURATION } 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";

const mockFocus = { type: "mock" };

Expand All @@ -47,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);
Expand All @@ -74,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);
Expand Down Expand Up @@ -205,10 +205,11 @@ describe("MatrixRTCSession", () => {
const mockFocus = { type: "livekit", livekit_service_url: "https://test.org" };
const joinSessionConfig = {};

const sessionMembershipData: SessionMembershipData = {
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],
Expand Down Expand Up @@ -236,8 +237,8 @@ describe("MatrixRTCSession", () => {
client._unstable_sendDelayedStateEvent = sendDelayedStateMock;
});

async function testSession(membershipData: SessionMembershipData): Promise<void> {
sess = MatrixRTCSession.roomSessionForRoom(client, makeMockRoom(membershipData));
async function testSession(membershipData: MembershipData): Promise<void> {
sess = MatrixRTCSession.roomSessionForRoom(client, makeMockRoom([membershipData]));

sess.joinRoomSession([mockFocus], mockFocus, joinSessionConfig);
await Promise.race([sentStateEvent, new Promise((resolve) => setTimeout(resolve, 500))]);
Expand Down Expand Up @@ -339,7 +340,7 @@ describe("MatrixRTCSession", () => {
};
});
});
sendEventMock = jest.fn();
sendEventMock = jest.fn().mockResolvedValue(undefined);
client.sendStateEvent = sendStateEventMock;
client._unstable_sendDelayedStateEvent = sendDelayedStateMock;
client.sendEvent = sendEventMock;
Expand Down Expand Up @@ -368,6 +369,51 @@ describe("MatrixRTCSession", () => {
expect(sess!.isJoined()).toEqual(true);
});

it("sends a notification when starting a call", async () => {
// Simulate a join, including the update to the room state
sess!.joinRoomSession([mockFocus], mockFocus, { notifyType: "ring" });
await Promise.race([sentStateEvent, new Promise((resolve) => setTimeout(resolve, 5000))]);
mockRoomState(mockRoom, [{ ...membershipTemplate, user_id: client.getUserId()! }]);
sess!.onRTCSessionMemberUpdate();

expect(client.sendEvent).toHaveBeenCalledWith(mockRoom!.roomId, EventType.CallNotify, {
"application": "m.call",
"m.mentions": { user_ids: [], room: true },
"notify_type": "ring",
"call_id": "",
});
});

it("doesn't send a notification when joining an existing call", async () => {
// Add another member to the call so that it is considered an existing call
mockRoomState(mockRoom, [membershipTemplate]);
sess!.onRTCSessionMemberUpdate();

// Simulate a join, including the update to the room state
sess!.joinRoomSession([mockFocus], mockFocus, { notifyType: "ring" });
await Promise.race([sentStateEvent, new Promise((resolve) => setTimeout(resolve, 5000))]);
mockRoomState(mockRoom, [membershipTemplate, { ...membershipTemplate, user_id: client.getUserId()! }]);
sess!.onRTCSessionMemberUpdate();

expect(client.sendEvent).not.toHaveBeenCalled();
});

it("doesn't send a notification when someone else starts the call faster than us", async () => {
// Simulate a join, including the update to the room state
sess!.joinRoomSession([mockFocus], mockFocus, { notifyType: "ring" });
await Promise.race([sentStateEvent, new Promise((resolve) => setTimeout(resolve, 5000))]);
// But this time we want to simulate a race condition in which we receive a state event
// from someone else, starting the call before our own state event has been sent
mockRoomState(mockRoom, [membershipTemplate]);
sess!.onRTCSessionMemberUpdate();
mockRoomState(mockRoom, [membershipTemplate, { ...membershipTemplate, user_id: client.getUserId()! }]);
sess!.onRTCSessionMemberUpdate();

// We assume that the responsibility to send a notification, if any, lies with the other
// participant that won the race
expect(client.sendEvent).not.toHaveBeenCalled();
});

it("sends a membership event when joining a call", async () => {
const realSetTimeout = setTimeout;
jest.useFakeTimers();
Expand Down Expand Up @@ -432,7 +478,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();
Expand All @@ -443,13 +489,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();
Expand Down Expand Up @@ -635,18 +681,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<EncryptionKeysEventContent>((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);
Expand Down Expand Up @@ -697,9 +739,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;
Expand All @@ -724,9 +764,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 });
Expand Down Expand Up @@ -773,10 +811,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 });

Expand Down Expand Up @@ -806,6 +840,7 @@ describe("MatrixRTCSession", () => {

// update created_ts
member2.created_ts = 5000;
mockRoomState(mockRoom, [member1, member2]);

const keysSentPromise2 = new Promise((resolve) => {
sendEventMock.mockImplementation(resolve);
Expand Down Expand Up @@ -869,9 +904,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);
Expand Down Expand Up @@ -900,7 +933,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}` }));
}
Expand All @@ -920,11 +953,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();
Expand Down Expand Up @@ -965,9 +994,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) => {
Expand Down
11 changes: 3 additions & 8 deletions spec/unit/matrixrtc/MatrixRTCSessionManager.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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));
Expand Down
10 changes: 5 additions & 5 deletions spec/unit/matrixrtc/MembershipManager.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ describe.each([
// 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<any>).mockResolvedValue({ delay_id: "id" });
(client._unstable_updateDelayedEvent as Mock<any>).mockResolvedValue(undefined);
Expand Down Expand Up @@ -454,11 +454,11 @@ describe.each([
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);
Expand Down Expand Up @@ -500,7 +500,7 @@ describe.each([

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);
Expand Down Expand Up @@ -817,7 +817,7 @@ describe.each([

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);

Expand Down
4 changes: 2 additions & 2 deletions spec/unit/matrixrtc/RoomAndToDeviceTransport.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ describe("RoomAndToDeviceTransport", () => {
});
it("only sends to device keys when sending a key", async () => {
transport.start();
await transport.sendKey("1235", 0, [mockCallMembership(membershipTemplate, roomId, "@alice:example.org")]);
await transport.sendKey("1235", 0, [mockCallMembership({ ...membershipTemplate, user_id: '@alice:example.org' }, roomId)]);
expect(toDeviceSendKeySpy).toHaveBeenCalledTimes(1);
expect(roomSendKeySpy).toHaveBeenCalledTimes(0);
expect(transport.enabled.room).toBeFalsy();
Expand Down Expand Up @@ -118,7 +118,7 @@ describe("RoomAndToDeviceTransport", () => {
expect(transport.enabled.room).toBeTruthy();
expect(transport.enabled.toDevice).toBeFalsy();

await transport.sendKey("1235", 0, [mockCallMembership(membershipTemplate, roomId, "@alice:example.org")]);
await transport.sendKey("1235", 0, [mockCallMembership({ ...membershipTemplate, user_id: '@alice:example.org' }, roomId)]);
expect(sendEventMock).toHaveBeenCalledTimes(1);
expect(roomSendKeySpy).toHaveBeenCalledTimes(1);
expect(toDeviceSendKeySpy).toHaveBeenCalledTimes(0);
Expand Down
12 changes: 4 additions & 8 deletions spec/unit/matrixrtc/ToDeviceKeyTransport.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,19 +62,16 @@ describe("ToDeviceKeyTransport", () => {
const keyIndex = 2;
await transport.sendKey(keyBase64Encoded, keyIndex, [
mockCallMembership(
Object.assign({}, membershipTemplate, { device_id: "BOBDEVICE" }),
{ ...membershipTemplate, user_id: "@bob:example.org", device_id: "BOBDEVICE" },
roomId,
"@bob:example.org",
),
mockCallMembership(
Object.assign({}, membershipTemplate, { device_id: "CARLDEVICE" }),
{ ...membershipTemplate, user_id: "@carl:example.org", device_id: "CARLDEVICE" },
roomId,
"@carl:example.org",
),
mockCallMembership(
Object.assign({}, membershipTemplate, { device_id: "MATDEVICE" }),
{ ...membershipTemplate, user_id: "@mat:example.org", device_id: "MATDEVICE" },
roomId,
"@mat:example.org",
),
]);

Expand Down Expand Up @@ -154,9 +151,8 @@ describe("ToDeviceKeyTransport", () => {
const keyIndex = 2;
await transport.sendKey(keyBase64Encoded, keyIndex, [
mockCallMembership(
Object.assign({}, membershipTemplate, { device_id: "MYDEVICE" }),
{ ...membershipTemplate, user_id: '@alice:example.org', device_id: 'MYDEVICE' },
roomId,
"@alice:example.org",
),
]);

Expand Down
Loading