PMS-140: Add Playwright E2E suite against staging, run on every merge to main #90

Merged
nrupard merged 2 commits from feat/pms-140-e2e-playwright into main 2026-06-05 16:07:44 +02:00
Owner

Closes PMS-140.

What

Stands up a Playwright E2E suite under e2e/ that runs against the deployed staging instance (not a CI-built artifact) after every merge to main. This is phase 1: get the harness working and shake out flakiness. The run is post-merge and informational, not a merge gate (the gate is PMS-141).

Harness

Hybrid: tests/global.setup.ts logs in through the staging SPA in a real browser and saves the session to storageState; the request-context api project reuses it for /api/v1 calls. The auth-ui project logs in fresh so its logout assertion never invalidates the shared session.

Coverage (all four AC areas)

  • Auth (tests/auth.spec.ts): browser SPA login asserts an authenticated session; logout invalidates it.
  • OIDC (tests/oidc.spec.ts): request-context /oauth2/authorize -> code -> /oauth2/token -> /oauth2/userinfo -> refresh, PKCE, endpoints read from the live discovery document, code captured from the 302 Location without following it.
  • Tickets CRUD (tests/tickets.spec.ts): create/read/update/list in the E2E tenant.
  • Contacts + tenants (tests/contacts.spec.ts): company + contact CRUD, tenant-scoped smoke, and a cross-tenant leak canary (cross-cutting issue #8).

Test-data policy

Every record carries an embedded e2e-<epoch>-<runId>-<n> tag and lives only in the dedicated E2E tenant. global.teardown.ts deletes this run's records and sweeps e2e- residue older than 24h. Teardown is best-effort and never throws.

CI

.forgejo/workflows/e2e.yml triggers on push to main, installs Node + Chromium, runs a deploy-sync gate (polls GET /api/v1/version until it reports the pushed commit's 12-char git hash, 15s interval / 10-min timeout) so it never tests a stale deploy, runs the suite, and uploads playwright-report/ + test-results/ on failure.

Adds a just test-e2e recipe and e2e/README.md (required secrets, one-time staging provisioning, local runs).

Known limitations / notes for review

  • Tickets have no DELETE route (src/modules/tickets/routes.rs), so test-created tickets are not hard-deleted; their parent companies are deleted instead. Documented.
  • SPA login selectors in lib/login.ts are permissive (the form markup lives in mokosh-clients, not this repo) and may need tuning on first real run; the post-login proof is an API probe, which is DOM-independent.
  • OIDC code capture assumes the registered client returns a code for the already-authenticated session; if /oauth2/authorize redirects to a login screen instead, a dedicated E2E OIDC client is needed (called out in README.md and the test failure message).
  • Cross-tenant company canary runs only when E2E_FOREIGN_COMPANY_ID is provided; otherwise it skips with a reason. A foreign-tenant-id read check always runs.
  • The suite has not been executed against staging from here (no staging credentials/secrets in this environment). Typecheck (tsc --noEmit) and Playwright test discovery both pass; first green run requires the one-time staging provisioning + secrets per README.md.

Validation done

  • npm ci lockfile committed; npx tsc --noEmit clean; npx playwright test --list discovers all 9 tests across the 3 projects.

🤖 Generated with Claude Code

Closes PMS-140. ## What Stands up a Playwright E2E suite under `e2e/` that runs against the **deployed staging instance** (not a CI-built artifact) after every merge to `main`. This is phase 1: get the harness working and shake out flakiness. The run is **post-merge and informational, not a merge gate** (the gate is PMS-141). ## Harness Hybrid: `tests/global.setup.ts` logs in through the staging SPA in a real browser and saves the session to `storageState`; the request-context `api` project reuses it for `/api/v1` calls. The `auth-ui` project logs in fresh so its logout assertion never invalidates the shared session. ## Coverage (all four AC areas) - **Auth** (`tests/auth.spec.ts`): browser SPA login asserts an authenticated session; logout invalidates it. - **OIDC** (`tests/oidc.spec.ts`): request-context `/oauth2/authorize` -> code -> `/oauth2/token` -> `/oauth2/userinfo` -> refresh, PKCE, endpoints read from the live discovery document, code captured from the 302 Location without following it. - **Tickets CRUD** (`tests/tickets.spec.ts`): create/read/update/list in the E2E tenant. - **Contacts + tenants** (`tests/contacts.spec.ts`): company + contact CRUD, tenant-scoped smoke, and a cross-tenant leak canary (cross-cutting issue #8). ## Test-data policy Every record carries an embedded `e2e-<epoch>-<runId>-<n>` tag and lives only in the dedicated E2E tenant. `global.teardown.ts` deletes this run's records and sweeps `e2e-` residue older than 24h. Teardown is best-effort and never throws. ## CI `.forgejo/workflows/e2e.yml` triggers on push to `main`, installs Node + Chromium, runs a **deploy-sync gate** (polls `GET /api/v1/version` until it reports the pushed commit's 12-char git hash, 15s interval / 10-min timeout) so it never tests a stale deploy, runs the suite, and uploads `playwright-report/` + `test-results/` on failure. Adds a `just test-e2e` recipe and `e2e/README.md` (required secrets, one-time staging provisioning, local runs). ## Known limitations / notes for review - **Tickets have no DELETE route** (`src/modules/tickets/routes.rs`), so test-created tickets are not hard-deleted; their parent companies are deleted instead. Documented. - **SPA login selectors** in `lib/login.ts` are permissive (the form markup lives in mokosh-clients, not this repo) and may need tuning on first real run; the post-login proof is an API probe, which is DOM-independent. - **OIDC code capture** assumes the registered client returns a `code` for the already-authenticated session; if `/oauth2/authorize` redirects to a login screen instead, a dedicated E2E OIDC client is needed (called out in `README.md` and the test failure message). - **Cross-tenant company canary** runs only when `E2E_FOREIGN_COMPANY_ID` is provided; otherwise it skips with a reason. A foreign-tenant-id read check always runs. - The suite has **not been executed against staging from here** (no staging credentials/secrets in this environment). Typecheck (`tsc --noEmit`) and Playwright test discovery both pass; first green run requires the one-time staging provisioning + secrets per `README.md`. ## Validation done - `npm ci` lockfile committed; `npx tsc --noEmit` clean; `npx playwright test --list` discovers all 9 tests across the 3 projects. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
test(e2e): add Playwright suite against staging, run post-merge to main
All checks were successful
Check / fmt + clippy + compile + tests (pull_request) Successful in 1m25s
b4ccc9aa46
Stand up a TypeScript Playwright E2E suite under e2e/ that exercises the deployed staging instance end to end (per PMS-140: test the deployed app, not a CI-built artifact). Hybrid harness: a real browser logs in through the SPA and saves storageState, which request-context API tests reuse. Covers SPA login/session/logout, the OIDC authorization-code + PKCE token flow (authorize -> token -> userinfo -> refresh via the live discovery document), tickets CRUD, and contacts + tenants smoke with a cross-tenant leak canary.

Test-data policy: every created record carries an embedded e2e-<epoch>-<runId>-<n> tag, lives only in the dedicated E2E tenant, and is removed by global teardown; e2e residue older than 24h is swept. The tickets module exposes no DELETE route, so test-created tickets are left in place and their parent companies are deleted instead; the gap is documented in e2e/README.md.

CI workflow .forgejo/workflows/e2e.yml runs on push to main (post-merge, informational - not a gate). A deploy-sync step polls GET /api/v1/version until it reports the pushed commit hash (15s interval, 10-min timeout) so the suite never tests a stale deploy, then runs the suite and uploads the report + traces on failure. Adds a just test-e2e recipe and e2e/README.md documenting required secrets, one-time staging provisioning, and local runs. Email-dependent flows are intentionally excluded this phase.

#PMS-140

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
nrupard self-assigned this 2026-06-05 15:45:34 +02:00
test(e2e): fix auth path, require OIDC redirect, harden teardown and CI
All checks were successful
Check / fmt + clippy + compile + tests (pull_request) Successful in 1m55s
Create release / Create release from merged PR (pull_request) Has been skipped
01a4d7da02
The SPA keeps its access token in WASM memory (mokosh-clients/src/hooks/fetch.rs:189) rather than cookies or localStorage, so Playwright's `storageState` cookie/localStorage replay cannot authenticate the request-context API tests against `src/modules/auth/middleware.rs` (Bearer-only). The previous setup logged into the SPA in a browser and assumed cookie reuse would work; every `api`-project test would have 401'd against the deployment.

Replace the hybrid harness:

- `setup` project now calls `POST /api/v1/auth/login` directly and writes the returned `access_token` to `e2e/.auth/token.txt`. A new `lib/fixtures.ts` extends Playwright's `test` to load the token and inject `Authorization: Bearer ...` via `extraHTTPHeaders` on the `api` project's `request` fixture. Teardown reads the same file.
- `auth-ui` project drives the SPA form in a browser but asserts on URL transitions (login leaves `/login`, logout returns to it) instead of probing the API, which would fail for the same in-memory-token reason.

Other findings from PR review:

- `E2E_OIDC_REDIRECT_URI` no longer defaults to `E2E_BASE_URL`; defaulting to the bare host produced `invalid_redirect_uri` at /oauth2/authorize or /oauth2/token. Now required, with a documented sample value.
- OIDC test scope now includes `offline_access`, which the OP requires to issue a refresh_token (the test's last leg).
- Login form selectors are scoped to a `<form>` ancestor and prefer `data-testid` so other text inputs on the page can't capture credentials.
- Cross-tenant canary uses a fresh `randomUUID()` instead of the nil UUID so future seed migrations that touch zero IDs cannot turn the canary into a false-pass.
- CI workflow gets a `concurrency: e2e-staging` group (serial, never cancel) so back-to-back merges to main don't collide on the shared E2E tenant and per-email login rate limit; `fetch-depth: 0` dropped.
- `wait-for-deploy.mjs` only logs when the observed `git_hash` changes; previously each 15s poll spammed a duplicate line.
- `lib/run.ts` tries `GITEA_RUN_ID` first since this is Forgejo Actions; `GITHUB_RUN_ID` stays as the compat fallback.
- Teardown reads `env.baseURL` directly instead of `config.projects[0]?.use?.baseURL`, which silently shifted if projects got reordered.

#PMS-140
nrupard deleted branch feat/pms-140-e2e-playwright 2026-06-05 16:07:45 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
psa-systems/mokosh-server!90
No description provided.