Files
Frank fc099d9b3c e2e: accept Plex's session-less hand-off to app.plex.tv
The full-suite re-run after the reset fix came back "1 flaky": 12-crawl reached
plex.franksdank.cloud, the guard cleared, and Plex Web — signed out in the
qa-e2e profile — handed the browser to its own hosted sign-in surface, so the
hostname never settled on the vhost (60 s poll, Expected plex.franksdank.cloud /
Received app.plex.tv). settleThroughGuard now takes a HANDOFFS landing list per
vhost; only plex.franksdank.cloud gets one, every other host still has to land on
its own vhost, and the guard assertions (Anubis cleared, no authentik uidField,
status < 500) are unchanged. Worklog and E2E gotcha 6 carry both symptoms: the
ERR_ABORTED reset and the hand-off.
2026-10-17 22:49:35 -07:00

90 lines
3.9 KiB
TypeScript

import { expect, test, type Page } from "@playwright/test";
// Clicks every open link on the user homepage and asserts the destination is
// the real app, reached through the guard chain without an authentik prompt.
// The link set is read live from the rendered homepage (it mirrors
// shared/homepage/services.json open entries) so a registry edit cannot leave
// this test asserting a stale world.
const CHALLENGE = "Making sure you're not a bot";
// Vendor hand-offs. An app may legitimately carry a session-less browser to its
// OWN hosted surface: Plex Web (plex.franksdank.cloud, served by our stack)
// bounces a signed-out visitor to app.plex.tv after it has already answered.
// That is the app working, not a guard-chain failure — the fleet contract this
// spec owns is still judged: the Anubis challenge cleared, no authentik
// uidField prompt appeared, and the app answered below 500. Any host NOT listed
// here still has to land on its own vhost.
const HANDOFFS: Record<string, string[]> = {
"plex.franksdank.cloud": ["app.plex.tv", "accounts.plex.tv", "clients.plex.tv"],
};
async function settleThroughGuard(
page: Page,
targetHost: string,
foreignHosts: string[] = [],
): Promise<void> {
// Each guarded vhost runs its own Anubis proof-of-work on first visit;
// wait for the challenge to clear before judging the destination.
await expect
.poll(async () => !(await page.locator("body").innerText()).includes(CHALLENGE), { timeout: 90_000 })
.toBe(true);
const landing = [targetHost, ...foreignHosts];
await expect
.poll(() => landing.includes(new URL(page.url()).hostname), { timeout: 60_000 })
.toBe(true);
expect(await page.locator('input[name="uidField"]').count()).toBe(0);
}
async function backHome(page: Page): Promise<void> {
// The homepage is the loop's only anchor, and some apps keep issuing their
// OWN navigation after load — Plex Web's "would like to sign in to your
// Plex account" iframe redirects the top frame, which cancels our goto with
// net::ERR_ABORTED. That abort is the app's behaviour, not a guard-chain
// failure (the app itself was already judged above), so tolerate it and
// re-issue rather than letting one host abort the whole crawl.
for (let attempt = 0; attempt < 3; attempt++) {
try {
await page.goto("/", { waitUntil: "domcontentloaded" });
return;
} catch (err) {
if (!String(err).includes("ERR_ABORTED")) throw err;
await page.waitForTimeout(1_000);
}
}
await page.goto("/");
}
test("every user open link reaches its app", async ({ page }) => {
test.setTimeout(420_000);
// Expected link count comes from the live registry, mirroring the
// showLink=true user entries — a registry edit cannot stale this test.
const registry: { name: string; category: string; showLink: boolean }[] =
await (await page.request.get("/services.json")).json();
const expectedUserLinks = registry.filter((s) => s.category === "user" && s.showLink).length;
await page.goto("/");
await expect(page.locator("#grid a.open")).toHaveCount(expectedUserLinks);
const hrefs = await page
.locator("#grid a.open")
.evaluateAll((els) => els.map((el) => ({ href: (el as HTMLAnchorElement).href, label: el.textContent ?? "" })));
const failures: string[] = [];
for (const link of hrefs) {
const target = new URL(link.href);
try {
await page.goto(link.href);
await settleThroughGuard(page, target.hostname, HANDOFFS[target.hostname] ?? []);
const resp = await page.request.get(page.url());
if (resp.status() >= 500) {
failures.push(`${link.label}: ${page.url()} answered ${resp.status()}`);
}
} catch (err) {
failures.push(`${link.label}: ${err instanceof Error ? err.message : String(err)}`);
}
await backHome(page);
await expect(page.locator("#grid a.open")).toHaveCount(expectedUserLinks);
}
expect(failures, failures.join("\n")).toEqual([]);
});