diff --git a/Makefile b/Makefile index 0fb9b47..62cc4a0 100644 --- a/Makefile +++ b/Makefile @@ -3,8 +3,8 @@ # The workspace root delegates to these (see ../Makefile). .PHONY: init env-check dev-utils dev dev-local dev-lan rebuild-frontend up down update install dev-backend dev-frontend \ - generate-client sync-example test test-backend test-checks test-frontend soak bench-startup lint lint-backend \ - lint-frontend format-frontend umami build docs docs-serve clean help + generate-client sync-example test test-backend test-checks test-stack test-frontend soak \ + bench-startup lint lint-backend lint-frontend format-frontend umami build docs docs-serve clean help COMPOSE_ROOT := $(CURDIR) # Explicit project name keeps this stack isolated from the sibling website @@ -25,6 +25,13 @@ COMPOSE_LOCAL := $(COMPOSE_DEV) -f docker/compose.local.yml # Same stack, with the frontend also published on APP_PORT and proxying the API # there, for clients that cannot resolve app.$(DOMAIN). COMPOSE_LAN := $(COMPOSE_LOCAL) -f docker/compose.lan.yml +# The frontend suite's own stack, a project of its own behind the local Traefik +# (compose.test.yml). The hostname is fixed, never read from a file or the +# command line, so the suite can only ever reach this stack. +TEST_DOMAIN := test.localhost +COMPOSE_TEST := DOMAIN=$(TEST_DOMAIN) STACK_NAME=fluksio-test TAG=test \ + docker compose -p fluksio-test --env-file $(COMPOSE_ROOT)/.env -f docker/compose.yml \ + -f docker/compose.dev.yml -f docker/compose.local.yml -f docker/compose.test.yml help: ## Show available targets @awk 'BEGIN{FS=":.*?## "} /^[a-zA-Z_-]+:.*?##/ {printf " \033[36m%-18s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST) @@ -141,6 +148,7 @@ update: ## Pull, rebuild using the layer cache, and recreate changed containers down: ## Stop all running containers -$(COMPOSE_LOCAL) down + -$(COMPOSE_TEST) down -$(COMPOSE_PROD_RUN) down # ── Development (local, no Docker) ─────────────────────────────── @@ -184,35 +192,38 @@ test-backend: ## Run backend tests (pytest + coverage) PW_VERSION = $(shell sed -n 's/.*"@playwright\/test": "[^0-9]*\([0-9.]*\)".*/\1/p' frontend/package.json | head -1) -# This suite creates and deletes flows, dashboards and users, so the hostname it -# is pointed at is not taken from any file: it is read off the running stack -# (the frontend container's own Traefik rule) and mapped onto the local Traefik -# by address, so DNS never decides. tests/guard.ts is the second line. The -# suite calls api. even on a LAN stack, so an api container recreated -# under another DOMAIN would answer every call with Traefik's 404. -test-frontend: ## Run frontend tests (Playwright e2e) against the local stack +# The suite creates and deletes flows, dashboards and users, so it runs against +# a stack of its own, never the dev instance. `up` only recreates what changed, +# and the stack is left running for the next run (and for a look at a failure +# on http://app.test.localhost). `make down` stops it. Its volumes are +# expendable: `docker compose -p fluksio-test down -v` starts it over. +test-stack: ## Start or update the frontend suite's own stack at http://app.test.localhost + @docker ps -q -f name='^fluksio-app-proxy-1$$' | grep -q . \ + || { echo " ✗ the local Traefik is not running, 'make dev' first"; exit 1; } + $(COMPOSE_TEST) up --build -d --wait redis backend frontend + +# The Playwright container, minus its network. tests/guard.ts refuses a public +# address on top of the fixed hostname. +PW_RUN = docker run --rm --ipc=host --user $$(id -u):$$(id -g) -e HOME=/tmp \ + -v $(COMPOSE_ROOT):/app -w /app/frontend \ + -e PLAYWRIGHT_BASE_URL=http://app.$(TEST_DOMAIN) \ + -e PLAYWRIGHT_API_URL=http://api.$(TEST_DOMAIN) \ + -e CI=$${CI:-1} \ + -e FIRST_SUPERUSER="$$(sed -n 's/^FIRST_SUPERUSER=//p' $(COMPOSE_ROOT)/.env | head -1)" \ + -e FIRST_SUPERUSER_PASSWORD="$$(sed -n 's/^FIRST_SUPERUSER_PASSWORD=//p' $(COMPOSE_ROOT)/.env | head -1)" +PW_IMAGE = mcr.microsoft.com/playwright:v$(PW_VERSION)-noble + +# The names are mapped onto the local Traefik by address, so DNS never decides. +# Twice, because Chromium pins *.localhost to loopback whatever /etc/hosts says. +test-frontend: test-stack ## Run frontend tests (Playwright e2e) against the test stack @ip=$$(docker network inspect proxy \ --format '{{range .Containers}}{{if eq .Name "fluksio-app-proxy-1"}}{{.IPv4Address}}{{end}}{{end}}' \ 2>/dev/null | cut -d/ -f1); \ - [ -n "$$ip" ] || { echo " ✗ proxy network or Traefik container not found — is the stack up?"; exit 1; }; \ - domain=$$(docker inspect fluksio-app 2>/dev/null \ - | grep -o 'Host(`app\.[^`]*`)' | head -1 | sed 's/Host(`app\.//;s/`)//'); \ - [ -n "$$domain" ] || { echo " ✗ no running app stack to test — 'make dev' first"; exit 1; }; \ - api=$$(docker inspect fluksio-api 2>/dev/null \ - | grep -o 'Host(`api\.[^`]*`)' | head -1 | sed 's/Host(`api\.//;s/`)//'); \ - [ "$$api" = "$$domain" ] || { echo " ✗ api container is on api.$${api:-?}, the app on app.$$domain — recreate the stack with 'make dev'"; exit 1; }; \ - docker run --rm --network proxy --ipc=host \ - --user $$(id -u):$$(id -g) -e HOME=/tmp \ - --add-host app.$$domain:$$ip --add-host api.$$domain:$$ip \ - -v $(COMPOSE_ROOT):/app -w /app/frontend \ - -e PLAYWRIGHT_BASE_URL=http://app.$$domain \ - -e PLAYWRIGHT_API_URL=http://api.$$domain \ - -e HOST_RESOLVER_RULES="MAP app.$$domain $$ip, MAP api.$$domain $$ip" \ - -e CI=$${CI:-1} \ - -e FIRST_SUPERUSER="$$(sed -n 's/^FIRST_SUPERUSER=//p' $(COMPOSE_ROOT)/.env | head -1)" \ - -e FIRST_SUPERUSER_PASSWORD="$$(sed -n 's/^FIRST_SUPERUSER_PASSWORD=//p' $(COMPOSE_ROOT)/.env | head -1)" \ - mcr.microsoft.com/playwright:v$(PW_VERSION)-noble \ - npx playwright test $(PLAYWRIGHT_ARGS) + [ -n "$$ip" ] || { echo " ✗ the local Traefik is not on the proxy network"; exit 1; }; \ + $(PW_RUN) --network proxy \ + --add-host app.$(TEST_DOMAIN):$$ip --add-host api.$(TEST_DOMAIN):$$ip \ + -e HOST_RESOLVER_RULES="MAP app.$(TEST_DOMAIN) $$ip, MAP api.$(TEST_DOMAIN) $$ip" \ + $(PW_IMAGE) npx playwright test $(PLAYWRIGHT_ARGS) # Load and chaos against a *running* stack, never part of `make test`: it # restarts this stack's containers. `SOAK_ARGS="--dry-run"` only looks. diff --git a/README.md b/README.md index e6ee9b9..1035146 100644 --- a/README.md +++ b/README.md @@ -66,7 +66,7 @@ make dev-frontend # Vite on :5173 ``` ```sh -make test # pytest + Playwright (the e2e half needs the stack up) +make test # pytest + Playwright (the e2e half on its own stack, behind make dev's Traefik) make lint # ruff + mypy + biome make generate-client # regenerate the frontend SDK from the OpenAPI schema ``` diff --git a/development.md b/development.md index a43215a..76582d2 100644 --- a/development.md +++ b/development.md @@ -129,12 +129,19 @@ SQLite file in a temp directory (deleted per session, so a run never touches dev data), its own superuser, and `ENVIRONMENT=local` with `DOMAIN=localhost`, so a checkout configured for a deployment cannot drag that deployment's configuration into the run. -**Frontend (e2e).** `make test-frontend` runs the suite inside the pinned -`mcr.microsoft.com/playwright:v-noble` image (version read from -`frontend/package.json`) on the `proxy` network, against a stack that must already be up. It -maps `app.$(DOMAIN)` and `api.$(DOMAIN)` onto the Traefik container's address twice, with -`--add-host` and with `HOST_RESOLVER_RULES`, because Chromium pins `*.localhost` to loopback -whatever `/etc/hosts` says. Narrow a run with +**Frontend (e2e).** `make test-frontend` runs the suite against a stack of its own, because +the suite creates and deletes flows, dashboards and users. `make test-stack`, which it runs +first, starts or updates the compose project `fluksio-test` (`docker/compose.test.yml` over the +local stack's files), with its own containers, volumes and image tags. The local Traefik serves +it on `app.test.localhost` and `api.test.localhost`, so `make dev` has to be up, in either +mode: the test stack never layers `compose.lan.yml` and publishes no port. It stays running +after a run for a look at a failure, `make down` stops it, and its volumes are expendable +(`docker compose -p fluksio-test down -v` starts it over). + +The suite runs inside the pinned `mcr.microsoft.com/playwright:v-noble` image +(version read from `frontend/package.json`) on the `proxy` network. It maps both names onto +the Traefik container's address twice, with `--add-host` and with `HOST_RESOLVER_RULES`, +because Chromium pins `*.localhost` to loopback whatever `/etc/hosts` says. Narrow a run with `make test-frontend PLAYWRIGHT_ARGS="--grep flows"`. The specs are `frontend/tests/*.spec.ts`; `playwright.config.ts` defines `setup` → diff --git a/docker/compose.test.yml b/docker/compose.test.yml new file mode 100644 index 0000000..9ad86d3 --- /dev/null +++ b/docker/compose.test.yml @@ -0,0 +1,23 @@ +# The frontend suite's own stack. `make test-stack` layers this on +# compose.local.yml under the project `fluksio-test`, with DOMAIN=test.localhost +# and STACK_NAME=fluksio-test. It shares the dev stack's Traefik and nothing +# else: its own containers, volumes, image tags and hostnames, so the flows, +# dashboards and users the suite creates never reach the dev instance's data. +services: + + # `container_name` ignores the project name, so the dev stack's names would + # collide. + redis: + container_name: !reset null + + backend: + container_name: !reset null + # compose.dev.yml also brings up the broker and the time-series database + # with it, which no spec talks to. + depends_on: !override + redis: + condition: service_healthy + restart: true + + frontend: + container_name: !reset null diff --git a/frontend/playwright.config.ts b/frontend/playwright.config.ts index 7795f7b..fd9f2c7 100644 --- a/frontend/playwright.config.ts +++ b/frontend/playwright.config.ts @@ -28,7 +28,7 @@ export default defineConfig({ /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ use: { /* Base URL to use in actions like `await page.goto('/')`. Defaults to the - integrated stack (`make dev`), the only origin the API allows CORS from. + suite's own stack (`make test-stack`), never the dev instance's data. Point PLAYWRIGHT_BASE_URL elsewhere to test another running server; the API origin follows it (see tests/config.ts). */ baseURL: appUrl, diff --git a/frontend/tests/config.ts b/frontend/tests/config.ts index 6af784a..8ab661f 100644 --- a/frontend/tests/config.ts +++ b/frontend/tests/config.ts @@ -29,7 +29,8 @@ export const firstSuperuserPassword = getEnvVar("FIRST_SUPERUSER_PASSWORD") * instance. Both origins come from the same place instead: whatever the browser * is pointed at is what teardown may write to. */ -export const appUrl = process.env.PLAYWRIGHT_BASE_URL || "http://app.localhost" +export const appUrl = + process.env.PLAYWRIGHT_BASE_URL || "http://app.test.localhost" export const apiUrl = process.env.PLAYWRIGHT_API_URL || apiOrigin(appUrl) diff --git a/frontend/tests/sweep.setup.ts b/frontend/tests/sweep.setup.ts index d142452..466c1d4 100644 --- a/frontend/tests/sweep.setup.ts +++ b/frontend/tests/sweep.setup.ts @@ -11,9 +11,9 @@ import { apiUrl, firstSuperuser, firstSuperuserPassword } from "./config.ts" * rather than after one is what makes this work at all: it happens even when * the run that made the mess died. * - * The suite runs against whatever stack is up, which is somebody's own - * instance with real flows in it, so the predicate has to be incapable of - * matching one. A name qualifies only if it has the shape every fixture has — + * `make test-frontend` runs against a stack of its own, but the suite can be + * pointed at any (PLAYWRIGHT_BASE_URL), and that may be somebody's own instance + * with real flows in it, so the predicate has to be incapable of matching one. A name qualifies only if it has the shape every fixture has — * `test__`, plus an optional `_` for a second * artefact of the same spec — and only if the timestamp it carries is an hour * old, which nothing the run now starting is about to create can be.