e2e: budget user/admin title asserts to 60s for cold Anubis PoW

This commit is contained in:
Frank
2026-09-01 17:09:17 -07:00
parent 9c593476af
commit 912c0edeff
2 changed files with 41 additions and 16 deletions
+20 -10
View File
@@ -1,25 +1,35 @@
import { expect, test } from "@playwright/test";
// The user-facing homepage as a standard (non-admin) authenticated user.
// Expected panels derive from the registry the homepage itself renders
// (/services.json), so a registry edit updates the test instead of breaking it.
test("user homepage renders only user-category services", async ({ page }) => {
await page.goto("/");
await expect(page).toHaveTitle(/frank — services/);
await expect(page).toHaveTitle(/frank — services/, { timeout: 60_000 });
const registry: { name: string; category: string }[] = await (await page.request.get("/services.json")).json();
const userNames = registry.filter((s) => s.category === "user").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(userNames.length);
const names = await page.locator("#grid .panel .name").allTextContents();
expect(names.length).toBeGreaterThanOrEqual(9);
for (const wanted of ["SearXNG", "OpenWebUI", "Gitea", "RomM", "Jellyfin", "Seerr"]) {
expect(names, `user panel for ${wanted}`).toContain(wanted);
}
for (const adminOnly of ["Grafana", "Prometheus", "Loki", "Hermes"]) {
expect(names, `admin service ${adminOnly} must not leak into user grid`).not.toContain(adminOnly);
}
expect([...names].sort()).toEqual([...userNames].sort());
// RBAC tripwire: an admin-category panel in the user grid means the gate leaks.
expect(names).not.toContain("Grafana");
});
test("user homepage exposes exactly the registry's open links", async ({ page }) => {
await page.goto("/");
await expect(page.locator("#grid a.open")).toHaveCount(9);
const registry: { name: string; category: string; showLink: boolean }[] = await (
await page.request.get("/services.json")
).json();
const openCount = registry.filter((s) => s.category === "user" && s.showLink).length;
await page.goto("/");
await expect(page.locator("#grid a.open")).toHaveCount(openCount, { timeout: 60_000 });
const hrefs = await page.locator("#grid a.open").evaluateAll((els) =>
els.map((el) => (el as HTMLAnchorElement).getAttribute("href")),
);
+21 -6
View File
@@ -5,25 +5,40 @@ import { expect, test } from "@playwright/test";
test("admin panel renders admin-category services", async ({ page }) => {
await page.goto("/admin/");
await expect(page).toHaveTitle(/frank — 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();
for (const wanted of ["Grafana", "Prometheus", "Loki"]) {
expect(names, `admin panel for ${wanted}`).toContain(wanted);
}
expect(names, "user services stay out of the admin grid").not.toContain("SearXNG");
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);
await page.goto("/admin/");
await expect(page.locator("#grid a.open")).toHaveCount(3); // grafana, prometheus, loki
const hrefs = await page.locator("#grid a.open").evaluateAll((els) =>
els.map((el) => (el as HTMLAnchorElement).href),
);
expect(hrefs.length).toBe(3); // grafana, prometheus, loki
for (const href of hrefs) {
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);