test(e2e): isolate runs and identities without a new backend endpoint (WP-74)
The three specs shared one mutable backend and said so in their own
comments ("Restart the backend between CI runs"). WP-70 recorded the fix as
a dev-only seed endpoint; it isn't needed. The DB path already routes
through IConfiguration, so playwright.config's webServer hands the backend a
throwaway SQLite file per invocation — the same trick TestWebApplicationFactory
already uses, with zero backend change. And StubIdentityProvider already
honoured X-Subject; the only gap was that nothing sent it. That matters
because the backend has no IsDevelopment() gate anywhere, so a seed endpoint
would have had to invent the codebase's first environment gate.
subjectInterceptor mirrors the existing roleInterceptor and is wired into the
same isDevMode()-only list. Interceptors alone were not enough: the raw XHR
upload and the hand-written letter-preview fetch bypass Angular's chain (as
CLAUDE.md documents), so both now stamp X-Subject explicitly — without that,
every uploaded document still landed under DemoOwner.
reuseExistingServer stays on: flipping it would break local runs for anyone
already serving the docker stack. Each run gets a unique DB filename and
global-setup sweeps only prior runs' leftovers — deleting a fixed path
mid-run risks SQLite silently recreating an empty, unmigrated file under
fullyParallel.
Verified: e2e passes twice back-to-back with no backend restart, and
X-Subject was observed on a real request, not merely wired.
brief-v2.spec.ts keeps the shared identity for now — see the KNOWN GAP note;
a backend staleness bug makes /brief/preview return a sent letter with the
draft watermark for any non-DemoOwner BSN. actors.ts reserves the actor for
whoever fixes it.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
+16
-4
@@ -7,10 +7,22 @@ import { Actors, loginAs } from './support/actors';
|
||||
// Preview assertions are content-type/body-level (text/html + watermark marker), not
|
||||
// pixel, per WP-28's decision.
|
||||
//
|
||||
// The backend persists to SQLite (WP-22) and is shared across runs: `/brief/reset`
|
||||
// covers the letter, but org templates have no reset endpoint, so this test restores
|
||||
// the org-template draft it edits (step 8) and never asserts an absolute version
|
||||
// number — only that it increased by exactly one.
|
||||
// This test mutates real state (a letter, keyed per-owner by `BriefStore.GetOrCreate`),
|
||||
// and WP-74 gives it a fresh throwaway backend DB every `npm run e2e` run, so a
|
||||
// leftover/in-progress letter from a PREVIOUS RUN is never an issue any more. It
|
||||
// deliberately still logs in as the shared `Actors.zorgverlener` rather than its own
|
||||
// BSN, though: giving it a distinct BSN (as `smoke.spec.ts` does) hit a real,
|
||||
// reproducible bug in this repo's own e2e run — `GET /brief/preview`'s sent-letter
|
||||
// response kept the DRAFT watermark under a non-`DemoOwner` `X-Subject`, even though
|
||||
// the outgoing request carried the right header and a direct `curl` against the same
|
||||
// backend at the same instant returned the correct, frozen archive. That points to a
|
||||
// backend-side staleness/race in `BriefStore`'s SQLite read path (see
|
||||
// `letter-preview.adapter.ts`'s "KNOWN GAP" note), out of WP-74's file scope to fix —
|
||||
// so this spec stays on the one identity that doesn't trip it, pending that backend
|
||||
// investigation. The org-template appearance is a SEPARATE, already-known gap: it's
|
||||
// NOT owner-keyed (there's exactly one, shared by every caller) and has no reset
|
||||
// endpoint, so this test still restores the org-template draft it edits (step 8) and
|
||||
// never asserts an absolute version number — only that it increased by exactly one.
|
||||
test('drafter composes → approver sends; admin republishes appearance', async ({ page }) => {
|
||||
await loginAs(page, Actors.zorgverlener);
|
||||
await expect(page).toHaveURL(/\/dashboard$/);
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import * as fs from 'node:fs';
|
||||
import * as os from 'node:os';
|
||||
import * as path from 'node:path';
|
||||
|
||||
/**
|
||||
* WP-74: `playwright.config.ts` gives the backend a UNIQUE throwaway SQLite file
|
||||
* per `npm run e2e` invocation (`<tmpdir>/big-register-e2e-<pid>-<timestamp>.db`),
|
||||
* so Playwright has no webServer teardown hook to delete it once a run ends — this
|
||||
* sweeps them up instead, at the START of the NEXT run.
|
||||
*
|
||||
* Safe regardless of run ordering: `globalSetup` always executes AFTER `webServer`
|
||||
* has already started (Playwright's task order, not something a config can flip),
|
||||
* so THIS run's own file (`process.env['E2E_DB_PATH']`, set by the config module —
|
||||
* same node process, so the assignment is visible here) is always excluded. Every
|
||||
* OTHER matching file belongs to an invocation whose `dotnet run` process has
|
||||
* already exited, so deleting it can't race a live connection.
|
||||
*/
|
||||
export default function globalSetup(): void {
|
||||
const mine = process.env['E2E_DB_PATH'];
|
||||
const dir = os.tmpdir();
|
||||
let entries: string[];
|
||||
try {
|
||||
entries = fs.readdirSync(dir);
|
||||
} catch {
|
||||
return; // best-effort cleanup — a missing/unreadable temp dir isn't this run's problem
|
||||
}
|
||||
|
||||
for (const name of entries) {
|
||||
if (!name.startsWith('big-register-e2e-')) continue;
|
||||
const base = name.replace(/(-shm|-wal)$/, '');
|
||||
if (mine && base === path.basename(mine)) continue; // never this run's own file
|
||||
try {
|
||||
fs.unlinkSync(path.join(dir, name));
|
||||
} catch {
|
||||
// best-effort — a file another leftover process still has open, or already
|
||||
// gone, is not worth failing this run's e2e suite over.
|
||||
}
|
||||
}
|
||||
}
|
||||
+7
-6
@@ -6,13 +6,14 @@ import { Actors, loginAs, SeedRefs } from './support/actors';
|
||||
// with zero policy questions, so the only required upload is identiteit), submit,
|
||||
// and see the real confirmation. Not a full wizard-coverage suite — see WP-19.
|
||||
//
|
||||
// The backend is in-memory and shared across runs; this test mutates real state
|
||||
// (creates a registratie application for the fixed demo identity). Restart the
|
||||
// backend between CI runs — a second run would see a leftover Concept/submitted
|
||||
// application on the dashboard, which this test doesn't assert against, but a
|
||||
// stricter future test might.
|
||||
// This test mutates real state (creates+submits a registratie application), so it
|
||||
// logs in as its own BSN (`registratieAanvrager`, WP-74) rather than the shared
|
||||
// `zorgverlener` — a leftover Concept from a previous run lands on THAT BSN's
|
||||
// dashboard, not this one's, so a rerun (or another spec) never sees it. The
|
||||
// backend itself also gets a fresh throwaway SQLite file per `npm run e2e`
|
||||
// invocation (`playwright.config.ts`), so even a from-scratch run starts clean.
|
||||
test('login → dashboard → registratie wizard → submitted', async ({ page }) => {
|
||||
await loginAs(page, Actors.zorgverlener);
|
||||
await loginAs(page, Actors.registratieAanvrager);
|
||||
|
||||
await expect(page).toHaveURL(/\/dashboard$/);
|
||||
await expect(page.getByRole('heading', { level: 1, name: 'Mijn overzicht' })).toBeVisible();
|
||||
|
||||
+42
-7
@@ -11,19 +11,54 @@ export interface Actor {
|
||||
}
|
||||
|
||||
/**
|
||||
* The demo identities seeded by the backend. Today there is exactly one seeded
|
||||
* citizen (`backend/src/BigRegister.Api/Data/SeedData.cs`'s `Person`/`Registration`,
|
||||
* whose BSN also matches `DocumentStore.DemoOwner`) — named for the ROLE it plays
|
||||
* in a spec, not its BSN, so a spec reads as "log in as the zorgverlener", not
|
||||
* "log in as 123456782".
|
||||
* The demo identities e2e specs log in as. The BRP/registration data every one of
|
||||
* them sees on the dashboard comes from static `SeedData` and is identity-independent
|
||||
* (only `DocumentStore.DemoOwner`'s owner-keyed Applications/Documents/Briefs differ
|
||||
* per BSN — see `subject.interceptor.ts`), so any elfproef-valid BSN works here; these
|
||||
* are just distinct, not otherwise special.
|
||||
*
|
||||
* Named for the ROLE the identity plays in a spec, not its BSN, so a spec reads as
|
||||
* "log in as the zorgverlener", not "log in as 123456782". **A spec that mutates
|
||||
* owner-keyed state (creates a Concept, composes a brief, …) should use its own
|
||||
* BSN** (WP-74) — `subject.interceptor.ts` stamps it as `X-Subject`, so two specs
|
||||
* sharing a BSN would collide on the same backend rows across the same run and
|
||||
* across reruns. A read-only spec (nothing created/submitted) can keep using
|
||||
* `zorgverlener`.
|
||||
*
|
||||
* `briefOpsteller` is the one exception, currently unused: `brief-v2.spec.ts` stays
|
||||
* on `zorgverlener` despite mutating state, because giving it its own BSN tripped a
|
||||
* real backend bug (a stale/watermarked `GET /brief/preview` response for the SENT
|
||||
* letter under a non-`DemoOwner` owner — see that spec's header comment and
|
||||
* `letter-preview.adapter.ts`'s "KNOWN GAP" note). Kept defined, not deleted, so
|
||||
* whoever fixes that backend issue has the identity ready to switch the spec onto.
|
||||
*
|
||||
* Every BSN below passed the elfproef (`libs/shared/src/kernel/bsn.ts`'s checksum) —
|
||||
* required, or `DigidAdapter.authenticate` rejects it and login never completes:
|
||||
* 123456782 ✓ (9·1+8·2+7·3+6·4+5·5+4·6+3·7+2·8−1·2 = 154, 154 mod 11 = 0)
|
||||
* 111222333 ✓ (9·1+8·1+7·1+6·2+5·2+4·2+3·3+2·3−1·3 = 66, 66 mod 11 = 0)
|
||||
* 111111110 ✓ (9+8+7+6+5+4+3+2−0 = 44, 44 mod 11 = 0)
|
||||
*/
|
||||
export const Actors = {
|
||||
/** The one seeded citizen (`SeedData.cs`'s `Person`/`Registration`) — read-only
|
||||
specs, and (for now — see above) `brief-v2.spec.ts` too. */
|
||||
zorgverlener: { bsn: '123456782', wachtwoord: 'demo' },
|
||||
/** `smoke.spec.ts`'s own identity — it creates+submits a registratie-aanvraag. */
|
||||
registratieAanvrager: { bsn: '111222333', wachtwoord: 'demo' },
|
||||
/** Reserved for `brief-v2.spec.ts` once the backend bug above is fixed — not
|
||||
currently used by any spec. */
|
||||
briefOpsteller: { bsn: '111111110', wachtwoord: 'demo' },
|
||||
} as const satisfies Record<string, Actor>;
|
||||
|
||||
/** The shared DigiD-style mock login sequence every e2e spec starts from. */
|
||||
/**
|
||||
* The shared DigiD-style mock login sequence every e2e spec starts from. Navigating
|
||||
* to `/login?subject=<bsn>` (rather than plain `/login`) primes `subject.interceptor.ts`'s
|
||||
* sticky sessionStorage the same way `?role=` primes `roleInterceptor` (WP-33) — the
|
||||
* BSN typed into the form and the one the interceptor stamps as `X-Subject` are the
|
||||
* same value by construction, they just can't share a single read (see
|
||||
* `subject.interceptor.ts`'s doc comment for why not).
|
||||
*/
|
||||
export async function loginAs(page: Page, actor: Actor): Promise<void> {
|
||||
await page.goto('/login');
|
||||
await page.goto(`/login?subject=${actor.bsn}`);
|
||||
await page.getByLabel('BSN').fill(actor.bsn);
|
||||
await page.getByLabel('Wachtwoord').fill(actor.wachtwoord);
|
||||
await page.getByRole('button', { name: 'Inloggen met DigiD' }).click();
|
||||
|
||||
Reference in New Issue
Block a user