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>
36 lines
2.2 KiB
TypeScript
36 lines
2.2 KiB
TypeScript
import { HttpInterceptorFn } from '@angular/common/http';
|
|
import { currentSubject } from './subject';
|
|
|
|
/**
|
|
* Dev-only (WP-74): stamps every API request with `X-Subject`, the BSN
|
|
* `StubIdentityProvider` (backend) resolves the caller's `ZorgverlenerCaller` from —
|
|
* every owner-keyed store (`ApplicationStore`, `DocumentStore`, `BriefStore`) reads
|
|
* off that resolved identity, so this is the seam that lets e2e specs log in as
|
|
* distinct citizens and mutate independent rows instead of all colliding on
|
|
* `DocumentStore.DemoOwner`. Scoped like `medewerker.interceptor.ts` (every
|
|
* `/api/v1/*` request, not an allow-list like `roleInterceptor`) — the identity
|
|
* middleware resolves a `CallerIdentity` for every request, not just some endpoints.
|
|
*
|
|
* **BSN source — a deliberate compromise, read before changing:** the "obvious"
|
|
* source would be the authenticated `Session.bsn` held by each app's own
|
|
* `SessionStore`, but `libs/shared` may not depend on an app-local `auth` context
|
|
* (the import-direction rule), and the one sanctioned cross-context seam —
|
|
* `SessionPort` (`@shared/application/session.port`) — deliberately exposes only
|
|
* `{ naam }`: `SessionStore`'s G1 comment is explicit that the BSN (a GDPR
|
|
* special-category identifier) is never persisted or otherwise handed outward, by
|
|
* design. Extending that port (or injecting `SessionStore` itself) would undo that
|
|
* boundary just to serve a dev/e2e convenience. So instead this reuses
|
|
* `role.interceptor.ts`'s own trick (see `subject.ts`, mirroring `role.ts`'s
|
|
* `currentRole()`): a `?subject=` seen in the URL is remembered in sessionStorage
|
|
* for the tab, and every later request reuses it. `e2e/support/actors.ts`'s
|
|
* `loginAs` sets it once per spec by navigating to `/login?subject=<bsn>` before
|
|
* filling in the login form. Outside e2e nothing ever sets `?subject=`, so no
|
|
* header is sent and the backend falls back to `DocumentStore.DemoOwner` exactly as
|
|
* before this WP.
|
|
*/
|
|
export const subjectInterceptor: HttpInterceptorFn = (req, next) => {
|
|
const subject = currentSubject();
|
|
if (!subject || !req.url.includes('/api/v1/')) return next(req);
|
|
return next(req.clone({ setHeaders: { 'X-Subject': subject } }));
|
|
};
|