diff --git a/src/livekit/auth/openID.test.ts b/src/livekit/auth/openID.test.ts index 04dcb0e5e..a613d1f88 100644 --- a/src/livekit/auth/openID.test.ts +++ b/src/livekit/auth/openID.test.ts @@ -49,11 +49,12 @@ describe("getSFUConfig", () => { serviceUrl: "https://sfu.example.org", roomId: "!example_room_id", slotId: "m.call#room", - role: "subscriber", + role: "publisher", logger, }; }); - afterEach(() => { + afterEach(async () => { + void (await fetchMock.flush()); vitest.clearAllMocks(); fetchMock.reset(); }); @@ -72,7 +73,6 @@ describe("getSFUConfig", () => { livekitIdentity: "@me:example.org:ABCDEF", livekitAlias: "!example_room_id", }); - void (await fetchMock.flush()); }); it("should fail if the SFU errors", async () => { @@ -95,7 +95,6 @@ describe("getSFUConfig", () => { "MatrixError: [500] Failed to look up user info from homeserver", ); - void (await fetchMock.flush()); return; } expect.fail("Expected test to throw;"); @@ -137,7 +136,6 @@ describe("getSFUConfig", () => { expect(config.jwt).toBe(testJWTToken); expect(callCount).toBe(2); - void (await fetchMock.flush()); }); it("should successfully send delay parameters to the JWT service legacy endpoint", async () => { @@ -150,7 +148,7 @@ describe("getSFUConfig", () => { if ( body.delay_id === "mock_delay_id" && body.delay_timeout === 3600000 && - body.delay_cs_api_url === "https://homeserverserver.org/cs_api" + body.delay_cs_api_url === "https://matrix.homeserverserver.org" ) { return { status: 200, @@ -172,98 +170,6 @@ describe("getSFUConfig", () => { jwt: testJWTToken, url: serviceUrl, }); - - void (await fetchMock.flush()); - }); - - it("should try legacy and then new endpoint with delay delegation", async () => { - fetchMock.post("https://sfu.example.org/get_token", () => { - return { - status: 500, - body: { - errcode: "M_LOOKUP_FAILED", - error: "Failed to look up user info from homeserver", - }, - }; - }); - fetchMock.post("https://sfu.example.org/sfu/get", () => { - return { - status: 500, - body: { - errcode: "M_LOOKUP_FAILED", - error: "Failed to look up user info from homeserver", - }, - }; - }); - try { - await getSFUConfig({ ...params, ...delayParams }); - } catch (ex) { - expect(ex).toBeInstanceOf(FailToGetOpenIdToken); - expect((ex as FailToGetOpenIdToken).cause).toBeInstanceOf(MatrixError); - const mxError = (ex as Error).cause as MatrixError; - expect(mxError.message).toEqual( - "MatrixError: [500] Failed to look up user info from homeserver", - ); - void (await fetchMock.flush()); - } - const calls = fetchMock.calls(); - expect(calls.length).toBe(2); - - expect(calls[0][0]).toStrictEqual("https://sfu.example.org/get_token"); - expect(calls[0][1]).toStrictEqual({ - // check if it uses correct delayID! - body: '{"room_id":"!example_room_id","slot_id":"m.call#ROOM","member":{"id":"@alice:example.org:DEVICE","claimed_user_id":"@alice:example.org","claimed_device_id":"DEVICE"},"delay_id":"mock_delay_id","delay_timeout":3600000,"delay_cs_api_url":"https://matrix.homeserverserver.org"}', - method: "POST", - headers: { - "Content-Type": "application/json", - }, - }); - - expect(calls[1][0]).toStrictEqual("https://sfu.example.org/sfu/get"); - - expect(calls[1][1]).toStrictEqual({ - body: '{"room":"!example_room_id","device_id":"DEVICE","delay_id":"mock_delay_id","delay_timeout":3600000,"delay_cs_api_url":"https://matrix.homeserverserver.org"}', - headers: { - "Content-Type": "application/json", - }, - method: "POST", - }); - }); - - it("dont try legacy if endpoint with delay delegation is sucessful", async () => { - fetchMock.post("https://sfu.example.org/get_token", () => { - return { - status: 200, - body: { url: serviceUrl, jwt: testJWTToken }, - }; - }); - fetchMock.post("https://sfu.example.org/sfu/get", () => { - return { - status: 500, - body: { error: "Test failure" }, - }; - }); - try { - await getSFUConfig({ ...params, ...delayParams }); - } catch (ex) { - expect(ex).toBeInstanceOf(FailToGetOpenIdToken); - expect((ex as FailToGetOpenIdToken).cause).toEqual( - new Error("SFU Config fetch failed with status code 500"), - ); - void (await fetchMock.flush()); - } - const calls = fetchMock.calls(); - expect(calls.length).toBe(1); - - expect(calls[0][0]).toStrictEqual("https://sfu.example.org/get_token"); - expect(calls[0][1]).toStrictEqual({ - // check if it uses correct delayID! - body: '{"room_id":"!example_room_id","slot_id":"m.call#ROOM","member":{"id":"@alice:example.org:DEVICE","claimed_user_id":"@alice:example.org","claimed_device_id":"DEVICE"},"delay_id":"mock_delay_id","delay_timeout":3600000,"delay_cs_api_url":"https://matrix.homeserverserver.org"}', - method: "POST", - headers: { - "Content-Type": "application/json", - }, - }); }); it("should retry fetching the openid token", async () => { @@ -293,6 +199,5 @@ describe("getSFUConfig", () => { livekitIdentity: "@me:example.org:ABCDEF", livekitAlias: "!example_room_id", }); - void (await fetchMock.flush()); }); }); diff --git a/src/settings/DeveloperSettingsTab.test.tsx b/src/settings/DeveloperSettingsTab.test.tsx index b8cb4aeac..82764d8ec 100644 --- a/src/settings/DeveloperSettingsTab.test.tsx +++ b/src/settings/DeveloperSettingsTab.test.tsx @@ -148,6 +148,7 @@ describe("DeveloperSettingsTab", () => { getCrypto: () => ({ getVersion: (): string => "x" }), getUserId: () => "@u:hs", getDeviceId: () => "DEVICE", + getDomain: () => "hs", } as unknown as MatrixClient; it("will not update custom transport without roomId", async () => { const user = userEvent.setup(); diff --git a/src/state/CallViewModel/CallViewModel.ts b/src/state/CallViewModel/CallViewModel.ts index 811fa6e9e..73be61347 100644 --- a/src/state/CallViewModel/CallViewModel.ts +++ b/src/state/CallViewModel/CallViewModel.ts @@ -742,9 +742,12 @@ export function createCallViewModel$( return combineLatest( connections.map((connection) => remoteMatrixLivekitMembers$.pipe( - switchMap((members) => - combineLatest(members.value.map((m) => m.participant.value$)), - ), + switchMap((members) => { + if (members.value.length === 0) return of([]); + return combineLatest( + members.value.map((m) => m.participant.value$), + ); + }), map((participants) => ({ url: JSON.stringify(connection.transport), // TODO livekitRoom: connection.livekitRoom, diff --git a/src/state/CallViewModel/CallViewModelTestUtils.ts b/src/state/CallViewModel/CallViewModelTestUtils.ts index 1c1be0127..0990e4e30 100644 --- a/src/state/CallViewModel/CallViewModelTestUtils.ts +++ b/src/state/CallViewModel/CallViewModelTestUtils.ts @@ -122,6 +122,8 @@ export function withCallViewModel(mode: MatrixRTCMode) { }; const room = mockMatrixRoom({ client: new (class extends EventEmitter { + public readonly baseUrl = "https://matrix.example.com"; + public getUserId(): string | undefined { return localRtcMember.userId; } diff --git a/src/state/CallViewModel/localMember/LocalMember.test.ts b/src/state/CallViewModel/localMember/LocalMember.test.ts index c34b998cf..07ca40a01 100644 --- a/src/state/CallViewModel/localMember/LocalMember.test.ts +++ b/src/state/CallViewModel/localMember/LocalMember.test.ts @@ -337,6 +337,7 @@ describe("LocalMembership", () => { }), state$: constant(ConnectionState.LivekitConnected), transport: mockTransport, + serverName: "example.org", } as Connection; const connectionTransportAConnecting = { ...connectionTransportAConnected, @@ -858,6 +859,7 @@ describe("LocalMembership", () => { const connection = { state$: constant(ConnectionState.LivekitConnected), transport: mockTransport, + serverName: "example.org", livekitRoom: mockLivekitRoom({ localParticipant: mockLocalParticipant({ isScreenShareEnabled: false, diff --git a/src/state/CallViewModel/localMember/LocalTransport.test.ts b/src/state/CallViewModel/localMember/LocalTransport.test.ts index 1b6c55828..ed6654e2f 100644 --- a/src/state/CallViewModel/localMember/LocalTransport.test.ts +++ b/src/state/CallViewModel/localMember/LocalTransport.test.ts @@ -46,8 +46,11 @@ describe("LocalTransport", () => { getDomain: () => "example.org", }), ).toStrictEqual({ - livekit_service_url: "https://lk.example.org", - type: "livekit", + transport: { + livekit_service_url: "https://lk.example.org", + type: "livekit", + }, + serverName: "example.org", }); }); @@ -74,8 +77,11 @@ describe("LocalTransport", () => { }); expect(await getLocalTransport(client)).toStrictEqual({ - livekit_service_url: "https://lk.example.org", - type: "livekit", + transport: { + livekit_service_url: "https://lk.example.org", + type: "livekit", + }, + serverName: "example.org", }); }); @@ -86,8 +92,11 @@ describe("LocalTransport", () => { }); expect(await getLocalTransport(client)).toStrictEqual({ - livekit_service_url: "https://lk.example.org", - type: "livekit", + transport: { + livekit_service_url: "https://lk.example.org", + type: "livekit", + }, + serverName: "example.org", }); }); @@ -97,8 +106,11 @@ describe("LocalTransport", () => { ]); expect(await getLocalTransport(client)).toStrictEqual({ - livekit_service_url: "https://lk.example.org", - type: "livekit", + transport: { + livekit_service_url: "https://lk.example.org", + type: "livekit", + }, + serverName: "example.org", }); }); diff --git a/src/state/CallViewModel/remoteMembers/Connection.test.ts b/src/state/CallViewModel/remoteMembers/Connection.test.ts index 088ab310d..c0d448fbb 100644 --- a/src/state/CallViewModel/remoteMembers/Connection.test.ts +++ b/src/state/CallViewModel/remoteMembers/Connection.test.ts @@ -28,9 +28,12 @@ import { } from "livekit-client"; import fetchMock from "fetch-mock"; import EventEmitter from "events"; -import { MatrixError, type IOpenIDToken } from "matrix-js-sdk"; +import { MatrixError } from "matrix-js-sdk"; import { logger, type Logger } from "matrix-js-sdk/lib/logger"; -import { type UnstableLivekitTransport } from "matrix-js-sdk/lib/matrixrtc"; +import { + type LivekitGetTokenResponse, + type UnstableLivekitTransport, +} from "matrix-js-sdk/lib/matrixrtc"; import { Connection, @@ -185,7 +188,7 @@ describe("Start connection states", () => { expect(connection.state$.getValue()).toEqual("Initialized"); }); - it("fail to getOpenId token then error state", async () => { + it("fail to get SFU token then error state", async () => { setupTest(); vi.useFakeTimers(); @@ -209,10 +212,10 @@ describe("Start connection states", () => { }); onTestFinished(() => s.unsubscribe()); - const deferred = Promise.withResolvers(); + const deferred = Promise.withResolvers(); - client.getOpenIdToken.mockImplementation( - async (): Promise => { + client._unstable_getLivekitToken.mockImplementation( + async (): Promise => { return await deferred.promise; }, ); diff --git a/src/state/CallViewModel/remoteMembers/ConnectionManager.test.ts b/src/state/CallViewModel/remoteMembers/ConnectionManager.test.ts index f4e2a3d64..a9fc21f4f 100644 --- a/src/state/CallViewModel/remoteMembers/ConnectionManager.test.ts +++ b/src/state/CallViewModel/remoteMembers/ConnectionManager.test.ts @@ -54,9 +54,15 @@ beforeEach(() => { vi.mocked(fakeConnectionFactory).createConnection = vi .fn() .mockImplementation( - (scope: ObservableScope, transport: UnstableLivekitTransport) => { + ( + scope: ObservableScope, + _role: unknown, + transport: UnstableLivekitTransport, + serverName: string, + ) => { const mockConnection = { transport, + serverName, remoteParticipants$: new BehaviorSubject([]), } as unknown as Connection; vi.mocked(mockConnection).start = vi.fn(); @@ -229,14 +235,14 @@ describe("ConnectionManagerData", () => { test("warns when a second connection to the same URL is merged", () => { const warn = vi.spyOn(logger, "warn").mockImplementation(() => {}); const data = new ConnectionManagerData(logger); - const connection = { transport: TRANSPORT_1 } as unknown as Connection; + const connection = TRANSPORT_1 as unknown as Connection; const p = (identity: string): RemoteParticipant => ({ identity }) as unknown as RemoteParticipant; data.add(connection, [p("a")]); data.add({ ...connection } as Connection, [p("b")]); expect(warn).toHaveBeenCalledWith( expect.stringContaining( - "second connection to https://lk.example.org: existing [a], adding [b]", + 'second connection to [null,"https://lk.example.org","example.org"]: existing [a], adding [b]', ), ); expect(data.getParticipantsForTransport(TRANSPORT_1)).toHaveLength(2); @@ -247,12 +253,14 @@ describe("connectionManagerData$ stream", () => { // Used in test to control fake connections' remoteParticipants$ streams let fakeRemoteParticipantsStreams: Map>; - function keyForTransport(transport: UnstableLivekitTransport): string { + function keyForTransport({ + transport, + serverName, + }: TransportLocator): string { return JSON.stringify([ - "url" in transport ? transport.url : undefined, - "livekit_service_url" in transport - ? transport.livekit_service_url - : undefined, + "url" in transport ? transport.url : null, + "livekit_service_url" in transport ? transport.livekit_service_url : null, + serverName, ]); } @@ -260,7 +268,7 @@ describe("connectionManagerData$ stream", () => { fakeRemoteParticipantsStreams = new Map(); function getRemoteParticipantsFor( - transport: UnstableLivekitTransport, + transport: TransportLocator, ): Behavior { return ( fakeRemoteParticipantsStreams.get(keyForTransport(transport)) ?? @@ -272,13 +280,22 @@ describe("connectionManagerData$ stream", () => { vi.mocked(fakeConnectionFactory).createConnection = vi .fn() .mockImplementation( - (scope: ObservableScope, transport: UnstableLivekitTransport) => { + ( + scope: ObservableScope, + _role: unknown, + transport: UnstableLivekitTransport, + serverName: string, + ) => { const fakeRemoteParticipants$ = new BehaviorSubject< RemoteParticipant[] >([]); const mockConnection = { transport, - remoteParticipants$: getRemoteParticipantsFor(transport), + serverName, + remoteParticipants$: getRemoteParticipantsFor({ + transport, + serverName, + }), } as unknown as Connection; vi.mocked(mockConnection).start = vi.fn(); vi.mocked(mockConnection).stop = vi.fn(); @@ -288,7 +305,7 @@ describe("connectionManagerData$ stream", () => { }); fakeRemoteParticipantsStreams.set( - keyForTransport(transport), + keyForTransport({ transport, serverName }), fakeRemoteParticipants$, ); return mockConnection; @@ -301,7 +318,7 @@ describe("connectionManagerData$ stream", () => { // Setup the fake participants streams behavior // ============================== fakeRemoteParticipantsStreams.set( - keyForTransport(TRANSPORT_1.transport), + keyForTransport(TRANSPORT_1), behavior("oa-b", { o: [], a: [{ identity: "user1A" } as RemoteParticipant], @@ -313,7 +330,7 @@ describe("connectionManagerData$ stream", () => { ); fakeRemoteParticipantsStreams.set( - keyForTransport(TRANSPORT_2.transport), + keyForTransport(TRANSPORT_2), behavior("o-a", { o: [], a: [{ identity: "user2A" } as RemoteParticipant], diff --git a/src/state/CallViewModel/remoteMembers/ConnectionManager.ts b/src/state/CallViewModel/remoteMembers/ConnectionManager.ts index 0c8fa1037..5a3f420ee 100644 --- a/src/state/CallViewModel/remoteMembers/ConnectionManager.ts +++ b/src/state/CallViewModel/remoteMembers/ConnectionManager.ts @@ -50,10 +50,8 @@ export class ConnectionManagerData { // This is enough as a key because the ConnectionManager is already scoped by room. // We also do not need to consider the slotId at this point since each `MatrixRTCSession` is already scoped by `slotDescription: {id, application}`. return JSON.stringify([ - "url" in transport ? transport.url : undefined, - "livekit_service_url" in transport - ? transport.livekit_service_url - : undefined, + "url" in transport ? transport.url : null, + "livekit_service_url" in transport ? transport.livekit_service_url : null, serverName, ]); } diff --git a/src/state/CallViewModel/remoteMembers/MatrixLivekitMembers.test.ts b/src/state/CallViewModel/remoteMembers/MatrixLivekitMembers.test.ts index 9a44aa74d..aa1188655 100644 --- a/src/state/CallViewModel/remoteMembers/MatrixLivekitMembers.test.ts +++ b/src/state/CallViewModel/remoteMembers/MatrixLivekitMembers.test.ts @@ -142,6 +142,7 @@ test("should signal participant on a connection that is publishing", async () => const connection = { transport: bobMembership.getTransport(), + serverName: "example.org", } as unknown as Connection; const dataWithPublisher = new ConnectionManagerData(); dataWithPublisher.add(connection, [ @@ -185,6 +186,7 @@ test("should signal participant on a connection that is not publishing", async ( const connection = { transport: bobMembership.getTransport(), + serverName: "example.org", } as unknown as Connection; const dataWithPublisher = new ConnectionManagerData(); dataWithPublisher.add(connection, []); @@ -226,9 +228,11 @@ describe("Publication edge case", () => { ); const connectionA = { transport: transportA, + serverName: "example.org", } as unknown as Connection; const connectionB = { transport: transportB, + serverName: "sample.com", } as unknown as Connection; connectionWithPublisher.add(connectionA, [ @@ -284,8 +288,14 @@ test("bob is publishing in the wrong connection", async () => { bobMembership.userId, bobMembership.deviceId, ); - const connectionA = { transport: transportA } as unknown as Connection; - const connectionB = { transport: transportB } as unknown as Connection; + const connectionA = { + transport: transportA, + serverName: "example.org", + } as unknown as Connection; + const connectionB = { + transport: transportB, + serverName: "sample.com", + } as unknown as Connection; // Bob is not publishing on A connectionWithPublisher.add(connectionA, []); diff --git a/src/utils/test-viewmodel.ts b/src/utils/test-viewmodel.ts index 9903b99f8..df4f21bb1 100644 --- a/src/utils/test-viewmodel.ts +++ b/src/utils/test-viewmodel.ts @@ -69,7 +69,7 @@ export function getBasicRTCSession( getUserId: () => localRtcMember.userId, getDeviceId: () => localRtcMember.deviceId, getSyncState: () => SyncState.Syncing, - getDomain: () => null, + getDomain: () => "example.com", getAccessToken: () => "fake-token", sendEvent: vitest.fn().mockResolvedValue({ event_id: "$fake:event" }), redactEvent: vitest.fn().mockResolvedValue({ event_id: "$fake:event" }),