Files
serverAppConfigs/tests/e2e/specs/19-frankbook.spec.ts
T
Frank 9c508218e9 e2e: scope the FrankBook-Pro leg to its own trigger-gated Playwright project
The 11-admin spec walked links that terminate on FrankBook-Pro
(192.168.0.112), which sleeps when idle — every deploy gate timed out
there (measured 2026-10-05, two deploy windows). Split: the frankbook
links move to tests/e2e/specs/19-frankbook.spec.ts under a frankbook
project that run.sh excludes by default and deploy.sh triggers ONLY
when a frankbook blast-radius path changed (the spec, frankbook-pro/,
the homepage registry, the Caddyfile arr vhosts). deploy.sh computes
the trigger from the same BASE..HEAD revision range the plan uses.
2026-10-05 18:04:02 -07:00

70 lines
2.8 KiB
TypeScript

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);
}
});