import { expect, test } from "@playwright/test"; // hermes.franksdank.cloud fronts the Hermex webui, which is a HOST-NETWORKED // container: Caddy cannot reach it by container name, so the vhost's upstream // is the host's LAN address (`reverse_proxy 192.168.0.143:8787` in // dankclaw/Caddyfile) and the app must therefore bind beyond loopback. // // That coupling broke on 2026-09-16: the unit's bind was tightened to // 127.0.0.1 for a stricter posture, which killed the upstream and left the // vhost returning 502 — and the whole e2e suite stayed green, because no spec // covered this hostname. It is the only guarded vhost whose upstream is a host // address rather than a container name, so it needs its own assertion. // // The app's own password login is expected once the guard chain passes (unlike // the SSO-silent vhosts in 13-sso), so this spec asserts only what a broken // upstream would violate: no 5xx, no gate page, and no Caddy error page. const VHOST = "https://hermes.franksdank.cloud/"; test("hermes vhost reaches its app through the guard chain", async ({ page }) => { test.setTimeout(180_000); // An unreachable upstream is a 502 from Caddy; nothing else in the chain // produces one, which is what makes this the load-bearing assertion. const resp = await page.request.get(VHOST, { maxRedirects: 5 }); expect(resp.status()).toBeLessThan(500); await page.goto(VHOST); await expect .poll( async () => !(await page.locator("body").innerText()).includes("Making sure you're not a bot"), { timeout: 90_000 }, ) .toBe(true); const body = await page.locator("body").innerText(); // Caddy's own error text must never be what a user sees here. expect(body).not.toContain("Bad Gateway"); expect(body).not.toContain("502"); // The guard's error surface, as opposed to the app's own login. expect(body).not.toContain("Powered by authentik"); expect(body).not.toContain("Making sure you're not a bot"); }); test("hermes vhost does not leak app bytes to an unauthenticated raw client", async ({ request, }) => { // Same rule as 15-unauth-fetch: a raw client solves no challenge, so it must // get the gate, not the application. const resp = await request.get(VHOST, { maxRedirects: 0 }); expect(resp.status()).toBeLessThan(500); const body = await resp.text().catch(() => ""); expect(body).not.toContain("Powered by authentik"); });