import { expect, test } from "@playwright/test"; // The *arr / qBittorrent admin links terminate on FrankBook-Pro // (192.168.0.112), a Mac mini that SLEEPS when idle: Caddy answers 502 for // the vhosts and the raw Sonarr link times out. This walk therefore lives in // its own project, excluded from the default e2e run (tests/e2e/run.sh) and // from the deploy gate's invocation (scripts/deploy.sh) unless a frankbook // trigger path changed: this spec, frankbook-pro/**, // shared/homepage/services.json, or the dankclaw Caddyfile that serves the // arr vhosts. A sleeping Mac mini must never fail an unrelated deploy. // // Assertions mirror 11-admin's "admin open links reach their apps through // SSO": the guard chain settles (Anubis PoW where present — Sonarr's raw-IP // link is internal-only and has none), the hostname holds, no authentik form // appears, and the app answers below 500. const FRANKBOOK_IP = "192.168.0.112"; const CHALLENGE = "Making sure you're not a bot"; interface RegistryEntry { name: string; category: string; showLink: boolean; url: string; probe: string; } test("frankbook admin open links reach their apps", async ({ page }) => { test.setTimeout(240_000); const registry: RegistryEntry[] = await (await page.request.get("/services.json")).json(); const frankbook = registry.filter( (s) => s.category === "admin" && s.showLink && ((s.probe ?? "").startsWith(FRANKBOOK_IP) || s.url.includes(FRANKBOOK_IP)), ); const frankbookUrls = frankbook.map((s) => new URL(s.url).origin); await page.goto("/admin/"); const hrefs = await page .locator("#grid a.open") .evaluateAll((els) => els.map((el) => (el as HTMLAnchorElement).href)); const frankbookHrefs = hrefs.filter((href) => frankbookUrls.includes(new URL(href).origin)); // The rendered frankbook link set must mirror the registry: a renamed arr // vhost or a dropped probe fails here instead of silently dropping the // walk's coverage. expect(frankbookHrefs, "rendered frankbook links must match the registry").toHaveLength( frankbook.length, ); for (const href of frankbookHrefs) { const target = new URL(href); await page.goto(href); // Guarded vhosts run their own Anubis proof-of-work on first visit; the // raw-IP Sonarr link has none and settles immediately. await expect .poll( () => page.locator("body").innerText().then((t) => !t.includes(CHALLENGE)), { timeout: 90_000 }, ) .toBe(true); await expect .poll(() => new URL(page.url()).hostname, { timeout: 60_000 }) .toBe(target.hostname); expect(await page.locator('input[name="uidField"]').count()).toBe(0); const resp = await page.request.get(page.url()); expect(resp.status(), `${target.hostname} must answer, not error`).toBeLessThan(500); } });