Skip to content

Commit

Permalink
MatrixRTC: MembershipManager test cases and deprecation of MatrixRTCS…
Browse files Browse the repository at this point in the history
…ession.room (#4713)

* WIP doodles on MembershipManager test cases

* .

* initial membership manager test setup.

* Updates from discussion

* revert renaming comments

* remove unused import

* fix leave delayed event resend test.
It was missing a flush.

* comment out and remove unused variables

* es lint

* use jsdom instead of node test environment

* remove unused variables

* remove unused export

* temp

* review

* fixup tests

* more review

* remove wait for expect dependency

* flatten tests and add comments

* add more leave test cases

* use defer

* remove @jest/environment dependency

* Cleanup awaits and Make mock types more correct.
Make every mock return a Promise if the real implementation does return a pormise.

* remove flush promise dependency

* add linting to matrixrtc tests

* Add fix async lints and use matrix rtc logger for test environment.

* prettier

* change to MatrixRTCSession logger

* make accessing the full room deprecated

* remove deprecated usage of full room

* Clean up the deprecation

---------

Co-authored-by: Hugh Nimmo-Smith <hughns@matrix.org>
  • Loading branch information
toger5 and hughns authored Feb 27, 2025
1 parent d81929d commit a3bbc49
Show file tree
Hide file tree
Showing 9 changed files with 758 additions and 151 deletions.
2 changes: 1 addition & 1 deletion .eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ module.exports = {
},
{
// Enable stricter promise rules for the MatrixRTC codebase
files: ["src/matrixrtc/**/*.ts"],
files: ["src/matrixrtc/**/*.ts", "spec/unit/matrixrtc/*.ts"],
rules: {
// Encourage proper usage of Promises:
"@typescript-eslint/no-floating-promises": "error",
Expand Down
134 changes: 7 additions & 127 deletions spec/unit/matrixrtc/MatrixRTCSession.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,12 @@ See the License for the specific language governing permissions and
limitations under the License.
*/

import { encodeBase64, EventType, MatrixClient, MatrixError, type MatrixEvent, type Room } from "../../../src";
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 { MatrixRTCSession, MatrixRTCSessionEvent } from "../../../src/matrixrtc/MatrixRTCSession";
import { type EncryptionKeysEventContent } from "../../../src/matrixrtc/types";
import { secureRandomString } from "../../../src/randomstring";
import { flushPromises } from "../../test-utils/flushPromises";
import { makeMockRoom, makeMockRoomState, membershipTemplate } from "./mocks";

const mockFocus = { type: "mock" };
Expand All @@ -37,10 +36,10 @@ describe("MatrixRTCSession", () => {
client.getDeviceId = jest.fn().mockReturnValue("AAAAAAA");
});

afterEach(() => {
afterEach(async () => {
client.stopClient();
client.matrixRTC.stop();
if (sess) sess.stop();
if (sess) await sess.stop();
sess = undefined;
});

Expand Down Expand Up @@ -322,11 +321,9 @@ describe("MatrixRTCSession", () => {
let sendStateEventMock: jest.Mock;
let sendDelayedStateMock: jest.Mock;
let sendEventMock: jest.Mock;
let updateDelayedEventMock: jest.Mock;

let sentStateEvent: Promise<void>;
let sentDelayedState: Promise<void>;
let updatedDelayedEvent: Promise<void>;

beforeEach(() => {
sentStateEvent = new Promise((resolve) => {
Expand All @@ -340,15 +337,12 @@ describe("MatrixRTCSession", () => {
};
});
});
updatedDelayedEvent = new Promise((r) => {
updateDelayedEventMock = jest.fn(r);
});
sendEventMock = jest.fn();
client.sendStateEvent = sendStateEventMock;
client._unstable_sendDelayedStateEvent = sendDelayedStateMock;
client.sendEvent = sendEventMock;

client._unstable_updateDelayedEvent = updateDelayedEventMock;
client._unstable_updateDelayedEvent = jest.fn();

mockRoom = makeMockRoom([]);
sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
Expand Down Expand Up @@ -432,120 +426,6 @@ describe("MatrixRTCSession", () => {
expect(client._unstable_sendDelayedStateEvent).toHaveBeenCalledTimes(1);
jest.useRealTimers();
});

describe("calls", () => {
const activeFocusConfig = { type: "livekit", livekit_service_url: "https://active.url" };
const activeFocus = { type: "livekit", focus_selection: "oldest_membership" };

async function testJoin(useOwnedStateEvents: boolean): Promise<void> {
if (useOwnedStateEvents) {
mockRoom.getVersion = jest.fn().mockReturnValue("org.matrix.msc3757.default");
}

jest.useFakeTimers();

// preparing the delayed disconnect should handle the delay being too long
const sendDelayedStateExceedAttempt = new Promise<void>((resolve) => {
const error = new MatrixError({
"errcode": "M_UNKNOWN",
"org.matrix.msc4140.errcode": "M_MAX_DELAY_EXCEEDED",
"org.matrix.msc4140.max_delay": 7500,
});
sendDelayedStateMock.mockImplementationOnce(() => {
resolve();
return Promise.reject(error);
});
});

const userStateKey = `${!useOwnedStateEvents ? "_" : ""}@alice:example.org_AAAAAAA`;
// preparing the delayed disconnect should handle ratelimiting
const sendDelayedStateAttempt = new Promise<void>((resolve) => {
const error = new MatrixError({ errcode: "M_LIMIT_EXCEEDED" });
sendDelayedStateMock.mockImplementationOnce(() => {
resolve();
return Promise.reject(error);
});
});

// setting the membership state should handle ratelimiting (also with a retry-after value)
const sendStateEventAttempt = new Promise<void>((resolve) => {
const error = new MatrixError(
{ errcode: "M_LIMIT_EXCEEDED" },
429,
undefined,
undefined,
new Headers({ "Retry-After": "1" }),
);
sendStateEventMock.mockImplementationOnce(() => {
resolve();
return Promise.reject(error);
});
});

sess!.joinRoomSession([activeFocusConfig], activeFocus, {
membershipServerSideExpiryTimeout: 9000,
});

await sendDelayedStateExceedAttempt.then(); // needed to resolve after the send attempt catches
await sendDelayedStateAttempt;
const callProps = (d: number) => {
return [mockRoom!.roomId, { delay: d }, "org.matrix.msc3401.call.member", {}, userStateKey];
};
expect(client._unstable_sendDelayedStateEvent).toHaveBeenNthCalledWith(1, ...callProps(9000));
expect(client._unstable_sendDelayedStateEvent).toHaveBeenNthCalledWith(2, ...callProps(7500));

jest.advanceTimersByTime(5000);

await sendStateEventAttempt.then(); // needed to resolve after resendIfRateLimited catches
jest.advanceTimersByTime(1000);

await sentStateEvent;
expect(client.sendStateEvent).toHaveBeenCalledWith(
mockRoom!.roomId,
EventType.GroupCallMemberPrefix,
{
application: "m.call",
scope: "m.room",
call_id: "",
expires: 14400000,
device_id: "AAAAAAA",
foci_preferred: [activeFocusConfig],
focus_active: activeFocus,
} satisfies SessionMembershipData,
userStateKey,
);
await sentDelayedState;

// should have prepared the heartbeat to keep delaying the leave event while still connected
await updatedDelayedEvent;
expect(client._unstable_updateDelayedEvent).toHaveBeenCalledTimes(1);

// ensures that we reach the code that schedules the timeout for the next delay update before we advance the timers.
await flushPromises();
jest.advanceTimersByTime(5000);
// should update delayed disconnect
expect(client._unstable_updateDelayedEvent).toHaveBeenCalledTimes(2);

jest.useRealTimers();
}

it("sends a membership event with session payload when joining a call", async () => {
await testJoin(false);
});

it("does not prefix the state key with _ for rooms that support user-owned state events", async () => {
await testJoin(true);
});
});

it("does nothing if join called when already joined", async () => {
sess!.joinRoomSession([mockFocus], mockFocus);
await sentStateEvent;
expect(client.sendStateEvent).toHaveBeenCalledTimes(1);

sess!.joinRoomSession([mockFocus], mockFocus);
expect(client.sendStateEvent).toHaveBeenCalledTimes(1);
});
});

describe("onMembershipsChanged", () => {
Expand Down Expand Up @@ -616,9 +496,9 @@ describe("MatrixRTCSession", () => {
sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
});

afterEach(() => {
afterEach(async () => {
// stop the timers
sess!.leaveRoomSession();
await sess!.leaveRoomSession();
});

it("creates a key when joining", () => {
Expand Down Expand Up @@ -715,7 +595,7 @@ describe("MatrixRTCSession", () => {
}
});

it("cancels key send event that fail", async () => {
it("cancels key send event that fail", () => {
const eventSentinel = {} as unknown as MatrixEvent;

client.cancelPendingEvent = jest.fn();
Expand Down
2 changes: 1 addition & 1 deletion spec/unit/matrixrtc/MatrixRTCSessionManager.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ import { makeMockRoom, makeMockRoomState, membershipTemplate } from "./mocks";
describe("MatrixRTCSessionManager", () => {
let client: MatrixClient;

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

0 comments on commit a3bbc49

Please sign in to comment.