- Every app card carries hex-clipped art: 19 amber-tinted brand marks from the Simple Icons CDN plus 19 improvised monogram SVGs, wired via a per-service "art" field; homepage_config.bats pins art presence for all 32 apps. - Model cards render zealot -> dragoon -> archon (registry order pinned in the seam). - Dragoon and zealot share one common uncensored legal warning (adult content, harmful/hateful/illegal descriptions, sensitive offensive cybersecurity topics, severe legal risk, "don't ruin this server for everybody"), hover label on card and docs. - Archon gets a bordered red permission callout: ask Frank before use. - SearXNG obfuscation-not-anonymity note moved to a card-level hover warning (.svc-warn) and a docs hover note. - Gitea: self-service signup flow; no need to ask Frank. - Footer docs/admin/account-settings become glowing bordered buttons; the admin button is shown only to homepage-admins members via a new Caddy /whoami endpoint echoing the outpost-injected X-Authentik-* headers (spoof-proof; the /admin* 403 stays the real gate). e2e pins both sides. - Docs naming swept: Protoss names primary, bracketed api ids, explicit note that a future switchover renames the ids themselves.
58 lines
2.8 KiB
TypeScript
58 lines
2.8 KiB
TypeScript
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");
|
|
});
|