tests: gate the hermes vhost, and stop specs from silently not running
Two gaps behind the same near-miss. 1. **No spec covered hermes.franksdank.cloud** — the one guarded vhost whose upstream is a host address (`reverse_proxy 192.168.0.143:8787`) rather than a container name, because its service is host-networked. That is how a loopback-only bind could take the vhost down with a 502 and leave every gate green. tests/e2e/specs/17-hermes-vhost.spec.ts asserts what a dead upstream violates: no 5xx through the guard chain, no Caddy error page, and no app bytes to an unauthenticated raw client. It does not assert SSO-silence, since this app has its own password login. 2. **Specs are matched by number prefix, not globbed.** A spec file whose prefix is in no project silently never runs — which is exactly what happened to the new one until it was registered. The prefix is registered now, the comment in playwright.config.ts says why, and tests/e2e_scaffold.bats fails if any spec file matches no project. The hermes-webui seam assertion is corrected too: it asserted the loopback bind I wrongly imposed, and now asserts the real contract — the unit's bind must serve the address the Caddyfile fronts it on.
This commit is contained in:
@@ -24,7 +24,12 @@ export default defineConfig({
|
||||
},
|
||||
{
|
||||
name: "standard",
|
||||
testMatch: /(10|12|13|16)-.*\.spec\.ts/,
|
||||
// NOTE: specs are matched by NUMBER PREFIX, not globbed. A spec file
|
||||
// whose prefix is not listed in any project silently never runs — a
|
||||
// green suite with a test that does not exist. 17- joined on 2026-09-16
|
||||
// (the hermes vhost, whose upstream is a host address rather than a
|
||||
// container name, so it needs its own pass condition).
|
||||
testMatch: /(10|12|13|16|17)-.*\.spec\.ts/,
|
||||
use: { storageState: ".auth/qa-e2e.json" },
|
||||
dependencies: ["setup"],
|
||||
},
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
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");
|
||||
});
|
||||
Reference in New Issue
Block a user