test(e2e): serve the portal + walking-skeleton Playwright e2e (closes #68) #72

Merged
not merged 11 commits from feat/68-e2e into main 2026-07-13 13:20:58 +00:00
Contributor

What & why

S-08d — serves the self-service portal in the stack and adds the walking-skeleton Playwright
e2e
: mock DigiD login → submit → confirmation, in a real browser. This closes the walking skeleton
end-to-end (portal → BFF → domain → Flowable → ACL → OpenZaak; openbaar reads the projection) and
finishes Iteration 1.

Closes #68

Definition of Done

  • Conventional Commits (refs #68).
  • CI: new verify-e2e step in the verify-stack job; frontend lane still green (5 projects).
  • docker compose up serves the portal (self-service, health-checked, :8140).
  • Docs — docs/frontend-decisions.md + walking-skeleton demo note.

Notes for reviewers — and a big honesty caveat

I could not validate this slice locally — no browser here, and it needs the live stack + a
container-run browser. The nginx image builds and starts cleanly, the app builds with the runtime
config, and the frontend unit lane is green; but the e2e + serving are CI-first and may well need a
round or two.
Key design choices that de-risk it:

  • Same-origin serving: nginx serves the app and reverse-proxies /self-service + /openbaar to
    the BFF. The api-client uses relative URLs → no CORS, no BFF change. nginx resolves the BFF at
    request time so it starts before the BFF.
  • Issuer alignment (the long-standing gap): the e2e runs inside the compose network, so the
    browser reaches Keycloak as keycloak:8080 — the same authority the BFF validates against. This is
    what finally makes the real submit return 202 instead of 401.
  • Runtime config.json: one build; dev → localhost:8180, compose image → keycloak:8080.
  • No Playwright-image pinning: the e2e runs in node:24 and installs Chromium at runtime, copied
    in via docker cp (no host pollution), matching the repo's verify-* pattern.

Most likely CI fiddles: Keycloak login selectors, the redirect/CORS dance, or browser-install time.
I'll iterate on the logs.

🤖 Generated with Claude Code

## What & why **S-08d** — serves the self-service portal in the stack and adds the **walking-skeleton Playwright e2e**: mock DigiD login → submit → confirmation, in a real browser. This closes the walking skeleton end-to-end (portal → BFF → domain → Flowable → ACL → OpenZaak; openbaar reads the projection) and finishes **Iteration 1**. Closes #68 ## Definition of Done - [x] Conventional Commits (`refs #68`). - [x] CI: new **`verify-e2e`** step in the `verify-stack` job; `frontend` lane still green (5 projects). - [x] `docker compose up` serves the portal (`self-service`, health-checked, `:8140`). - [x] Docs — `docs/frontend-decisions.md` + walking-skeleton demo note. ## Notes for reviewers — and a big honesty caveat **I could not validate this slice locally** — no browser here, and it needs the live stack + a container-run browser. The nginx image builds and starts cleanly, the app builds with the runtime config, and the frontend unit lane is green; but **the e2e + serving are CI-first and may well need a round or two.** Key design choices that de-risk it: - **Same-origin serving:** nginx serves the app and reverse-proxies `/self-service` + `/openbaar` to the BFF. The api-client uses relative URLs → **no CORS**, no BFF change. nginx resolves the BFF at request time so it starts before the BFF. - **Issuer alignment (the long-standing gap):** the e2e runs **inside the compose network**, so the browser reaches Keycloak as `keycloak:8080` — the same authority the BFF validates against. This is what finally makes the real submit return **202** instead of 401. - **Runtime `config.json`:** one build; dev → `localhost:8180`, compose image → `keycloak:8080`. - **No Playwright-image pinning:** the e2e runs in `node:24` and installs Chromium at runtime, copied in via `docker cp` (no host pollution), matching the repo's `verify-*` pattern. Most likely CI fiddles: Keycloak login selectors, the redirect/CORS dance, or browser-install time. I'll iterate on the logs. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
not added 4 commits 2026-07-01 12:09:54 +00:00
The app loads /config.json at startup (main.ts) so the OIDC authority is set per
environment from one build; appConfig becomes a factory and derives redirectUrl +
secureApiOrigin from the app origin (same-origin as the BFF). A multi-stage
Dockerfile builds the app and serves it via nginx, reverse-proxying /self-service
+ /openbaar to the bff (relative URLs → no CORS); nginx resolves the BFF at request
time. The compose image bakes config.json with the keycloak:8080 authority so the
browser's token issuer matches the BFF (ADR-0010).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add the self-service nginx service (build the app image, depends_on bff healthy +
keycloak started, health-checked, host port 8140). Add it to WAIT_SVCS and the CI
log dump.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
tests/e2e Playwright spec drives DigiD login (jan-burger/test123) → submit →
confirmation against the compose-served portal. run-e2e-check.sh runs it inside the
compose network (node container, browser installed at runtime) so the token issuer
(keycloak:8080) matches the BFF authority (ADR-0010). Wired as verify-e2e (Makefile +
verify chain + a verify-stack CI step).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
docs(portal-self-service): serving/e2e decisions + walking-skeleton demo note (refs #68)
Some checks failed
CI / lint (pull_request) Successful in 1m5s
CI / build (pull_request) Successful in 52s
CI / unit (pull_request) Successful in 1m2s
CI / frontend (pull_request) Successful in 1m31s
CI / mutation (pull_request) Successful in 3m55s
CI / verify-stack (pull_request) Has been cancelled
d3f23a4da3
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
not added the type:slicearea:portal-self-servicearea:infra labels 2026-07-01 12:10:04 +00:00
not added 1 commit 2026-07-01 12:37:59 +00:00
fix(portal-self-service): health-check nginx over IPv4 (127.0.0.1) (refs #68)
Some checks failed
CI / lint (pull_request) Successful in 1m13s
CI / build (pull_request) Successful in 1m0s
CI / unit (pull_request) Successful in 1m6s
CI / frontend (pull_request) Failing after 7m11s
CI / mutation (pull_request) Successful in 3m59s
CI / verify-stack (pull_request) Failing after 7m47s
be016f920c
nginx listens on IPv4 only (listen 80), but 'localhost' inside the container resolves
to ::1 first, so the wget healthcheck got connection-refused and self-service never
went healthy — timing out the CI stack bring-up. Probe 127.0.0.1 instead.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
not added 1 commit 2026-07-13 10:52:01 +00:00
ci(portal-self-service): run Vitest ahead of the production build to stop worker-start timeout (refs #68)
Some checks failed
CI / lint (pull_request) Successful in 1m11s
CI / build (pull_request) Successful in 54s
CI / unit (pull_request) Successful in 1m3s
CI / frontend (pull_request) Successful in 2m0s
CI / mutation (pull_request) Successful in 4m5s
CI / verify-stack (pull_request) Failing after 10m20s
2e00ad38ba
The frontend lane ran `nx run-many -t lint test build`, so the ~5min
self-service production build shared nx's task pool with the Vitest test
worker. @angular/build:unit-test's Vitest worker has hard-coded 60s/90s
startup timeouts (not configurable); on a CPU-constrained CI runner the
concurrent build starved the worker and it failed with "Timeout waiting
for worker to respond" — flaky, since it passed on the prior commit.

Split the target into a light lint+test phase and a separate build phase
so tests get CPU and the worker starts well inside its window.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
not added 1 commit 2026-07-13 11:37:37 +00:00
fix(e2e): treat the http portal origin as secure so DigiD PKCE login works (refs #68)
Some checks failed
CI / lint (pull_request) Successful in 1m12s
CI / build (pull_request) Successful in 53s
CI / unit (pull_request) Successful in 1m3s
CI / frontend (pull_request) Successful in 1m47s
CI / mutation (pull_request) Successful in 4m2s
CI / verify-stack (pull_request) Failing after 7m51s
39923e0e68
The walking-skeleton e2e timed out waiting for the Keycloak login form
(`#username`). Root cause: in the compose network the portal is served over
plain HTTP on a non-localhost origin (http://self-service), which is not a
secure context, so Web Crypto (`crypto.subtle`) is undefined. angular-auth-
oidc-client needs SubtleCrypto to build the PKCE code challenge, so
`authorize()` threw ("Cannot read properties of undefined (reading 'digest')")
and the login redirect never fired.

Production serves the portal over HTTPS, where this works. Instead of
terminating TLS in the throwaway e2e stack, tell Chromium to treat the origin
as secure via --unsafely-treat-insecure-origin-as-secure. The flag is only
honoured by the full Chromium build (new headless), not Playwright's default
headless-shell, so pin channel: 'chromium'.

Verified against a minimal in-network stack (keycloak + self-service): login
redirect now reaches the Keycloak form, and the full login → token exchange →
authenticated portal renders with no console errors.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
not added 1 commit 2026-07-13 12:09:33 +00:00
fix(portal-self-service): attach the DigiD token to relative BFF calls (refs #68)
All checks were successful
CI / lint (pull_request) Successful in 1m12s
CI / build (pull_request) Successful in 51s
CI / unit (pull_request) Successful in 1m3s
CI / verify-stack (pull_request) Successful in 7m59s
CI / frontend (pull_request) Successful in 1m48s
CI / mutation (pull_request) Successful in 3m57s
0e6c7d2066
After login the submit silently did nothing: the confirmation ("...is
ontvangen...") never rendered because the POST to the BFF went out with no
Authorization header, so the BFF rejected it and the no-error-handler
subscribe left the page unchanged.

Root cause: angular-auth-oidc-client's interceptor attaches the token when
`req.url.startsWith(secureRoute)`. The api-client calls the BFF with RELATIVE
URLs (same-origin via the nginx proxy), so `req.url` is `/self-service/...` —
but secureRoutes was configured as the app ORIGIN (`http://self-service`),
which a relative URL never starts with. No match → no token.

Configure secureRoutes with the relative `/self-service/` prefix instead. The
unit test mocked the api-client, so only the walking-skeleton e2e exercises the
real token attachment — now green.

Verified against a focused stack (keycloak + self-service + real BFF + stub
domain): the submit now carries the bearer token, the BFF forwards to the
domain, and the portal shows the confirmation with the returned reference.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
not added 2 commits 2026-07-13 12:33:13 +00:00
Failing test: when postSelfServiceRegistrations errors, the page should show an
alert, not the confirmation, and keep the submit button available for retry.
Currently submit() has no error handler, so the rejection is swallowed and the
page silently stays put — exactly the failure mode that hid the missing-token
bug behind a 90s e2e timeout.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
feat(portal-self-service): surface submit failures with a retryable alert (refs #68)
All checks were successful
CI / build (pull_request) Successful in 54s
CI / lint (pull_request) Successful in 1m11s
CI / unit (pull_request) Successful in 1m0s
CI / frontend (pull_request) Successful in 1m49s
CI / mutation (pull_request) Successful in 4m3s
CI / verify-stack (pull_request) Successful in 7m42s
7e152e4432
Add an error branch to submit(): on a failed BFF call, set a `failed` signal,
re-enable the button, and render a role="alert" message so the user knows the
submit did not go through and can retry — instead of the click silently doing
nothing.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
not added 1 commit 2026-07-13 13:00:37 +00:00
test(portal-self-service): guard that the DigiD token attaches to relative BFF calls (refs #68)
All checks were successful
CI / lint (pull_request) Successful in 1m8s
CI / build (pull_request) Successful in 53s
CI / unit (pull_request) Successful in 58s
CI / frontend (pull_request) Successful in 2m10s
CI / mutation (pull_request) Successful in 3m58s
CI / verify-stack (pull_request) Successful in 7m48s
986e36bc7d
The token-attachment bug (secureRoutes set to the app origin, which a relative
api-client URL never matches) was only caught by the full-stack e2e. Add a fast
unit guard: drive the REAL angular-auth-oidc-client interceptor and the REAL
api-client against the production route value, faking only the config source and
the token storage. Asserts the bearer token rides the relative /self-service/
call and is withheld from the anonymous /openbaar/ call.

Extract the value to a shared SECURE_API_ROUTES constant so the test binds to
exactly what the app configures. Verified the guard fails (Authorization null)
if the value regresses to an origin.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
not merged commit 2a746736dc into main 2026-07-13 13:20:58 +00:00
Sign in to join this conversation.