Add delegation test

This commit is contained in:
Johannes Marbach
2026-10-08 08:45:07 +02:00
parent f86c3f7114
commit f5d3ee0891
3 changed files with 111 additions and 2 deletions
+1 -1
View File
@@ -5,7 +5,7 @@ sender_localpart: "_lk_jwt_service"
namespaces: namespaces:
users: users:
- exclusive: false - exclusive: false
regex: "@.*:homeserver-1" # Cover all local users regex: "@.*:synapse\\.othersite\\.m\\.localhost" # Cover all local users
# Required for Synapse to call POST /_matrix/app/v1/ping on this service. # Required for Synapse to call POST /_matrix/app/v1/ping on this service.
url: "http://auth-server-1:16080" url: "http://auth-server-1:16080"
# Enable membership look-ups via /is_joined. # Enable membership look-ups via /is_joined.
+1 -1
View File
@@ -5,7 +5,7 @@ sender_localpart: "_lk_jwt_service"
namespaces: namespaces:
users: users:
- exclusive: false - exclusive: false
regex: "@.*:homeserver" # Cover all local users regex: "@.*:synapse\\.m\\.localhost" # Cover all local users
# Required for Synapse to call POST /_matrix/app/v1/ping on this service. # Required for Synapse to call POST /_matrix/app/v1/ping on this service.
url: "http://auth-server:6080" url: "http://auth-server:6080"
# Enable membership look-ups via /is_joined. # Enable membership look-ups via /is_joined.
+109
View File
@@ -125,6 +125,7 @@ test("Subscriber falls back to the JWT service when the homeserver lacks MSC4195
"Pevara", "Pevara",
"2_0", "2_0",
); );
// Wait for the call to connect and render. // 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);
@@ -170,6 +171,58 @@ test("Publisher shows an error when the homeserver lacks MSC4195", async ({
).toBeVisible(); ).toBeVisible();
}); });
test("Delegated leave on a url transport ends the membership when the guest drops", async ({
browser,
page,
browserName,
}) => {
skipOnFirefox(browserName);
// The host starts the call.
await advertiseTransport(page, shapes.url);
await page.goto("/");
await SpaHelpers.createCall(page, "Androl", "HelloCall", true, "2_0");
const inviteLink = await SpaHelpers.getCallInviteLink(page);
// The guest joins.
const guestContext = await browser.newContext({ reducedMotion: "reduce" });
const guestPage = await guestContext.newPage();
await advertiseTransport(guestPage, shapes.url);
const delayedLeave = recordDelayedLeave(guestPage);
const delegations = recordDelegations(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 handed its delayed leave to the homeserver, after probing it for
// delegation support with bodiless requests.
await expect
.poll(() => delegations.at(-1))
.toEqual({ status: 200, sfuUrl, delayId: delayedLeave.delayId });
const probes = delegations.slice(0, -1);
expect(probes.length).toBeGreaterThan(0);
for (const probe of probes) expect(probe).toEqual({ status: 401 });
// The delay is long enough that a leave within the window below cannot come
// from the delayed event expiring on its own.
expect(delayedLeave.delayMs).toBeGreaterThan(60_000);
// The guest drops without leaving.
await guestPage.reload();
// The host eventually sees the guest go once the delayed event fires.
await expect(page.getByTestId("videoTile")).toHaveCount(1, {
timeout: 30_000,
});
});
function skipOnFirefox(browserName: string): void { function skipOnFirefox(browserName: string): void {
test.skip( test.skip(
browserName === "firefox", browserName === "firefox",
@@ -216,6 +269,61 @@ async function rejectCsApiTokens(page: Page): Promise<void> {
}); });
} }
/** A recorded delayed leave event. */
interface DelayedLeave {
delayMs?: number;
delayId?: string;
}
/** Records the delayed leave event the page schedules on joining. */
function recordDelayedLeave(page: Page): DelayedLeave {
const delayedLeave: DelayedLeave = {};
page.on("response", (response) => {
const delay = new URL(response.request().url()).searchParams.get(
"org.matrix.msc4140.delay",
);
if (delay === null || response.status() !== 200) return;
delayedLeave.delayMs = Number(delay);
response
.json()
.then((body: { delay_id: string }) => {
delayedLeave.delayId = body.delay_id;
})
.catch(() => {});
});
return delayedLeave;
}
/** A recorded delegation request for a delayed leave event. */
interface Delegation {
status: number;
sfuUrl?: string;
delayId?: string;
}
/** Records requests to the MSC4195 `delegate_delayed_leave` endpoint. */
function recordDelegations(page: Page): Delegation[] {
const delegations: Delegation[] = [];
page.on("response", (response) => {
const request = response.request();
if (
request.method() !== "POST" ||
!request.url().endsWith("/rtc/livekit/delegate_delayed_leave")
)
return;
const body = request.postDataJSON() as {
url: string;
delay_id: string;
} | null;
delegations.push({
status: response.status(),
...(body && { sfuUrl: body.url, delayId: body.delay_id }),
});
});
return delegations;
}
/** A recorded token request. */
interface AuthRequest { interface AuthRequest {
flow: Flow; flow: Flow;
status: number; status: number;
@@ -223,6 +331,7 @@ interface AuthRequest {
sfuUrl?: string; sfuUrl?: string;
} }
/** Records token requests. */
function recordAuthRequests(page: Page): AuthRequest[] { function recordAuthRequests(page: Page): AuthRequest[] {
const requests: AuthRequest[] = []; const requests: AuthRequest[] = [];
page.on("response", (response) => { page.on("response", (response) => {