Consolidate E2E into e2e-tests/ (Dockerfile + compose.{visual,auth,oauth,full,devstack}.yml, WORKDIR /e2e-tests); move ci.sh to repo root

This commit is contained in:
2026-06-23 23:48:05 +02:00
parent bb612baa2c
commit 913bd6813a
25 changed files with 87 additions and 84 deletions
+4 -1
View File
@@ -4,4 +4,7 @@ npm-debug.log
*.log *.log
.DS_Store .DS_Store
e2e/artifacts e2e-tests/artifacts
# Orchestration, not test code — keep them out of the runner image (COPY e2e-tests/ ./)
e2e-tests/Dockerfile
e2e-tests/compose.*.yml
+1 -1
View File
@@ -4,4 +4,4 @@
node_modules node_modules
# Playwright E2E outputs (screenshots, html report, traces) # Playwright E2E outputs (screenshots, html report, traces)
e2e/artifacts/ e2e-tests/artifacts/
-12
View File
@@ -1,12 +0,0 @@
# Playwright runner — browsers preinstalled, pinned to match @playwright/test in e2e/.
# Built/run via compose.e2e.yml; targets the `web` service over the network.
FROM mcr.microsoft.com/playwright:v1.49.1-noble
WORKDIR /e2e
COPY e2e/package.json e2e/package-lock.json ./
RUN npm ci
COPY e2e/ ./
CMD ["npx", "playwright", "test"]
+19 -19
View File
@@ -650,8 +650,8 @@ off-canvas layout, icon sprite, CSRF-guarded sign-out, the public landing, the 4
plugin permission-gating. plugin permission-gating.
```bash ```bash
docker compose -f compose.yml -f compose.e2e.yml run --build --rm e2e # run the suite docker compose -f compose.yml -f e2e-tests/compose.visual.yml run --build --rm e2e # run the suite
docker compose -f compose.yml -f compose.e2e.yml down -v # tear down after docker compose -f compose.yml -f e2e-tests/compose.visual.yml down -v # tear down after
``` ```
**Auth — token timeout + refresh** (`auth-refresh.spec.ts`) — the full-stack counterpart: it **Auth — token timeout + refresh** (`auth-refresh.spec.ts`) — the full-stack counterpart: it
@@ -662,8 +662,8 @@ live Kratos session (roles re-read from Keto), and once that session is revoked
cookie is **cleared**. cookie is **cleared**.
```bash ```bash
docker compose -f compose.yml -f compose.e2e-auth.yml run --build --rm e2e # run the suite docker compose -f compose.yml -f e2e-tests/compose.auth.yml run --build --rm e2e # run the suite
docker compose -f compose.yml -f compose.e2e-auth.yml down -v # tear down after docker compose -f compose.yml -f e2e-tests/compose.auth.yml down -v # tear down after
``` ```
**OAuth2 login + consent** (`oauth-login.spec.ts`) — another app logs in *through* us: it **OAuth2 login + consent** (`oauth-login.spec.ts`) — another app logs in *through* us: it
@@ -674,24 +674,24 @@ then shows the consent screen for the third-party client and **Allow** drives Hy
the authorization code. the authorization code.
```bash ```bash
docker compose -f compose.yml -f compose.e2e-oauth.yml run --build --rm e2e # run the suite docker compose -f compose.yml -f e2e-tests/compose.oauth.yml run --build --rm e2e # run the suite
docker compose -f compose.yml -f compose.e2e-oauth.yml down -v # tear down after docker compose -f compose.yml -f e2e-tests/compose.oauth.yml down -v # tear down after
``` ```
**Full browser flow** (`full-flow.spec.ts`) — the real Playwright UI against the live stack: **Full browser flow** (`full-flow.spec.ts`) — the real Playwright UI against the live stack:
the themed **password login** and a **mocked-SSO** login (an in-network mock OIDC provider, the themed **password login** and a **mocked-SSO** login (an in-network mock OIDC provider,
`e2e/mock-oidc.mjs`), **menu filtering by role**, the **users/groups/roles** admin CRUD, a `e2e-tests/mock-oidc.mjs`), **menu filtering by role**, the **users/groups/roles** admin CRUD, a
permission-gated **plugin page**, and **logout**. Because the themed form posts straight to permission-gated **plugin page**, and **logout**. Because the themed form posts straight to
Kratos and cookies are host-scoped, a tiny same-origin gateway (`e2e/proxy.mjs`) fronts web + Kratos and cookies are host-scoped, a tiny same-origin gateway (`e2e-tests/proxy.mjs`) fronts web +
Kratos on one host (`ory/kratos/e2e-proxy.yml` points Kratos at it) — exactly as a production Kratos on one host (`ory/kratos/e2e-proxy.yml` points Kratos at it) — exactly as a production
reverse proxy would. reverse proxy would.
```bash ```bash
docker compose -f compose.yml -f compose.e2e-full.yml run --build --rm e2e # run the suite docker compose -f compose.yml -f e2e-tests/compose.full.yml run --build --rm e2e # run the suite
docker compose -f compose.yml -f compose.e2e-full.yml down -v # tear down after docker compose -f compose.yml -f e2e-tests/compose.full.yml down -v # tear down after
``` ```
`--build` rebuilds the runner so spec edits are always picked up (the image bakes in `e2e/`). `--build` rebuilds the runner so spec edits are always picked up (the image bakes in `e2e-tests/`).
**Dev-stack login regression** (`devstack-login.spec.ts`) — drives the *plain* `docker **Dev-stack login regression** (`devstack-login.spec.ts`) — drives the *plain* `docker
compose up` topology (not the same-origin gateway above) with the runner on the **host compose up` topology (not the same-origin gateway above) with the runner on the **host
@@ -702,26 +702,26 @@ first-run banner advertises (`http://localhost:3000`) **and** from the wrong hos
[canonical-host redirect](#canonical-host-one-public-url). It guards against the regression [canonical-host redirect](#canonical-host-one-public-url). It guards against the regression
where the advertised login URL dumps the user on the `/error` "Page not found" page; the where the advertised login URL dumps the user on the `/error` "Page not found" page; the
proxied full-flow suite can't catch this (it fronts web + Kratos on one origin). Part of proxied full-flow suite can't catch this (it fronts web + Kratos on one origin). Part of
`scripts/ci.sh` — it needs host networking and the host ports `3000`/`4433` free (Linux). `ci.sh` — it needs host networking and the host ports `3000`/`4433` free (Linux).
```bash ```bash
docker compose -f compose.yml -f compose.override.yml -f compose.e2e-devstack.yml run --build --rm e2e # run it docker compose -f compose.yml -f compose.override.yml -f e2e-tests/compose.devstack.yml run --build --rm e2e # run it
docker compose -f compose.yml -f compose.override.yml -f compose.e2e-devstack.yml down -v # tear down docker compose -f compose.yml -f compose.override.yml -f e2e-tests/compose.devstack.yml down -v # tear down
``` ```
Screenshots + an HTML report land in `e2e/artifacts/` (git-ignored). Every user-facing flow Screenshots + an HTML report land in `e2e-tests/artifacts/` (git-ignored). Every user-facing flow
is covered end-to-end; tests are independent and run **fully in parallel** for speed is covered end-to-end; tests are independent and run **fully in parallel** for speed
([AGENTS.md](AGENTS.md)) — keep new tests side-effect-free so the suite stays fast. ([AGENTS.md](AGENTS.md)) — keep new tests side-effect-free so the suite stays fast.
### The full gate (one command) ### The full gate (one command)
`scripts/ci.sh` is the whole gate in one reproducible command — typecheck → unit tests → `ci.sh` is the whole gate in one reproducible command — typecheck → unit tests →
each E2E suite against its own fresh stack, with a guaranteed `down -v` after each (even on each E2E suite against its own fresh stack, with a guaranteed `down -v` after each (even on
failure) and a non-zero exit on the first failure. Run it locally before a release, or wire failure) and a non-zero exit on the first failure. Run it locally before a release, or wire
it into your CI service: it into your CI service:
```bash ```bash
bash scripts/ci.sh bash ci.sh
``` ```
Each E2E suite **owns a clean stack** — never point two suites at one backend (auth-refresh Each E2E suite **owns a clean stack** — never point two suites at one backend (auth-refresh
@@ -932,8 +932,8 @@ ory/ Ory service config (kratos/: identity schema, kratos.yml, o
plugins/ Drop-in plugin folders (scanned at /app/plugins; bind-mount or bake in). Ships scheduling/ — the reference plugin (list/form over an upstream + permission-gated nav) you copy plugins/ Drop-in plugin folders (scanned at /app/plugins; bind-mount or bake in). Ships scheduling/ — the reference plugin (list/form over an upstream + permission-gated nav) you copy
examples/ Non-app helpers; shifts-upstream/ is the dev mock backend the reference plugin reads/writes (stand-in for your real service) examples/ Non-app helpers; shifts-upstream/ is the dev mock backend the reference plugin reads/writes (stand-in for your real service)
docs/ Reference docs (plugin-contract.md — the authoritative plugin API) docs/ Reference docs (plugin-contract.md — the authoritative plugin API)
e2e/ Playwright E2E: visual.spec (design system, Ory-free) + auth-refresh.spec (token timeout/re-mint) + oauth-login.spec (OAuth2 login + consent) + full-flow.spec (browser UI: password/SSO login, menu-by-role, admin CRUD, plugin page, logout) + devstack-login.spec (regression: login works from the banner's localhost URL and 127.0.0.1 is canonicalised, on the plain `docker compose up` topology); proxy.mjs (same-origin gateway) + mock-oidc.mjs (mock SSO provider) back full-flow. Dockerfile.e2e + compose.e2e[-auth|-oauth|-full|-devstack].yml run them e2e-tests/ Playwright E2E: visual.spec (design system, Ory-free) + auth-refresh.spec (token timeout/re-mint) + oauth-login.spec (OAuth2 login + consent) + full-flow.spec (browser UI: password/SSO login, menu-by-role, admin CRUD, plugin page, logout) + devstack-login.spec (regression: login works from the banner's localhost URL and 127.0.0.1 is canonicalised, on the plain `docker compose up` topology); proxy.mjs (same-origin gateway) + mock-oidc.mjs (mock SSO provider) back full-flow. e2e-tests/Dockerfile + e2e-tests/compose.{visual,auth,oauth,full,devstack}.yml run them
scripts/ci.sh The full CI gate: typecheck → unit tests → every E2E suite, each on a fresh, always-torn-down stack (`bash scripts/ci.sh`) ci.sh The full CI gate: typecheck → unit tests → every E2E suite, each on a fresh, always-torn-down stack (`bash ci.sh`)
``` ```
## Extending the core ## Extending the core
+12 -12
View File
@@ -3,20 +3,20 @@
# that is always torn down. One reproducible command — run it locally or wire it into your CI # that is always torn down. One reproducible command — run it locally or wire it into your CI
# service. Docker-only (it drives `docker compose`; node/npm/tsc run inside containers, never the host). # service. Docker-only (it drives `docker compose`; node/npm/tsc run inside containers, never the host).
# #
# bash scripts/ci.sh # bash ci.sh
# #
# Exits non-zero on the first failure. Each E2E suite OWNS a clean stack — never point two suites at # Exits non-zero on the first failure. Each E2E suite OWNS a clean stack — never point two suites at
# one backend (auth-refresh revokes the admin's sessions; full-flow writes users/groups/roles to Keto). # one backend (auth-refresh revokes the admin's sessions; full-flow writes users/groups/roles to Keto).
set -euo pipefail set -euo pipefail
cd "$(dirname "$0")/.." cd "$(dirname "$0")"
step() { printf '\n\033[1;34m==> %s\033[0m\n' "$1"; } step() { printf '\n\033[1;34m==> %s\033[0m\n' "$1"; }
# Pins that MUST move in lockstep: a browser/runner mismatch yields confusing E2E failures. # Pins that MUST move in lockstep: a browser/runner mismatch yields confusing E2E failures.
step "Playwright pin lockstep (Dockerfile.e2e image == e2e/package.json @playwright/test)" step "Playwright pin lockstep (e2e-tests/Dockerfile image == e2e-tests/package.json @playwright/test)"
# `|| true` so a no-match doesn't trip `set -e`/`pipefail` before the explicit check below can report. # `|| true` so a no-match doesn't trip `set -e`/`pipefail` before the explicit check below can report.
img=$(grep -oE 'playwright:v[0-9.]+' Dockerfile.e2e | grep -oE '[0-9.]+$' || true) img=$(grep -oE 'playwright:v[0-9.]+' e2e-tests/Dockerfile | grep -oE '[0-9.]+$' || true)
pkg=$(grep -oE '"@playwright/test": "[0-9.]+"' e2e/package.json | grep -oE '[0-9.]+' || true) pkg=$(grep -oE '"@playwright/test": "[0-9.]+"' e2e-tests/package.json | grep -oE '[0-9.]+' || true)
[ -n "$img" ] && [ "$img" = "$pkg" ] || { echo "Playwright pin mismatch/unreadable: image v$img vs @playwright/test $pkg"; exit 1; } [ -n "$img" ] && [ "$img" = "$pkg" ] || { echo "Playwright pin mismatch/unreadable: image v$img vs @playwright/test $pkg"; exit 1; }
echo "ok ($img)" echo "ok ($img)"
@@ -41,19 +41,19 @@ e2e() {
[ "$rc" -eq 0 ] || { echo "E2E suite $1 failed (exit $rc)"; exit "$rc"; } [ "$rc" -eq 0 ] || { echo "E2E suite $1 failed (exit $rc)"; exit "$rc"; }
} }
e2e compose.e2e.yml # visual / design-system parity (Ory-free) e2e e2e-tests/compose.visual.yml # visual / design-system parity (Ory-free)
e2e compose.e2e-auth.yml # token timeout + silent re-mint e2e e2e-tests/compose.auth.yml # token timeout + silent re-mint
e2e compose.e2e-oauth.yml # OAuth2 login + consent e2e e2e-tests/compose.oauth.yml # OAuth2 login + consent
e2e compose.e2e-full.yml # full browser flow: login (password + SSO), menu, CRUD, plugin, logout e2e e2e-tests/compose.full.yml # full browser flow: login (password + SSO), menu, CRUD, plugin, logout
# Dev-stack login regression — runs against the PLAIN `docker compose up` topology (base + override) # Dev-stack login regression — runs against the PLAIN `docker compose up` topology (base + override)
# with the runner on the HOST network, so it can't use the shared e2e() helper (which merges only # with the runner on the HOST network, so it can't use the shared e2e() helper (which merges only
# compose.yml + the suite). Needs host networking + the host ports 3000/4433 free (Linux CI). # compose.yml + the suite). Needs host networking + the host ports 3000/4433 free (Linux CI).
step "E2E: compose.e2e-devstack.yml (dev-stack login: localhost works + 127.0.0.1 canonicalised)" step "E2E: e2e-tests/compose.devstack.yml (dev-stack login: localhost works + 127.0.0.1 canonicalised)"
devstack_files=(-f compose.yml -f compose.override.yml -f compose.e2e-devstack.yml) devstack_files=(-f compose.yml -f compose.override.yml -f e2e-tests/compose.devstack.yml)
rc=0 rc=0
docker compose -p plainpages-e2e-devstack "${devstack_files[@]}" run --build --rm e2e || rc=$? docker compose -p plainpages-e2e-devstack "${devstack_files[@]}" run --build --rm e2e || rc=$?
docker compose -p plainpages-e2e-devstack "${devstack_files[@]}" down -v >/dev/null 2>&1 || true docker compose -p plainpages-e2e-devstack "${devstack_files[@]}" down -v >/dev/null 2>&1 || true
[ "$rc" -eq 0 ] || { echo "E2E suite compose.e2e-devstack.yml failed (exit $rc)"; exit "$rc"; } [ "$rc" -eq 0 ] || { echo "E2E suite e2e-tests/compose.devstack.yml failed (exit $rc)"; exit "$rc"; }
step "ALL GREEN" step "ALL GREEN"
+1 -1
View File
@@ -399,7 +399,7 @@ worked example: thin handlers bound to an injectable upstream client, unit-teste
of the stack. of the stack.
3. **E2E the user-facing flow.** Per AGENTS.md §6, ship a side-effect-free Playwright test in 3. **E2E the user-facing flow.** Per AGENTS.md §6, ship a side-effect-free Playwright test in
`e2e/` for each plugin page/form so the suite stays `fullyParallel`, run against the live `web` `e2e-tests/` for each plugin page/form so the suite stays `fullyParallel`, run against the live `web`
service with the plugin mounted. The reference's permission-gating is covered in `visual.spec.ts`; service with the plugin mounted. The reference's permission-gating is covered in `visual.spec.ts`;
its authenticated list/form happy-path is the full-E2E item (needs cross-host login infra). its authenticated list/form happy-path is the full-E2E item (needs cross-host login infra).
+12
View File
@@ -0,0 +1,12 @@
# Playwright runner — browsers preinstalled, pinned to match @playwright/test in e2e-tests/.
# Built/run via e2e-tests/compose.visual.yml; targets the `web` service over the network.
FROM mcr.microsoft.com/playwright:v1.49.1-noble
WORKDIR /e2e-tests
COPY e2e-tests/package.json e2e-tests/package-lock.json ./
RUN npm ci
COPY e2e-tests/ ./
CMD ["npx", "playwright", "test"]
@@ -1,7 +1,7 @@
import { expect, test } from "@playwright/test"; import { expect, test } from "@playwright/test";
// Full-stack auth E2E: token timeout + silent re-mint ("stay signed in"). Runs against the // Full-stack auth E2E: token timeout + silent re-mint ("stay signed in"). Runs against the
// real Ory stack via compose.e2e-auth.yml, where the session→JWT TTL is shortened to 8s and the // real Ory stack via e2e-tests/compose.auth.yml, where the session→JWT TTL is shortened to 8s and the
// web clock skew is 0 — so the ~10m token lapses in seconds and the hot path re-mints it from the // web clock skew is 0 — so the ~10m token lapses in seconds and the hot path re-mints it from the
// still-live Kratos session. We drive the flow over HTTP (fetch, manual cookies) because Kratos // still-live Kratos session. We drive the flow over HTTP (fetch, manual cookies) because Kratos
// and web sit on different hosts here; web's own server-side cookie relay is what we exercise. // and web sit on different hosts here; web's own server-side cookie relay is what we exercise.
@@ -1,9 +1,9 @@
# Full-stack auth E2E — token timeout + silent re-mint ("stay signed in"). The Ory-free # Full-stack auth E2E — token timeout + silent re-mint ("stay signed in"). The Ory-free
# visual suite (compose.e2e.yml) covers the design system; this is its full-stack counterpart: # visual suite (e2e-tests/compose.visual.yml) covers the design system; this is its full-stack counterpart:
# real Postgres + Kratos + Keto + bootstrap + web, with a SHORT tokenizer TTL (ory/kratos/e2e.yml) # real Postgres + Kratos + Keto + bootstrap + web, with a SHORT tokenizer TTL (ory/kratos/e2e.yml)
# and zero clock skew, so the JWT lapses and re-mints within seconds instead of ~10m. # and zero clock skew, so the JWT lapses and re-mints within seconds instead of ~10m.
# docker compose -f compose.yml -f compose.e2e-auth.yml run --build --rm e2e # docker compose -f compose.yml -f e2e-tests/compose.auth.yml run --build --rm e2e
# docker compose -f compose.yml -f compose.e2e-auth.yml down -v # tear down after # docker compose -f compose.yml -f e2e-tests/compose.auth.yml down -v # tear down after
services: services:
web: web:
# This suite exercises only the Kratos session → JWT re-mint; it needs Kratos + Keto + bootstrap, # This suite exercises only the Kratos session → JWT re-mint; it needs Kratos + Keto + bootstrap,
@@ -38,7 +38,7 @@ services:
e2e: e2e:
build: build:
context: . context: .
dockerfile: Dockerfile.e2e dockerfile: e2e-tests/Dockerfile
depends_on: depends_on:
web: web:
condition: service_healthy condition: service_healthy
@@ -48,4 +48,4 @@ services:
KRATOS_PUBLIC_URL: http://kratos:4433 KRATOS_PUBLIC_URL: http://kratos:4433
command: ["npx", "playwright", "test", "auth-refresh.spec.ts"] command: ["npx", "playwright", "test", "auth-refresh.spec.ts"]
volumes: volumes:
- ./e2e/artifacts:/e2e/artifacts - ./e2e-tests/artifacts:/e2e-tests/artifacts
@@ -4,8 +4,8 @@
# runs against the *plain* `docker compose up` topology and drives the browser on the HOST network, so # runs against the *plain* `docker compose up` topology and drives the browser on the HOST network, so
# it sees http://localhost:3000 (web) and http://127.0.0.1:4433 (Kratos public) exactly as a host # it sees http://localhost:3000 (web) and http://127.0.0.1:4433 (Kratos public) exactly as a host
# browser does. Merge the dev override so the live stack is byte-for-byte `docker compose up`: # browser does. Merge the dev override so the live stack is byte-for-byte `docker compose up`:
# docker compose -f compose.yml -f compose.override.yml -f compose.e2e-devstack.yml run --build --rm e2e # docker compose -f compose.yml -f compose.override.yml -f e2e-tests/compose.devstack.yml run --build --rm e2e
# docker compose -f compose.yml -f compose.override.yml -f compose.e2e-devstack.yml down -v # tear down # docker compose -f compose.yml -f compose.override.yml -f e2e-tests/compose.devstack.yml down -v # tear down
services: services:
web: web:
# Pin APP_URL so the regression is deterministic regardless of any APP_URL exported in the shell # Pin APP_URL so the regression is deterministic regardless of any APP_URL exported in the shell
@@ -34,7 +34,7 @@ services:
e2e: e2e:
build: build:
context: . context: .
dockerfile: Dockerfile.e2e dockerfile: e2e-tests/Dockerfile
command: ["npx", "playwright", "test", "devstack-login.spec.ts"] command: ["npx", "playwright", "test", "devstack-login.spec.ts"]
# Host network: reach the host-published ports (web 3000, Kratos public 4433) at the very # Host network: reach the host-published ports (web 3000, Kratos public 4433) at the very
# hostnames a user types — localhost / 127.0.0.1 — so the cross-host CSRF-cookie split reproduces. # hostnames a user types — localhost / 127.0.0.1 — so the cross-host CSRF-cookie split reproduces.
@@ -45,4 +45,4 @@ services:
environment: environment:
BASE_URL: http://localhost:3000 BASE_URL: http://localhost:3000
volumes: volumes:
- ./e2e/artifacts:/e2e/artifacts - ./e2e-tests/artifacts:/e2e-tests/artifacts
@@ -1,9 +1,9 @@
# Full browser E2E — the real Playwright UI flow against the live stack: password + # Full browser E2E — the real Playwright UI flow against the live stack: password +
# mocked-SSO login, menu filtering by role, users/groups/roles CRUD, a plugin page, logout. A tiny # mocked-SSO login, menu filtering by role, users/groups/roles CRUD, a plugin page, logout. A tiny
# same-origin gateway (proxy, e2e/proxy.mjs) fronts web + Kratos on one host so the browser's cookies # same-origin gateway (proxy, e2e-tests/proxy.mjs) fronts web + Kratos on one host so the browser's cookies
# round-trip (ory/kratos/e2e-proxy.yml points Kratos at it); a mock OIDC provider backs the SSO test. # round-trip (ory/kratos/e2e-proxy.yml points Kratos at it); a mock OIDC provider backs the SSO test.
# docker compose -f compose.yml -f compose.e2e-full.yml run --build --rm e2e # docker compose -f compose.yml -f e2e-tests/compose.full.yml run --build --rm e2e
# docker compose -f compose.yml -f compose.e2e-full.yml down -v # tear down after # docker compose -f compose.yml -f e2e-tests/compose.full.yml down -v # tear down after
services: services:
web: web:
# First-party + SSO flows need Kratos + Keto + bootstrap, not Hydra — drop it so the stack is # First-party + SSO flows need Kratos + Keto + bootstrap, not Hydra — drop it so the stack is
@@ -60,14 +60,14 @@ services:
ISSUER: http://mock-oidc:9000 ISSUER: http://mock-oidc:9000
SSO_EMAIL: sso-user@plainpages.local SSO_EMAIL: sso-user@plainpages.local
volumes: volumes:
- ./e2e/mock-oidc.mjs:/mock-oidc.mjs:ro - ./e2e-tests/mock-oidc.mjs:/mock-oidc.mjs:ro
healthcheck: healthcheck:
test: ["CMD", "wget", "-q", "-O", "-", "http://localhost:9000/.well-known/openid-configuration"] test: ["CMD", "wget", "-q", "-O", "-", "http://localhost:9000/.well-known/openid-configuration"]
interval: 2s interval: 2s
timeout: 4s timeout: 4s
retries: 15 retries: 15
# Same-origin gateway: Kratos-owned paths → kratos, everything else → web (e2e/proxy.mjs). # Same-origin gateway: Kratos-owned paths → kratos, everything else → web (e2e-tests/proxy.mjs).
proxy: proxy:
image: node:24.16.0-alpine3.24 image: node:24.16.0-alpine3.24
command: ["node", "/proxy.mjs"] command: ["node", "/proxy.mjs"]
@@ -78,7 +78,7 @@ services:
KRATOS_URL: http://kratos:4433 KRATOS_URL: http://kratos:4433
WEB_URL: http://web:3000 WEB_URL: http://web:3000
volumes: volumes:
- ./e2e/proxy.mjs:/proxy.mjs:ro - ./e2e-tests/proxy.mjs:/proxy.mjs:ro
healthcheck: healthcheck:
test: ["CMD", "wget", "-q", "-O", "-", "http://localhost/public/css/styles.css"] test: ["CMD", "wget", "-q", "-O", "-", "http://localhost/public/css/styles.css"]
interval: 2s interval: 2s
@@ -88,7 +88,7 @@ services:
e2e: e2e:
build: build:
context: . context: .
dockerfile: Dockerfile.e2e dockerfile: e2e-tests/Dockerfile
command: ["npx", "playwright", "test", "full-flow.spec.ts"] command: ["npx", "playwright", "test", "full-flow.spec.ts"]
depends_on: depends_on:
mock-oidc: mock-oidc:
@@ -99,4 +99,4 @@ services:
BASE_URL: http://proxy BASE_URL: http://proxy
KRATOS_ADMIN_URL: http://kratos:4434 KRATOS_ADMIN_URL: http://kratos:4434
volumes: volumes:
- ./e2e/artifacts:/e2e/artifacts - ./e2e-tests/artifacts:/e2e-tests/artifacts
@@ -3,8 +3,8 @@
# it via the Kratos session and accepts. Runs against the real stack (Postgres + Kratos + Keto + # it via the Kratos session and accepts. Runs against the real stack (Postgres + Kratos + Keto +
# Hydra + bootstrap + web). The runner drives the flow over HTTP (fetch, manual cookies), so it # Hydra + bootstrap + web). The runner drives the flow over HTTP (fetch, manual cookies), so it
# reaches the Ory services by their compose-network names. # reaches the Ory services by their compose-network names.
# docker compose -f compose.yml -f compose.e2e-oauth.yml run --build --rm e2e # docker compose -f compose.yml -f e2e-tests/compose.oauth.yml run --build --rm e2e
# docker compose -f compose.yml -f compose.e2e-oauth.yml down -v # tear down after # docker compose -f compose.yml -f e2e-tests/compose.oauth.yml down -v # tear down after
services: services:
web: web:
# Dev throwaways are fine for the test stack; the runner hits web over http. # Dev throwaways are fine for the test stack; the runner hits web over http.
@@ -32,7 +32,7 @@ services:
e2e: e2e:
build: build:
context: . context: .
dockerfile: Dockerfile.e2e dockerfile: e2e-tests/Dockerfile
depends_on: depends_on:
web: web:
condition: service_healthy condition: service_healthy
@@ -43,4 +43,4 @@ services:
KRATOS_PUBLIC_URL: http://kratos:4433 KRATOS_PUBLIC_URL: http://kratos:4433
command: ["npx", "playwright", "test", "oauth-login.spec.ts"] command: ["npx", "playwright", "test", "oauth-login.spec.ts"]
volumes: volumes:
- ./e2e/artifacts:/e2e/artifacts - ./e2e-tests/artifacts:/e2e-tests/artifacts
@@ -1,9 +1,9 @@
# Playwright E2E. Brings up the app + a Playwright runner and exercises the live pages (design # Playwright E2E. Brings up the app + a Playwright runner and exercises the live pages (design
# system, theme switch, mobile layout, CSRF, landing, 404, plugin gating) — Ory-free, so it's fast. # system, theme switch, mobile layout, CSRF, landing, 404, plugin gating) — Ory-free, so it's fast.
# docker compose -f compose.yml -f compose.e2e.yml run --build --rm e2e # docker compose -f compose.yml -f e2e-tests/compose.visual.yml run --build --rm e2e
# docker compose -f compose.yml -f compose.e2e.yml down -v # tear down after # docker compose -f compose.yml -f e2e-tests/compose.visual.yml down -v # tear down after
# --build rebuilds the runner (the image bakes in e2e/) so spec edits are picked up. # --build rebuilds the runner (the image bakes in e2e-tests/) so spec edits are picked up.
# Screenshots + HTML report land in ./e2e/artifacts/ (git-ignored). # Screenshots + HTML report land in ./e2e-tests/artifacts/ (git-ignored).
services: services:
web: web:
# The dashboard renders mock data — no Ory needed. Drop the base file's kratos/keto # The dashboard renders mock data — no Ory needed. Drop the base file's kratos/keto
@@ -24,8 +24,8 @@ services:
e2e: e2e:
build: build:
context: . context: .
dockerfile: Dockerfile.e2e dockerfile: e2e-tests/Dockerfile
# Just the Ory-free visual suite; the full-stack auth spec runs via compose.e2e-auth.yml. # Just the Ory-free visual suite; the full-stack auth spec runs via e2e-tests/compose.auth.yml.
command: ["npx", "playwright", "test", "visual.spec.ts"] command: ["npx", "playwright", "test", "visual.spec.ts"]
depends_on: depends_on:
web: web:
@@ -36,4 +36,4 @@ services:
# The committed dev tokenizer key — the spec signs a session JWT with it so the gated # The committed dev tokenizer key — the spec signs a session JWT with it so the gated
# dashboard renders; web verifies it with the same key (the file it mounts read-only). # dashboard renders; web verifies it with the same key (the file it mounts read-only).
- ./ory/kratos/tokenizer/jwks.json:/repo/jwks.json:ro - ./ory/kratos/tokenizer/jwks.json:/repo/jwks.json:ro
- ./e2e/artifacts:/e2e/artifacts - ./e2e-tests/artifacts:/e2e-tests/artifacts
@@ -12,7 +12,7 @@ import { expect, test } from "@playwright/test";
// browser URLs derive from it, and a real /error page replaces the 404. // browser URLs derive from it, and a real /error page replaces the 404.
// //
// This is faithful to the user's environment: the runner uses the host network // This is faithful to the user's environment: the runner uses the host network
// (compose.e2e-devstack.yml) against the plain `docker compose up` topology, so it sees // (e2e-tests/compose.devstack.yml) against the plain `docker compose up` topology, so it sees
// http://localhost:3000 (web) and http://127.0.0.1:4433 (Kratos public) exactly as a host browser // http://localhost:3000 (web) and http://127.0.0.1:4433 (Kratos public) exactly as a host browser
// does. The proxied full-flow suite can't catch this regression — it fronts web + Kratos on one origin. // does. The proxied full-flow suite can't catch this regression — it fronts web + Kratos on one origin.
const ADMIN_EMAIL = "admin@plainpages.local"; // seeded by bootstrap const ADMIN_EMAIL = "admin@plainpages.local"; // seeded by bootstrap
@@ -2,7 +2,7 @@ import { type Browser, type Page, expect, test } from "@playwright/test";
import { randomUUID } from "node:crypto"; import { randomUUID } from "node:crypto";
// Full browser E2E: the real Playwright UI against the live stack via the same-origin // Full browser E2E: the real Playwright UI against the live stack via the same-origin
// gateway (compose.e2e-full.yml) — the browser-UI login the earlier full-stack suites deferred here. // gateway (e2e-tests/compose.full.yml) — the browser-UI login the earlier full-stack suites deferred here.
// Coverage is the test titles below, plus the standalone SSO test. // Coverage is the test titles below, plus the standalone SSO test.
// //
// Runs on a fresh stack (`down -v` after, like the other full-stack suites). The serial admin // Runs on a fresh stack (`down -v` after, like the other full-stack suites). The serial admin
@@ -1,7 +1,7 @@
import { defineConfig, devices } from "@playwright/test"; import { defineConfig, devices } from "@playwright/test";
// Visual + functional checks against the live app (the `web` compose service, BASE_URL). Run via // Visual + functional checks against the live app (the `web` compose service, BASE_URL). Run via
// compose.e2e.yml. Parallel per the project's E2E principle; deterministic colorScheme/viewport // e2e-tests/compose.visual.yml. Parallel per the project's E2E principle; deterministic colorScheme/viewport
// so the rendered design is stable across runs. // so the rendered design is stable across runs.
export default defineConfig({ export default defineConfig({
testDir: ".", testDir: ".",
+2 -2
View File
@@ -1,6 +1,6 @@
# Browser-E2E overlay (compose.e2e-full.yml) — merged after kratos.yml via a second `-c`. The # Browser-E2E overlay (e2e-tests/compose.full.yml) — merged after kratos.yml via a second `-c`. The
# full-flow suite drives the real browser, so web + Kratos must share one origin (the `proxy` # full-flow suite drives the real browser, so web + Kratos must share one origin (the `proxy`
# gateway, e2e/proxy.mjs). Point Kratos' public base_url and every self-service URL at that host so # gateway, e2e-tests/proxy.mjs). Point Kratos' public base_url and every self-service URL at that host so
# the flow action, the session cookie, and the after-login redirect all stay same-origin as the # the flow action, the session cookie, and the after-login redirect all stay same-origin as the
# browser sees them. The normal (10m) tokenizer TTL from kratos.yml is kept — no re-mint mid-test. # browser sees them. The normal (10m) tokenizer TTL from kratos.yml is kept — no re-mint mid-test.
serve: serve:
+1 -1
View File
@@ -1,4 +1,4 @@
# E2E overlay (compose.e2e-auth.yml) — merged after kratos.yml via a second `-c`. Two changes # E2E overlay (e2e-tests/compose.auth.yml) — merged after kratos.yml via a second `-c`. Two changes
# that let the auth-refresh suite exercise token timeout + re-mint in seconds: # that let the auth-refresh suite exercise token timeout + re-mint in seconds:
# 1. A very short session→JWT tokenizer TTL, so the JWT lapses while the Kratos session lives. # 1. A very short session→JWT tokenizer TTL, so the JWT lapses while the Kratos session lives.
# 2. A public base_url on the compose-network hostname, so the Playwright runner can drive the # 2. A public base_url on the compose-network hostname, so the Playwright runner can drive the
+3 -3
View File
@@ -11,14 +11,14 @@ import { readFileSync } from "node:fs";
const read = (p: string) => readFileSync(new URL(`../${p}`, import.meta.url), "utf8"); const read = (p: string) => readFileSync(new URL(`../${p}`, import.meta.url), "utf8");
const compose = read("compose.yml"); const compose = read("compose.yml");
const override = read("compose.override.yml"); const override = read("compose.override.yml");
const e2e = read("compose.e2e.yml"); const visual = read("e2e-tests/compose.visual.yml");
// compose.yml lists web first, postgres second — slice the web service block. // compose.yml lists web first, postgres second — slice the web service block.
const webBlock = compose.slice(compose.indexOf("\n web:"), compose.indexOf("\n postgres:")); const webBlock = compose.slice(compose.indexOf("\n web:"), compose.indexOf("\n postgres:"));
test("every image is pinned to an exact version, never a floating tag", () => { test("every image is pinned to an exact version, never a floating tag", () => {
// One scan over all compose files (postgres, the three Ory pairs, mailpit); web/e2e build. // One scan over all compose files (postgres, the three Ory pairs, mailpit); web/e2e build.
const images = [...[compose, override, e2e].join("\n").matchAll(/image:\s*(\S+)/g)].map((m) => m[1]!); const images = [...[compose, override, visual].join("\n").matchAll(/image:\s*(\S+)/g)].map((m) => m[1]!);
assert.ok(images.length >= 8, "scans the pinned images"); assert.ok(images.length >= 8, "scans the pinned images");
for (const img of images) { for (const img of images) {
assert.match(img.split(":").at(-1)!, /^v?\d+\.\d+/, `${img} pins a version-like tag`); assert.match(img.split(":").at(-1)!, /^v?\d+\.\d+/, `${img} pins a version-like tag`);
@@ -91,5 +91,5 @@ test("a one-shot bootstrap seeds the stack before web starts", () => {
test("the visual E2E does not drag in the Ory stack", () => { test("the visual E2E does not drag in the Ory stack", () => {
// web's Ory deps are reset for E2E (the dashboard is mock data — no Ory needed). // web's Ory deps are reset for E2E (the dashboard is mock data — no Ory needed).
assert.match(e2e, /depends_on:\s*!reset\b/, "E2E resets web's depends_on"); assert.match(visual, /depends_on:\s*!reset\b/, "E2E resets web's depends_on");
}); });