Add error tests

This commit is contained in:
Johannes Marbach
2026-10-08 08:26:03 +02:00
parent 4010411b81
commit f86c3f7114
+148 -38
View File
@@ -32,29 +32,48 @@ type Shape = keyof typeof shapes;
/** Which endpoint a page asks for its SFU tokens. */ /** Which endpoint a page asks for its SFU tokens. */
type Flow = "cs-api" | "service-legacy" | "service-default"; type Flow = "cs-api" | "service-legacy" | "service-default";
const pairs: [host: Shape, guest: Shape][] = [ const cases: {
["url", "url"], hostShape: Shape;
["both", "both"], guestShape: Shape;
["url", "legacy"], hostFlows: Flow[];
guestFlows: Flow[];
}[] = [
{
hostShape: "url",
guestShape: "url",
hostFlows: ["cs-api"],
guestFlows: ["cs-api"],
},
{
hostShape: "both",
guestShape: "both",
hostFlows: ["cs-api"],
guestFlows: ["cs-api"],
},
{
hostShape: "url",
guestShape: "legacy",
hostFlows: ["cs-api", "service-default"],
guestFlows: ["service-legacy", "cs-api"],
},
]; ];
for (const [hostShape, guestShape] of pairs) { for (const { hostShape, guestShape, hostFlows, guestFlows } of cases) {
test(`One to one call with ${hostShape} host and ${guestShape} guest transport`, async ({ test(`One to one call with ${hostShape} host and ${guestShape} guest transport`, async ({
browser, browser,
page, page,
browserName, browserName,
}) => { }) => {
test.skip( skipOnFirefox(browserName);
browserName === "firefox",
"The is test is not working on firefox CI environment. No mic/audio device inputs so cam/mic are disabled",
);
// The host starts the call.
await advertiseTransport(page, shapes[hostShape]); await advertiseTransport(page, shapes[hostShape]);
const hostRequests = recordAuthRequests(page); const hostRequests = recordAuthRequests(page);
await page.goto("/"); await page.goto("/");
await SpaHelpers.createCall(page, "Androl", "HelloCall", true, "2_0"); await SpaHelpers.createCall(page, "Androl", "HelloCall", true, "2_0");
const inviteLink = await SpaHelpers.getCallInviteLink(page); const inviteLink = await SpaHelpers.getCallInviteLink(page);
// The guest joins.
const guestContext = await browser.newContext({ reducedMotion: "reduce" }); const guestContext = await browser.newContext({ reducedMotion: "reduce" });
const guestPage = await guestContext.newPage(); const guestPage = await guestContext.newPage();
await advertiseTransport(guestPage, shapes[guestShape]); await advertiseTransport(guestPage, shapes[guestShape]);
@@ -66,33 +85,104 @@ for (const [hostShape, guestShape] of pairs) {
"2_0", "2_0",
); );
// Wait for the call to connect and render.
await SpaHelpers.expectVideoTilesCount(page, 2); await SpaHelpers.expectVideoTilesCount(page, 2);
await SpaHelpers.expectVideoTilesCount(guestPage, 2); await SpaHelpers.expectVideoTilesCount(guestPage, 2);
expect(hostRequests.flows).toEqual(expectedFlows(hostShape, guestShape)); // The host and guest ran through the expected flows.
expect(guestRequests.flows).toEqual(expectedFlows(guestShape, hostShape)); expect(flowsOf(hostRequests)).toEqual(new Set(hostFlows));
for (const requests of [hostRequests, guestRequests]) { expect(flowsOf(guestRequests)).toEqual(new Set(guestFlows));
for (const url of requests.csApiUrls) expect(url).toBe(sfuUrl);
// All requests succeeded and the SFU URL was set on C-S requests.
for (const request of [...hostRequests, ...guestRequests]) {
expect(request.status).toBe(200);
if (request.flow === "cs-api") expect(request.sfuUrl).toBe(sfuUrl);
} }
}); });
} }
test("Subscriber falls back to the JWT service when the homeserver lacks MSC4195", async ({
browser,
page,
browserName,
}) => {
skipOnFirefox(browserName);
// The host starts the call with both transport shapes.
await advertiseTransport(page, shapes.both);
await page.goto("/");
await SpaHelpers.createCall(page, "Androl", "HelloCall", true, "2_0");
const inviteLink = await SpaHelpers.getCallInviteLink(page);
// The guest joins but their C-S requests fail.
const guestContext = await browser.newContext({ reducedMotion: "reduce" });
const guestPage = await guestContext.newPage();
await rejectCsApiTokens(guestPage);
const guestRequests = recordAuthRequests(guestPage);
await SpaHelpers.joinCallFromInviteLink(
guestPage,
inviteLink,
"Pevara",
"2_0",
);
// Wait for the call to connect and render.
await SpaHelpers.expectVideoTilesCount(page, 2);
await SpaHelpers.expectVideoTilesCount(guestPage, 2);
// The guest ran through all flows.
expect(flowsOf(guestRequests)).toEqual(
new Set<Flow>(["cs-api", "service-legacy", "service-default"]),
);
// The C-S token requests failed.
for (const request of guestRequests) {
expect(request.status).toBe(request.flow === "cs-api" ? 404 : 200);
}
// The C-S token requests preceeded the fallback request.
const flows = guestRequests.map((r) => r.flow);
expect(flows.indexOf("service-default")).toBeGreaterThan(
flows.indexOf("cs-api"),
);
});
test("Publisher shows an error when the homeserver lacks MSC4195", async ({
page,
browserName,
}) => {
skipOnFirefox(browserName);
// The host tries to start a call with only the C-S transport shape available but
// their C-S requests fail.
await advertiseTransport(page, shapes.url);
await rejectCsApiTokens(page);
await page.goto("/");
await SpaHelpers.createCall(page, "Androl", "HelloCall", true, "2_0");
// We land on an error page.
await expect(
page.getByRole("heading", { name: "Something went wrong" }),
).toBeVisible();
await expect(
page.getByText(
"The authorization service for your media server (SFU) is out of date.",
),
).toBeVisible();
});
function skipOnFirefox(browserName: string): void {
test.skip(
browserName === "firefox",
"The is test is not working on firefox CI environment. No mic/audio device inputs so cam/mic are disabled",
);
}
/** /**
* A page publishes on its own transport and subscribes on every other * Maps recorded requests to a set of flows. Publisher and subscriber connections
* transport in the call, so the flows it uses depend on both shapes. * start concurrently so using arrays for comparison isn't convenient.
*/ */
function expectedFlows(own: Shape, peer: Shape): Set<Flow> { function flowsOf(requests: AuthRequest[]): Set<Flow> {
const flows = new Set<Flow>([publishFlow(own)]); return new Set(requests.map((r) => r.flow));
if (peer !== own) flows.add(subscribeFlow(peer));
return flows;
}
function publishFlow(shape: Shape): Flow {
return shape === "legacy" ? "service-legacy" : "cs-api";
}
function subscribeFlow(shape: Shape): Flow {
return shape === "legacy" ? "service-default" : "cs-api";
} }
/** /**
@@ -111,24 +201,44 @@ async function advertiseTransport(
}); });
} }
interface AuthRequests { /**
flows: Set<Flow>; * Makes the homeserver look like one without MSC4195: its `get_token`
/** The `url` field of every request to the MSC4195 `get_token` endpoint. */ * endpoint answers as Synapse does for an unknown route.
csApiUrls: string[]; */
async function rejectCsApiTokens(page: Page): Promise<void> {
await page.route("**/rtc/livekit/get_token", async (route) => {
if (route.request().method() !== "POST") return route.continue();
await route.fulfill({
status: 404,
json: { errcode: "M_UNRECOGNIZED", error: "Unrecognized request" },
headers: { "Access-Control-Allow-Origin": "*" },
});
});
} }
function recordAuthRequests(page: Page): AuthRequests { interface AuthRequest {
const requests: AuthRequests = { flows: new Set(), csApiUrls: [] }; flow: Flow;
page.on("request", (request) => { status: number;
/** The SFU a request to the MSC4195 `get_token` endpoint asked for. */
sfuUrl?: string;
}
function recordAuthRequests(page: Page): AuthRequest[] {
const requests: AuthRequest[] = [];
page.on("response", (response) => {
const request = response.request();
if (request.method() !== "POST") return; if (request.method() !== "POST") return;
const url = request.url(); const url = request.url();
if (url.endsWith("/rtc/livekit/get_token")) { if (url.endsWith("/rtc/livekit/get_token")) {
requests.flows.add("cs-api"); requests.push({
requests.csApiUrls.push((request.postDataJSON() as { url: string }).url); flow: "cs-api",
status: response.status(),
sfuUrl: (request.postDataJSON() as { url: string }).url,
});
} else if (url.endsWith("/livekit/jwt/sfu/get")) { } else if (url.endsWith("/livekit/jwt/sfu/get")) {
requests.flows.add("service-legacy"); requests.push({ flow: "service-legacy", status: response.status() });
} else if (url.endsWith("/livekit/jwt/get_token")) { } else if (url.endsWith("/livekit/jwt/get_token")) {
requests.flows.add("service-default"); requests.push({ flow: "service-default", status: response.status() });
} }
}); });
return requests; return requests;