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:
+4
-1
@@ -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
@@ -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/
|
||||||
|
|||||||
@@ -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"]
|
|
||||||
@@ -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
@@ -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"
|
||||||
@@ -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).
|
||||||
|
|
||||||
|
|||||||
@@ -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: ".",
|
||||||
@@ -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
@@ -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
@@ -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");
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user