Files
app/frontend/tests/guard.ts
T
stroblmeandClaude Opus 5 41b2c28b2b The e2e suite names its own origins, and refuses a live instance
`tests/utils/api.ts` took the API origin from `VITE_API_URL`, which
`tests/config.ts` loads out of `app/.env`. In a checkout configured for a
deployment that names the deployment — so the browser went to the local stack
while every setup and teardown call, `deleteAll` included, went to the live
one. `privateApi.ts` had the same reading, and it creates users.

Both origins now come from one place: `PLAYWRIGHT_BASE_URL`, with the API
derived from it (`app.<domain>` → `api.<domain>`) or named outright by
`PLAYWRIGHT_API_URL`, which is what CI and the compose service set. Nothing in
the suite reads `VITE_API_URL` any more.

Belt and braces, since a stack served under a real domain answers to the same
names its production instance does: a global setup resolves both origins and
refuses anything that is not loopback or a private range, before a test runs.
`PLAYWRIGHT_ALLOW_PUBLIC=1` says you meant it.

`make test-frontend` is now that safe run — the Playwright image on the proxy
network with both names mapped onto Traefik by address, as the host user so it
does not leave root-owned results behind.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NUb8YpL2s3gmN9WTACTt4q
2026-08-20 20:46:04 +02:00

61 lines
2.2 KiB
TypeScript

import { lookup } from "node:dns/promises"
import { apiUrl, appUrl } from "./config.ts"
/**
* Refuse to run against a machine on the internet.
*
* The suite creates flows, dashboards and users and deletes them again, so the
* cost of pointing it at the wrong instance is somebody's data. That is not a
* hypothetical: a stack served under a real domain answers to the same names
* its production instance does, and the only thing telling them apart is which
* address those names resolve to here.
*
* So that is what is checked. Loopback and the private ranges are a stack on
* this machine or on its docker network; anything else is refused by name and
* address, before a single test runs. `PLAYWRIGHT_ALLOW_PUBLIC=1` is the way
* to say you meant it.
*/
const PRIVATE_V4 = [
/^127\./,
/^10\./,
/^192\.168\./,
/^169\.254\./,
/^172\.(1[6-9]|2\d|3[01])\./,
]
function isPrivate(address: string): boolean {
if (address === "::1" || address === "::") return true
// ::ffff:172.18.0.2 and friends.
const mapped = address.replace(/^::ffff:/i, "")
if (mapped !== address) return isPrivate(mapped)
if (/^f[cd][0-9a-f]{2}:/i.test(address)) return true // unique-local
if (/^fe80:/i.test(address)) return true // link-local
return PRIVATE_V4.some((range) => range.test(address))
}
export default async function guardTheTarget(): Promise<void> {
if (process.env.PLAYWRIGHT_ALLOW_PUBLIC === "1") return
for (const target of new Set([appUrl, apiUrl])) {
const { hostname } = new URL(target)
let address: string
try {
address = (await lookup(hostname)).address
} catch {
// Unresolvable is the run's own problem to report; it cannot be a live
// instance, which is all this guard is here for.
continue
}
if (isPrivate(address)) continue
throw new Error(
`Refusing to run: ${target} resolves to ${address}, which is not this ` +
"machine or its docker network. This suite creates and deletes flows, " +
"dashboards and users.\n" +
"Run it against the local stack — `make -C app test-frontend` maps " +
"the names onto Traefik — or set PLAYWRIGHT_ALLOW_PUBLIC=1 if you " +
"really mean this one.",
)
}
}