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.
94 lines
4.3 KiB
TypeScript
94 lines
4.3 KiB
TypeScript
import { expect, test } from "@playwright/test";
|
|
|
|
// The admin panel as a homepage-admins member. The 200 itself is the RBAC
|
|
// allow-leg: the same path returns 403 for the standard user (10-homepage).
|
|
|
|
test("admin panel renders admin-category services", async ({ page }) => {
|
|
await page.goto("/admin/");
|
|
await expect(page).toHaveTitle(/frank — admin/, { timeout: 60_000 });
|
|
|
|
const registry: { name: string; category: string }[] = await (await page.request.get("/services.json")).json();
|
|
const adminNames = registry.filter((s) => s.category === "admin").map((s) => s.name);
|
|
|
|
// Panels hydrate client-side; poll the count, then compare exact sets.
|
|
await expect
|
|
.poll(() => page.locator("#grid .panel .name").count(), { timeout: 60_000 })
|
|
.toBe(adminNames.length);
|
|
const names = await page.locator("#grid .panel .name").allTextContents();
|
|
|
|
expect([...names].sort()).toEqual([...adminNames].sort());
|
|
// RBAC tripwire: a user-category panel in the admin grid means category leakage.
|
|
expect(names).not.toContain("SearXNG");
|
|
});
|
|
|
|
test("admin open links reach their apps through SSO", async ({ page }) => {
|
|
test.setTimeout(240_000);
|
|
// Expected link count comes from the live registry (admin showLink=true
|
|
// entries, now including the *arr vhosts that use their own auth). The
|
|
// frankbook-hosted links (probe on 192.168.0.112) are NOT walked here:
|
|
// they terminate on FrankBook-Pro, which sleeps when idle, so their walk
|
|
// lives in 19-frankbook.spec.ts (frankbook project — excluded from the
|
|
// default e2e run and from the deploy gate unless a frankbook trigger
|
|
// path changed; see tests/e2e/run.sh and scripts/deploy.sh).
|
|
const registry: {
|
|
name: string;
|
|
category: string;
|
|
showLink: boolean;
|
|
url: string;
|
|
probe: string;
|
|
}[] = await (await page.request.get("/services.json")).json();
|
|
const expectedAdminLinks = registry.filter((s) => s.category === "admin" && s.showLink).length;
|
|
const frankbookUrls = registry
|
|
.filter(
|
|
(s) =>
|
|
s.category === "admin" &&
|
|
s.showLink &&
|
|
((s.probe ?? "").startsWith("192.168.0.112") || s.url.includes("192.168.0.112")),
|
|
)
|
|
.map((s) => new URL(s.url).origin);
|
|
await page.goto("/admin/");
|
|
await expect(page.locator("#grid a.open")).toHaveCount(expectedAdminLinks);
|
|
const hrefs = await page.locator("#grid a.open").evaluateAll((els) =>
|
|
els.map((el) => (el as HTMLAnchorElement).href),
|
|
);
|
|
|
|
for (const href of hrefs) {
|
|
if (frankbookUrls.includes(new URL(href).origin)) continue; // 19-frankbook walks these
|
|
const target = new URL(href);
|
|
await page.goto(href);
|
|
// Every guarded vhost runs its own Anubis proof-of-work on first visit.
|
|
await expect
|
|
.poll(
|
|
() => page.locator("body").innerText().then((t) => !t.includes("Making sure you're not a bot")),
|
|
{ timeout: 60_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);
|
|
}
|
|
});
|
|
|
|
// Admin-space reachability must not depend on the panel's link list: an admin
|
|
// session must reach every guarded admin app by direct navigation, even where
|
|
// no open-link exists. This is the RBAC allow-leg for the outpost gates: an
|
|
// authenticated admin always lands in the app, never on a login wall or a 403.
|
|
test("admin direct navigation reaches every admin app without an auth wall", async ({ page }) => {
|
|
test.setTimeout(240_000);
|
|
for (const sub of ["grafana", "prometheus", "loki"]) {
|
|
const host = `${sub}.franksdank.cloud`;
|
|
await page.goto(`https://${host}/`);
|
|
// The outpost hands off silently for an authenticated session; the admin
|
|
// must land in the app, not on authentik's flow UI.
|
|
await expect
|
|
.poll(() => page.locator('input[name="uidField"]').isVisible().catch(() => false), { timeout: 60_000 })
|
|
.toBe(false);
|
|
expect(new URL(page.url()).hostname, `${host} must not bounce an admin off its host`).toBe(host);
|
|
const resp = await page.request.get(`https://${host}/`);
|
|
expect(resp.status(), `${host} must answer an authenticated admin, not error`).toBeLessThan(500);
|
|
}
|
|
});
|