diff --git a/src/state/CallViewModel/localMember/LocalTransport.test.ts b/src/state/CallViewModel/localMember/LocalTransport.test.ts index 09f6ecec..310fb145 100644 --- a/src/state/CallViewModel/localMember/LocalTransport.test.ts +++ b/src/state/CallViewModel/localMember/LocalTransport.test.ts @@ -37,6 +37,7 @@ import { Epoch, ObservableScope } from "../../ObservableScope"; import { MatrixRTCTransportMissingError, FailToGetOpenIdToken, + SFUTokenRefusedError, } from "../../../utils/errors"; import * as openIDSFU from "../../../livekit/openIDSFU"; import { customLivekitUrl } from "../../../settings/settings"; @@ -125,6 +126,45 @@ describe("LocalTransport", () => { expect(() => active$.value).toThrow(expectedError); }); + it("[lotus] passes SFUTokenRefusedError through untouched", async () => { + const scope = new ObservableScope(); + mockConfig({ + livekit: { livekit_service_url: "https://lk.example.org" }, + }); + const refused = new SFUTokenRefusedError("This voice channel is full."); + vi.spyOn(openIDSFU, "getSFUConfigWithOpenID").mockImplementation( + async () => { + throw refused; + }, + ); + const errors: Error[] = []; + const { active$ } = createLocalTransport$({ + scope, + roomId: "!example_room_id", + memberships$: constant(new Epoch([])), + client: { + baseUrl: "https://example.org", + getDomain: () => "example.org", + // eslint-disable-next-line @typescript-eslint/naming-convention + _unstable_getRTCTransports: async () => Promise.resolve([]), + getOpenIdToken: vi.fn(), + getDeviceId: vi.fn(), + }, + ownMembershipIdentity: ownMemberMock, + forceJwtEndpoint: JwtEndpointVersion.Legacy, + delayId$: constant("delay_id_mock"), + }); + active$.subscribe( + () => undefined, + (e) => errors.push(e), + ); + await flushPromises(); + expect(errors).toStrictEqual([refused]); + expect((errors[0] as SFUTokenRefusedError).localisedMessage).toBe( + "This voice channel is full.", + ); + }); + it("emits preferred transport after OpenID resolves", async () => { // Use config so transport discovery succeeds, but delay OpenID JWT fetch mockConfig({ diff --git a/src/state/CallViewModel/localMember/LocalTransport.ts b/src/state/CallViewModel/localMember/LocalTransport.ts index f98a266f..a987af24 100644 --- a/src/state/CallViewModel/localMember/LocalTransport.ts +++ b/src/state/CallViewModel/localMember/LocalTransport.ts @@ -26,6 +26,7 @@ import { type Epoch, type ObservableScope } from "../../ObservableScope.ts"; import { Config } from "../../../config/Config.ts"; import { FailToGetOpenIdToken, + SFUTokenRefusedError, MatrixRTCTransportMissingError, NoMatrix2AuthorizationService, } from "../../../utils/errors.ts"; @@ -261,7 +262,9 @@ async function doOpenIdAndJWTFromUrl( function mapAuthErrorToUserFriendlyError(e: unknown): Error { if ( e instanceof FailToGetOpenIdToken || - e instanceof NoMatrix2AuthorizationService + e instanceof NoMatrix2AuthorizationService || + // [lotus] carries the token service's own refusal reason — keep it. + e instanceof SFUTokenRefusedError ) { // rethrow as is return e;