Files
atomic-design-poc/docs/project/backlog/WP-74-e2e-isolation.md
ehoandClaude Sonnet 5 ae7781efef
CI / changes (push) Successful in 8s
CI / lint (push) Successful in 1m9s
CI / frontend (push) Successful in 2m27s
CI / backend (push) Successful in 1m56s
CI / e2e (push) Successful in 3m16s
CI / semgrep (push) Successful in 1m7s
CI / api-client-drift (push) Successful in 1m50s
CI / storybook-a11y (push) Successful in 11m4s
docs: close WP-72..75, regenerate behaviour spec
Four close-outs and their README rows. The behaviour spec is regenerated
once here rather than per-track — it derives from every test name in the
repo, so any track running it would have conflicted with the other three.

Records two findings the arc surfaced but did not cause: the /brief/preview
staleness for non-DemoOwner identities (blocking per-spec identity isolation
in brief-v2.spec.ts), and that WP-72/73 had to share a commit because both
edit Program.cs — separate execution waves prevented build collisions but
did not produce separable diffs.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-19 16:34:54 +02:00

4.3 KiB

WP-74 — E2E isolation without a new backend endpoint

Status: done (42f7bd6) Phase: 12 — DDD hardening

Why

The three Playwright specs shared one mutable backend and admitted it in their own comments ("Restart the backend between CI runs — a second run would see a leftover Concept"). A crashed mid-wizard run poisoned every subsequent run via CreateConcept's 409, and both mutating specs acted as the same identity (DocumentStore.DemoOwner), so any new state-touching spec would collide immediately.

Decisions (pre-made)

WP-70 recorded the fix as a dev-only seed endpoint. That premise was wrong, and exploration established why:

  • The DB path already routes through IConfiguration (Program.cs, Db.ConnectionString = GetConnectionString("AppDb") ?? …), so ConnectionStrings__AppDb as an env var gives a throwaway DB with zero backend change — the same trick TestWebApplicationFactory already uses per-test.
  • StubIdentityProvider already honours an X-Subject header; the only gap was that no FE interceptor sent one.
  • The backend has no IsDevelopment() gate anywhere (grep: zero hits), so a seed endpoint would have had to invent the codebase's first environment gate — a new security posture for no gain.

So: throwaway DB + a dev-only X-Subject interceptor. No new endpoint, no environment gate.

Acceptance criteria

  • npm run e2e passes twice back-to-back with no backend restart — the actual acceptance test, and the thing that failed before this WP.
  • X-Subject observed on a real request reaching the backend (X-Subject: 111222333 on GET /api/v1/uploads/categories), not merely wired.
  • Each new BSN elfproef-verified by script against the weights [9,8,7,6,5,4,3,2,-1].
  • No backend change, no new endpoint, no IsDevelopment() gate.
  • Committed port config still defaults to 4200 (verification used an override).

Notes on the two caveats

  • reuseExistingServer stays on. Flipping it to false would hard-fail npm run e2e for anyone already running the docker stack on 4200/5000 — a real local-workflow regression. The consequence (the throwaway DB only applies when Playwright itself spawns the backend; always true in CI) is documented in a comment on the webServer entry.
  • Unique DB filename per invocation, with global-setup.ts sweeping only prior runs' leftovers. A fixed name unlinked mid-run is only safe if SQLite's pool never reopens by path afterwards; under fullyParallel that risks silently recreating an empty, unmigrated DB.

Deviation: interceptors alone were not enough

Two hand-written call sites bypass Angular's interceptor chain (as CLAUDE.md documents) and needed X-Subject stamped explicitly:

  • libs/shared/src/upload/upload.adapter.ts's raw XHR upload — without this every uploaded document landed under DemoOwner, breaking submit for any other identity.
  • apps/ssp/.../letter-preview.adapter.ts's preview fetch (plus cache: 'no-store', correct regardless since the endpoint sends no Cache-Control).

Known gap (a real backend bug, not caused by this WP)

Under any BSN other than DemoOwner, GET /brief/preview returns a sent letter still carrying the draft watermark — while curl against the same backend at the same instant returns the correct frozen archive. Client caching was ruled out (no-store, then cache-busting query strings), the dev proxy was ruled out, and it reproduced across two BSNs and never for DemoOwner. This points at a staleness/race in BriefStore's SQLite read path.

brief-v2.spec.ts therefore keeps the shared zorgverlener identity — it still gains throwaway-DB repeatability, just not per-spec identity isolation. actors.ts reserves a briefOpsteller actor for whoever fixes the backend. Tracked as a follow-up below.

Verification

npm run e2e   # twice consecutively, no backend restart
npm run lint && npm run typecheck && npm test && npm run build

Note: port 4200 was held by an unrelated container on the dev machine, so verification ran with E2E_BASE_URL pointed at an alternate port. The committed default is unchanged.

Follow-ups

  • /brief/preview staleness for non-DemoOwner identities (above) — the blocker for giving brief-v2.spec.ts its own identity.