import { expect, test } from "@playwright/test"; // Clicks every open link on the user homepage and asserts the destination is // the real app, reached through the guard chain without an authentik prompt. // The link set is read live from the rendered homepage (it mirrors // shared/homepage/services.json), so registry edits flow into the crawl // automatically. Apps with their own auth (Gitea, OpenWebUI, Calibre-Web, // Plex, Jellyfin) must show THEIR OWN landing page; guarded apps must arrive // via silent SSO. test("every user open link reaches its app", async ({ page }) => { await page.goto("/"); const hrefs = await page.locator("#grid a.open").evaluateAll((els) => els.map((el) => (el as HTMLAnchorElement).href), ); expect(hrefs.length).toBeGreaterThanOrEqual(8); const failures: string[] = []; for (const href of hrefs) { const target = new URL(href); try { await page.locator(`#grid a.open[href="${href}"]`).click(); // Guarded hops pass through Anubis (auto-solves) and authentik (silent // with a session); wait until we have landed off the IdP. await expect .poll(() => new URL(page.url()).hostname, { timeout: 60_000 }) .not.toBe("auth.franksdank.cloud"); expect(new URL(page.url()).hostname, `${target.hostname} should serve its own vhost`).toBe( target.hostname, ); expect( await page.locator('input[name="uidField"]').count(), `${target.hostname} must not re-prompt for authentik login`, ).toBe(0); const body = await page.locator("body").innerText(); expect(body, `${target.hostname} must not show an Anubis interstitial`).not.toContain( "Making sure you're not a bot", ); const resp = await page.request.get(page.url()); expect(resp.status(), `${target.hostname} must answer, not error`).toBeLessThan(500); } catch (err) { failures.push(`${target.hostname}: ${err instanceof Error ? err.message : String(err)}`); await page.goto("/"); await page.locator("#grid .panel").first().waitFor(); } } expect(failures, failures.join("\n")).toEqual([]); });