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/, { 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. // The three Protoss model cards are fused into one Model Information // component: each model renders a `.mi-name` inside a full-width panel, // not a standalone `.panel .name`. Count both render styles so the // registry stays the single source of truth for the expected names. const nameSel = "#grid .panel .name, #grid .mi-name"; await expect .poll(() => page.locator(nameSel).count(), { timeout: 60_000 }) .toBe(userNames.length); const names = await page.locator(nameSel).allTextContents(); 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 }) => { 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")), ); for (const href of hrefs) { expect(href, "links use current vhost names").not.toMatch(/chat\.|jellyfin\.|comfyui\./); } }); test("standard user is denied the admin panel (RBAC)", async ({ page }) => { const resp = await page.request.get("/admin/"); expect(resp.status()).toBe(403); expect(await resp.text()).toContain("Forbidden: homepage admins only"); // The admin button is identity-gated: /whoami reports the authentik // groups, and the page hides the button from non-admins (the server-side // 403 above stays the real gate). Direct navigation dead-ends all the same. await page.goto("/"); await expect(page.locator("#admin-link")).toBeHidden(); await page.goto("/admin/"); await expect(page.locator("body")).toContainText("Forbidden: homepage admins only"); });