diff --git a/apps/web/src/components/structures/MatrixChat.test.tsx b/apps/web/src/components/structures/MatrixChat.test.tsx index 8b3e7ef3aa1..233fc6bec25 100644 --- a/apps/web/src/components/structures/MatrixChat.test.tsx +++ b/apps/web/src/components/structures/MatrixChat.test.tsx @@ -163,6 +163,10 @@ describe("", () => { getThirdpartyProtocols: vi.fn().mockResolvedValue({}), getClientWellKnown: vi.fn().mockReturnValue({}), _unstable_getRTCTransports: vi.fn().mockResolvedValue([]), + cachedRtcTransports: { + wait: vi.fn().mockResolvedValue([]), + get: vi.fn().mockReturnValue([]), + }, waitForClientWellKnown: vi.fn().mockResolvedValue({}), isVersionSupported: vi.fn().mockResolvedValue(false), initRustCrypto: vi.fn(), diff --git a/apps/web/src/components/views/rooms/RoomHeader/RoomHeader.test.tsx b/apps/web/src/components/views/rooms/RoomHeader/RoomHeader.test.tsx index 643fc0eea22..589982dd398 100644 --- a/apps/web/src/components/views/rooms/RoomHeader/RoomHeader.test.tsx +++ b/apps/web/src/components/views/rooms/RoomHeader/RoomHeader.test.tsx @@ -123,6 +123,7 @@ describe("RoomHeader", () => { // Mock CallStore.instance.getCall to return null by default // Individual tests can override this when they need a specific Call object vi.spyOn(CallStore.instance, "getCall").mockReturnValue(null); + vi.spyOn(CallStore.instance, "getConfiguredRTCTransports").mockReturnValue([]); // Reset the mock RoomViewStore mockRoomViewStore.isViewingCall.mockReturnValue(false); @@ -419,9 +420,9 @@ describe("RoomHeader", () => { beforeEach(async () => { SdkConfig.put({}); // Enable Element Call - client._unstable_getRTCTransports = vi - .fn() - .mockResolvedValue([{ type: "livekit", livekit_service_url: "https://example.org" }]); + vi.spyOn(CallStore.instance, "getConfiguredRTCTransports").mockReturnValue([ + { type: "livekit", livekit_service_url: "https://example.org" }, + ]); // And ensure the CallStore has the transports configured. await setupAsyncStoreWithClient(CallStore.instance, client); }); @@ -810,6 +811,9 @@ describe("RoomHeader", () => { getMxcAvatarUrl: () => "mxc://avatar.url/image.png", }, ]); + vi.spyOn(CallStore.instance, "getConfiguredRTCTransports").mockReturnValue([ + { type: "livekit", livekit_service_url: "https://example.org" }, + ]); }); afterEach(() => { diff --git a/apps/web/src/hooks/useRoomCall.test.ts b/apps/web/src/hooks/useRoomCall.test.ts index 4c583c72f7b..b21f44e6249 100644 --- a/apps/web/src/hooks/useRoomCall.test.ts +++ b/apps/web/src/hooks/useRoomCall.test.ts @@ -7,7 +7,7 @@ Please see LICENSE files in the repository root for full details. // @vitest-environment happy-dom -import { vi, describe, it, expect, beforeEach, afterEach } from "vitest"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { renderHook, waitFor } from "test-utils-rtl"; import { getMockClientWithEventEmitter, @@ -25,6 +25,8 @@ import RoomContext, { type RoomContextType } from "../contexts/RoomContext"; import type LegacyCallHandler from "../LegacyCallHandler"; import { CallStore } from "../stores/CallStore"; import { SDKContextClass } from "../contexts/SDKContextClass"; +import { ClientEvent } from "matrix-js-sdk/src/matrix"; +import { act } from "react"; describe("useRoomCall", () => { const client = getMockClientWithEventEmitter({ @@ -32,9 +34,9 @@ describe("useRoomCall", () => { ...mockClientMethodsServer(), ...mockClientMethodsRooms(), matrixRTC: new MockEventEmitter(), - _unstable_getRTCTransports: vi.fn().mockResolvedValue([]), getCrypto: () => null, }); + const room = mkRoom(client, "!test-room"); // Create a stable room context for this test const mockRoomViewStore = { @@ -82,7 +84,7 @@ describe("useRoomCall", () => { await waitFor(() => expect(result.current.callOptions).toEqual([PlatformCallType.LegacyCall])); }); it("Blocks Element Call if transport foci are the wrong type", async () => { - client._unstable_getRTCTransports.mockResolvedValue([{ type: "anything-else" }]); + vi.mocked(client.cachedRtcTransports.get).mockReturnValue([{ type: "anything-else" }]); await setupAsyncStoreWithClient(CallStore.instance, client); const { result } = render(); await waitFor(() => expect(result.current.callOptions).toEqual([PlatformCallType.LegacyCall])); @@ -98,7 +100,7 @@ describe("useRoomCall", () => { await waitFor(() => expect(result.current.callOptions).toEqual([PlatformCallType.LegacyCall])); }); it("Allows Element Call if foci is provided via getRTCTransports", async () => { - client._unstable_getRTCTransports.mockResolvedValue([ + vi.mocked(client.cachedRtcTransports.get).mockReturnValue([ { type: "livekit", livekit_service_url: "https://example.org" }, ]); await setupAsyncStoreWithClient(CallStore.instance, client); @@ -108,13 +110,13 @@ describe("useRoomCall", () => { expect(result.current.callOptions).toEqual([PlatformCallType.ElementCall, PlatformCallType.LegacyCall]), ); }); - it("Allows Element Call if foci is provided via .well-known", async () => { - client.getClientWellKnown.mockReturnValue({ - "org.matrix.msc4143.rtc_foci": { + it("Allows Element Call if transport is provided by client discovery", async () => { + vi.mocked(client.cachedRtcTransports.get).mockReturnValue([ + { type: "livekit", livekit_service_url: "https://example.org", }, - }); + ]); await setupAsyncStoreWithClient(CallStore.instance, client); const { result } = render(); await waitFor(() => @@ -123,9 +125,7 @@ describe("useRoomCall", () => { }); it("Ensure handler reacts to transport changes", async () => { // Clear all transports - client._unstable_getRTCTransports.mockResolvedValue([]); - client.getClientWellKnown.mockReturnValue({}); - + vi.mocked(client.cachedRtcTransports.get).mockReturnValue([]); await setupAsyncStoreWithClient(CallStore.instance, client); const { result } = render(); @@ -133,9 +133,12 @@ describe("useRoomCall", () => { expect(result.current.callOptions).toEqual([PlatformCallType.LegacyCall]); // Now enable a transport and ensure that useRoomCall picks it up reactively. - client._unstable_getRTCTransports.mockResolvedValue([ - { type: "livekit", livekit_service_url: "https://example.org" }, - ]); + act(() => { + const transports = [{ type: "livekit", livekit_service_url: "https://example.org" }]; + vi.mocked(client.cachedRtcTransports.get).mockReturnValue(transports); + client.emit(ClientEvent.RtcTransportsUpdated, transports); + }); + await setupAsyncStoreWithClient(CallStore.instance, client); await waitFor(() => expect(result.current.callOptions).toEqual([PlatformCallType.ElementCall, PlatformCallType.LegacyCall]), diff --git a/apps/web/src/stores/CallStore.ts b/apps/web/src/stores/CallStore.ts index d5ce82665ee..83549d22781 100644 --- a/apps/web/src/stores/CallStore.ts +++ b/apps/web/src/stores/CallStore.ts @@ -8,7 +8,7 @@ Please see LICENSE files in the repository root for full details. import { logger } from "matrix-js-sdk/src/logger"; import { type MatrixRTCSession, MatrixRTCSessionManagerEvents, type Transport } from "matrix-js-sdk/src/matrixrtc"; -import { MatrixError, type EmptyObject, type Room } from "matrix-js-sdk/src/matrix"; +import { ClientEvent, type EmptyObject, type Room } from "matrix-js-sdk/src/matrix"; import defaultDispatcher from "../dispatcher/dispatcher"; import { UPDATE_EVENT } from "./AsyncStore"; @@ -16,8 +16,8 @@ import { AsyncStoreWithClient } from "./AsyncStoreWithClient"; import WidgetStore from "./WidgetStore"; import SettingsStore from "../settings/SettingsStore"; import { SettingLevel } from "../settings/SettingLevel"; -import SdkConfig from "../SdkConfig"; import { Call, CallEvent, ConnectionState } from "../models/Call"; +import SdkConfig from "../SdkConfig.ts"; export enum CallStoreEvent { // Signals a change in the call associated with a given room @@ -41,8 +41,6 @@ export class CallStore extends AsyncStoreWithClient { return this._instance; } - private readonly configuredMatrixRTCTransports = new Set(); - private constructor() { super(defaultDispatcher); this.setMaxListeners(100); // One for each RoomTile @@ -52,44 +50,15 @@ export class CallStore extends AsyncStoreWithClient { // nothing to do } - /** - * Fetch transports used by MatrixRTC services, such as Element Call. - * This function is called once during Store startup which means we don't refetch - * transports every time we need to check for Element Call support. - */ - protected async fetchTransports(): Promise { - if (!this.matrixClient) return; - this.configuredMatrixRTCTransports.clear(); - // Prefer checking the proper endpoint for transports. - try { - const transports = await this.matrixClient._unstable_getRTCTransports(); - transports.forEach((t) => this.configuredMatrixRTCTransports.add(t)); - } catch (ex) { - // Expected, MSC not implemented. - // - // Homeservers will return a 404 M_UNRECOGNIZED matrix error if they - // don't implement a requested endpoint. - if (ex instanceof MatrixError === false || ex.errcode !== "M_UNRECOGNIZED") { - logger.warn("Unexpected error when trying to fetch RTC transports", ex); - } - } - // See https://github.com/matrix-org/matrix-spec-proposals/blob/d61969a9a3696b6c54d7987b1643b5bc03670927/proposals/4143-matrix-rtc.md#discovery-of-foci-using-well-knownmatrixclient - // This well-known option has since been removed from the spec but is still widely deployed. - // Reading it can be disabled via config; the modern endpoint above is unaffected. - if (SdkConfig.get("enable_client_well_known_lookups")) { - await this.matrixClient.waitForClientWellKnown(); - const foci = this.matrixClient.getClientWellKnown()?.["org.matrix.msc4143.rtc_foci"]; - if (Array.isArray(foci)) { - foci.forEach((foci) => this.configuredMatrixRTCTransports.add(foci)); - } - } - this.emit(CallStoreEvent.TransportsUpdated); - } - protected async onReady(): Promise { if (!this.matrixClient) return; // Fetch transports, but don't await the result. - void this.fetchTransports(); + this.matrixClient.cachedRtcTransports.wait().catch(() => { + if (SdkConfig.get("enable_client_well_known_lookups")) { + void this.matrixClient?.waitForClientWellKnown(); + } + }); + // We assume that the calls present in a room are a function of room // widgets and group calls, so we initialize the room map here and then // update it whenever those change @@ -99,6 +68,8 @@ export class CallStore extends AsyncStoreWithClient { this.matrixClient.matrixRTC.on(MatrixRTCSessionManagerEvents.SessionStarted, this.onRTCSessionStart); WidgetStore.instance.on(UPDATE_EVENT, this.onWidgets); + this.matrixClient.on(ClientEvent.RtcTransportsUpdated, this.onRTCTransportsUpdated); + // If the room ID of a previously connected call is still in settings at // this time, that's a sign that we failed to disconnect from it // properly, and need to clean up after ourselves @@ -125,9 +96,9 @@ export class CallStore extends AsyncStoreWithClient { this.callListeners.clear(); this.calls.clear(); this._connectedCalls.clear(); - this.configuredMatrixRTCTransports.clear(); this.matrixClient?.matrixRTC.off(MatrixRTCSessionManagerEvents.SessionStarted, this.onRTCSessionStart); + this.matrixClient?.off(ClientEvent.RtcTransportsUpdated, this.onRTCTransportsUpdated); WidgetStore.instance.off(UPDATE_EVENT, this.onWidgets); } @@ -138,6 +109,7 @@ export class CallStore extends AsyncStoreWithClient { public get connectedCalls(): Set { return this._connectedCalls; } + private set connectedCalls(value: Set) { const prevValue = this._connectedCalls; this._connectedCalls = value; @@ -156,6 +128,7 @@ export class CallStore extends AsyncStoreWithClient { private callListeners = new Map unknown>>(); private inUpdateRoom = false; + private updateRoom(room: Room): void { // XXX: This method is guarded with the flag this.inUpdateRoom because // we need to block this method from calling itself recursively. That @@ -242,10 +215,26 @@ export class CallStore extends AsyncStoreWithClient { }; public getConfiguredRTCTransports(): Transport[] { - return [...this.configuredMatrixRTCTransports]; + const rtcTransports = this.matrixClient?.cachedRtcTransports.get(); + const enableClientWellKnownLookups = SdkConfig.get("enable_client_well_known_lookups"); + if (rtcTransports || !enableClientWellKnownLookups) { + return rtcTransports ?? []; + } + const wellKnown = this.matrixClient?.getClientWellKnown(); + const foci = wellKnown?.["org.matrix.msc4143.rtc_foci"]; + if (foci !== undefined) { + if (Array.isArray(foci)) + return foci; // Contents assumed to be valid Transports + else logger.warn(`org.matrix.msc4143.rtc_foci is not an array in .well-known`); + } + return []; } private onRTCSessionStart = (roomId: string, session: MatrixRTCSession): void => { this.updateRoom(session.room); }; + + private onRTCTransportsUpdated = (transports: Transport[]): void => { + this.emit(CallStoreEvent.TransportsUpdated, transports); + }; } diff --git a/apps/web/src/stores/widgets/ElementWidgetDriver.ts b/apps/web/src/stores/widgets/ElementWidgetDriver.ts index 6de3a30aadf..ed657a851ff 100644 --- a/apps/web/src/stores/widgets/ElementWidgetDriver.ts +++ b/apps/web/src/stores/widgets/ElementWidgetDriver.ts @@ -68,6 +68,7 @@ import { ModuleApi } from "../../modules/Api"; import { toWidgetDescriptor } from "../../modules/WidgetLifecycleApi"; import SettingsStore from "../../settings/SettingsStore"; import { mediaFromMxc } from "../../customisations/Media"; +import SdkConfig from "../../SdkConfig.ts"; function getRememberedCapabilitiesForWidget(widget: Widget): Capability[] { return JSON.parse(localStorage.getItem(`widget_${widget.id}_approved_caps`) || "[]"); @@ -748,12 +749,28 @@ export class ElementWidgetDriver extends WidgetDriver { public async getRtcTransports(): Promise { const client = MatrixClientPeg.safeGet(); - // Delegate to the authenticated CS endpoint (MSC4143). Any error (e.g. the - // homeserver not supporting it) propagates and is turned into a widget error - // response by ClientWidgetApi. The js-sdk Transport and widget-api IRtcTransport - // types are structurally identical. - const transports = await client._unstable_getRTCTransports(); - return { rtc_transports: transports }; + try { + // Delegate to the authenticated CS endpoint (MSC4519). The js-sdk Transport and + // widget-api IRtcTransport types are structurally identical. + const transports = await client.cachedRtcTransports.wait(); + return { rtc_transports: transports ?? [] }; + } catch (e) { + // If the homeserver does not support the API, fall back to legacy well-known lookup. + if ( + e instanceof MatrixError && + e.errcode === "M_NOT_FOUND" && + SdkConfig.get("enable_client_well_known_lookups") + ) { + const wellKnown = await client.waitForClientWellKnown(); + const foci = wellKnown?.["org.matrix.msc4143.rtc_foci"]; + if (foci !== undefined) { + if (Array.isArray(foci)) return { rtc_transports: foci }; + else logger.warn(`org.matrix.msc4143.rtc_foci is not an array in .well-known`); + } + } + // Re-throw to turn the error into a widget error response + throw e; + } } public async readEventRelations( diff --git a/apps/web/test/test-utils/call.ts b/apps/web/test/test-utils/call.ts index 69b3a328402..12dbd1b47d1 100644 --- a/apps/web/test/test-utils/call.ts +++ b/apps/web/test/test-utils/call.ts @@ -155,7 +155,10 @@ export function setUpClientRoomAndStores(): { stubClient(); const client = mocked(MatrixClientPeg.safeGet()); DMRoomMap.makeShared(client); - + client.cachedRtcTransports = { + wait: jest.fn().mockResolvedValue(undefined), + get: jest.fn().mockReturnValue(undefined), + } as unknown as any; const room = new Room("!1:example.org", client, "@alice:example.org", { pendingEventOrdering: PendingEventOrdering.Detached, }); diff --git a/apps/web/test/test-utils/client.ts b/apps/web/test/test-utils/client.ts index 91c3e56238d..c799eb2244b 100644 --- a/apps/web/test/test-utils/client.ts +++ b/apps/web/test/test-utils/client.ts @@ -134,13 +134,19 @@ export const mockClientPushProcessor = () => /** * Returns basic mocked client methods related to server support */ -export const mockClientMethodsServer = (): Partial, unknown>> => ({ +export const mockClientMethodsServer = (): Partial< + Record | PropertyLikeKeys, unknown> +> => ({ getIdentityServerUrl: vi.fn(), getHomeserverUrl: vi.fn(), getCapabilities: vi.fn().mockResolvedValue({}), getCachedCapabilities: vi.fn().mockResolvedValue({}), getClientWellKnown: vi.fn().mockReturnValue({}), waitForClientWellKnown: vi.fn().mockResolvedValue({}), + cachedRtcTransports: { + wait: vi.fn().mockResolvedValue([]), + get: vi.fn().mockReturnValue([]), + }, doesServerSupportUnstableFeature: vi.fn().mockResolvedValue(false), isVersionSupported: vi.fn().mockResolvedValue(false), getVersions: vi.fn().mockResolvedValue({}), diff --git a/apps/web/test/test-utils/test-utils.ts b/apps/web/test/test-utils/test-utils.ts index 5a3261ccc5d..b74eb077cf6 100644 --- a/apps/web/test/test-utils/test-utils.ts +++ b/apps/web/test/test-utils/test-utils.ts @@ -321,6 +321,10 @@ export function createTestClient(): MatrixClient { _unstable_sendStickyEvent: vi.fn(), _unstable_sendStickyDelayedEvent: vi.fn(), _unstable_getRTCTransports: vi.fn(), + cachedRtcTransports: { + wait: vi.fn().mockResolvedValue([]), + get: vi.fn().mockReturnValue([]), + } as unknown as MockedObject, searchUserDirectory: vi.fn().mockResolvedValue({ limited: false, results: [] }), setDeviceVerified: vi.fn(), joinRoom: vi.fn(), diff --git a/apps/web/test/unit-tests/stores/CallStore-test.ts b/apps/web/test/unit-tests/stores/CallStore-test.ts index 61bdc44f37d..18ef431193f 100644 --- a/apps/web/test/unit-tests/stores/CallStore-test.ts +++ b/apps/web/test/unit-tests/stores/CallStore-test.ts @@ -11,13 +11,13 @@ import { type MockedObject } from "jest-mock"; import { ElementCall } from "../../../src/models/Call"; import { CallStore } from "../../../src/stores/CallStore"; -import SdkConfig from "../../../src/SdkConfig"; import { setUpClientRoomAndStores, cleanUpClientRoomAndStores, setupAsyncStoreWithClient, enableCalls, } from "../../test-utils"; +import SdkConfig from "../../../src/SdkConfig.ts"; describe("CallStore", () => { let client: MockedObject; @@ -49,8 +49,8 @@ describe("CallStore", () => { expect(CallStore.instance.getCall(room.roomId)).not.toBe(null); expect(CallStore.instance.getConfiguredRTCTransports()).toHaveLength(0); }); - it("calculates RTC transports with both modern and legacy endpoints", async () => { - client._unstable_getRTCTransports.mockResolvedValue([ + it("delegates transport discovery to the client", async () => { + client.cachedRtcTransports.get.mockReturnValue([ { type: "type-a", some_data: "value" }, { type: "type-b", some_data: "foo" }, ]); @@ -64,17 +64,16 @@ describe("CallStore", () => { expect(CallStore.instance.getConfiguredRTCTransports()).toEqual([ { type: "type-a", some_data: "value" }, { type: "type-b", some_data: "foo" }, - { type: "type-c", other_data: "bar" }, - { type: "type-d", other_data: "baz" }, ]); }); + it("does not fall back to client well-known when enable_client_well_known_lookups is false", async () => { const sdkConfigGet = SdkConfig.get; jest.spyOn(SdkConfig, "get").mockImplementation((key?: any, altCaseName?: string): any => { if (key === "enable_client_well_known_lookups") return false; return sdkConfigGet(key, altCaseName); }); - client._unstable_getRTCTransports.mockResolvedValue([{ type: "type-a", some_data: "value" }]); + client.cachedRtcTransports.get.mockReturnValue([{ type: "type-a", some_data: "value" }]); client.getClientWellKnown.mockReturnValue({ "org.matrix.msc4143.rtc_foci": [{ type: "type-c", other_data: "bar" }], }); diff --git a/apps/web/test/unit-tests/stores/RoomViewStore-test.ts b/apps/web/test/unit-tests/stores/RoomViewStore-test.ts index 3b1cca51cc7..0f5618447a1 100644 --- a/apps/web/test/unit-tests/stores/RoomViewStore-test.ts +++ b/apps/web/test/unit-tests/stores/RoomViewStore-test.ts @@ -140,6 +140,10 @@ describe("RoomViewStore", function () { getAccountData: jest.fn(), waitForClientWellKnown: jest.fn().mockResolvedValue(undefined), getClientWellKnown: jest.fn().mockReturnValue({}), + cachedRtcTransports: { + get: jest.fn().mockReturnValue([]), + wait: jest.fn().mockResolvedValue([]), + }, matrixRTC: new (class extends EventEmitter { getRoomSession() { return new (class extends EventEmitter { diff --git a/apps/web/test/unit-tests/stores/widgets/ElementWidgetDriver-test.ts b/apps/web/test/unit-tests/stores/widgets/ElementWidgetDriver-test.ts index 2824dbc9c49..0af754b158a 100644 --- a/apps/web/test/unit-tests/stores/widgets/ElementWidgetDriver-test.ts +++ b/apps/web/test/unit-tests/stores/widgets/ElementWidgetDriver-test.ts @@ -45,6 +45,7 @@ import dis from "../../../../src/dispatcher/dispatcher"; import Modal from "../../../../src/Modal"; import SettingsStore from "../../../../src/settings/SettingsStore"; import { WidgetType } from "../../../../src/widgets/WidgetType.ts"; +import SdkConfig from "../../../../src/SdkConfig.ts"; describe("ElementWidgetDriver", () => { let client: MockedObject; @@ -69,6 +70,9 @@ describe("ElementWidgetDriver", () => { beforeEach(() => { stubClient(); client = mocked(MatrixClientPeg.safeGet()); + client.cachedRtcTransports = { + wait: jest.fn(), + } as any; client.getUserId.mockReturnValue("@alice:example.org"); client.getSafeUserId.mockReturnValue("@alice:example.org"); }); @@ -431,20 +435,75 @@ describe("ElementWidgetDriver", () => { it("gets the RTC transports from the homeserver", async () => { const transports = [{ type: "livekit", livekit_service_url: "https://livekit-jwt.example.com" }]; - client._unstable_getRTCTransports.mockResolvedValue(transports); + client.cachedRtcTransports.wait.mockResolvedValue(transports); await expect(driver.getRtcTransports()).resolves.toEqual({ rtc_transports: transports }); - - expect(client._unstable_getRTCTransports).toHaveBeenCalledWith(); }); it("propagates errors from the homeserver", async () => { - const error = new MatrixError({ errcode: "M_NOT_FOUND", error: "Not found" }, 404); + const error = new MatrixError( + { + errcode: "M_LIMIT_EXCEEDED", + error: "Too many requests", + retry_after_ms: 1_000, + }, + 429, + ); - client._unstable_getRTCTransports.mockRejectedValue(error); + client.cachedRtcTransports.wait.mockRejectedValue(error); await expect(driver.getRtcTransports()).rejects.toBe(error); }); + + it("Should not fallback to well-known if config disallows and transport discovery not available", async () => { + const sdkConfigGet = SdkConfig.get; + jest.spyOn(SdkConfig, "get").mockImplementation((key?: any, altCaseName?: string): any => { + if (key === "enable_client_well_known_lookups") return false; + return sdkConfigGet(key, altCaseName); + }); + client.cachedRtcTransports.wait.mockRejectedValue( + new MatrixError({ errcode: "M_NOT_FOUND", error: "Not found" }, 404), + ); + + await expect(driver.getRtcTransports()).rejects.toThrow(); + + expect(client.waitForClientWellKnown).not.toHaveBeenCalled(); + expect(client.getClientWellKnown).not.toHaveBeenCalled(); + }); + + it("Should not fallback to well-known if config disallows and homerserver advertise no transports", async () => { + const sdkConfigGet = SdkConfig.get; + jest.spyOn(SdkConfig, "get").mockImplementation((key?: any, altCaseName?: string): any => { + if (key === "enable_client_well_known_lookups") return false; + return sdkConfigGet(key, altCaseName); + }); + client.cachedRtcTransports.wait.mockResolvedValue([]); + + await expect(driver.getRtcTransports()).resolves.toEqual({ rtc_transports: [] }); + + expect(client.waitForClientWellKnown).not.toHaveBeenCalled(); + expect(client.getClientWellKnown).not.toHaveBeenCalled(); + }); + + it("Should fallback to well-known if config allows", async () => { + const sdkConfigGet = SdkConfig.get; + jest.spyOn(SdkConfig, "get").mockImplementation((key?: any, altCaseName?: string): any => { + if (key === "enable_client_well_known_lookups") return true; + return sdkConfigGet(key, altCaseName); + }); + client.cachedRtcTransports.wait.mockRejectedValue( + new MatrixError({ errcode: "M_NOT_FOUND", error: "Not found" }, 404), + ); + + const transports = [{ type: "livekit", livekit_service_url: "https://livekit-jwt.example.com" }]; + client.waitForClientWellKnown.mockResolvedValue({ + "org.matrix.msc4143.rtc_foci": transports, + }); + + await expect(driver.getRtcTransports()).resolves.toEqual({ rtc_transports: transports }); + + expect(client.waitForClientWellKnown).toHaveBeenCalled(); + }); }); describe("readEventRelations", () => {