Compare commits

...
8 Commits
Author SHA1 Message Date
ehoandClaude Sonnet 5 c2e06cc8d7 docs(backlog): WP-30 status update — 5 of 6 items landed
CI / changes (push) Successful in 30s
CI / lint (push) Successful in 4m0s
CI / frontend (push) Successful in 4m42s
CI / backend (push) Successful in 2m27s
CI / e2e (push) Successful in 3m36s
CI / semgrep (push) Failing after 1m11s
CI / storybook-a11y (push) Successful in 8m38s
CI / api-client-drift (push) Successful in 1m48s
Records what's implemented (items 1/3/4/5/6), what's deliberately skipped
this round (item 2, blocked on act_runner access), and that the WP can't be
marked fully done until a real Gitea run confirms the CI-timing/path-filter
behavior this environment can't observe. npm run ci confirmed green locally.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-30 09:36:36 +02:00
ehoandClaude Sonnet 5 a0d8804a53 feat(backend): optional lean deployable image (WP-30 #5)
Multi-stage backend/Dockerfile (sdk build -> aspnet:10.0 runtime, ~312MB) +
docker-compose.prod.yml, additive only — not wired into CI or the existing
dev docker-compose.yml (which keeps the SDK image for dotnet run hot-reload).
New .dockerignore keeps the build context lean (node_modules alone is
~750MB) since the Dockerfile COPYs from the repo root to pick up
public/letter.css (WP-25's FE<->BE letter contract) as a sibling of backend/.

Verified for real: built the image, ran it, and curled a live
GET /api/v1/brief/preview against the running container — got back the
actual rendered letter HTML with letter.css inlined, confirming the
walk-up-from-BaseDirectory lookup resolves inside this image layout too.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-30 09:35:21 +02:00
ehoandClaude Sonnet 5 e7db69d8a9 perf(ci): path-filter jobs by which side changed (WP-30 #3)
New `changes` job (dorny/paths-filter, pinned to SHA) computes frontend/
backend outputs; every downstream job gates its real steps on the relevant
output(s) instead of being skipped as a whole job. Conservative "skip steps,
not jobs" variant: every job still runs and reports a status (checkout always
executes) even when its side is untouched, so a required-status-check never
waits on a job that never started — the tradeoff the WP itself flagged as the
open risk of this item. e2e/semgrep/api-client-drift gate on either side
(they exercise both). `.github/workflows/**` counts as both sides, so a CI
change always gets a full run. Validated with `actionlint` (0 issues) and a
local YAML parse; the actual skip behavior can only be confirmed on a real
Gitea PR run.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-30 09:28:55 +02:00
ehoandClaude Sonnet 5 e02e8ce058 perf(ci): split lint into its own fast-fail job (WP-30 #4)
New `lint` job (lint + format:check + check:tokens) runs in parallel with
`frontend`, reporting in ~5 min instead of waiting on the full
test:coverage/ng build --localize/npm audit chain. Depends on the item-1
node_modules cache (otherwise this would duplicate a full npm ci for no
speed benefit, per the WP's own note).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-30 09:26:41 +02:00
ehoandClaude Sonnet 5 e46b87b26d perf(ci): cache node_modules, skip npm ci on a hit (WP-30 #1)
actions/cache on node_modules keyed by package-lock.json hash, across all 4
npm-based jobs (frontend, storybook-a11y, e2e, api-client-drift). The if:
guard is what makes this a real speedup: npm ci deletes-then-reinstalls
unconditionally, so caching node_modules alone does nothing unless the
install step is skipped outright on a cache hit.

Unverifiable from this environment — CI timing needs a real Gitea run to
confirm; shipped as its own commit so a red run is easy to bisect/revert.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-30 09:26:05 +02:00
ehoandClaude Sonnet 5 526da76617 fix(ci): triage semgrep findings, make the gate blocking (WP-30 #6)
Local semgrep run found 25 findings (not the WP's remembered 27 — already-stale
by the time this ran): dependabot cooldown, npm min-release-age, every GitHub
Action pinned to a full commit SHA (dependabot's existing github-actions
ecosystem entry keeps these current), and 2 detect-non-literal-regexp findings
in e2e/create-ssp.mjs suppressed as false positives (non-attacker-controlled
input: a test's own captured version number, a local generator's CLI arg).
`semgrep scan` now runs with `--error`, a real blocking gate instead of
report-only.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-30 09:25:21 +02:00
ehoandClaude Sonnet 5 29257048b5 fix(format): prettier drift in 2 docs (pre-existing, caught by format:check)
No content change — markdown list-marker/line-wrap normalization only.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-30 09:24:53 +02:00
ehoandClaude Sonnet 5 5cb3e1a9f0 feat(zgw): docker OpenZaak integration-test harness (WP-54)
Opt-in docker-compose (postgres+redis+OpenZaak, no celery/nginx) +
bootstrap-catalogus.sh seed a real OpenZaak instance; OpenZaakIntegrationTests
(Category=Integration, excluded from default dotnet test/CI) proves the ZGW
seam against it for the first time. That live run caught a real bug:
ZgwHttpClient never sent Content-Crs/Accept-Crs headers, so every write would
412 against a spec-compliant OpenZaak — fixed alongside the harness.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-30 09:08:35 +02:00
21 changed files with 771 additions and 103 deletions
+12
View File
@@ -0,0 +1,12 @@
# WP-30: backend/Dockerfile does `COPY . .` from the repo root (needs public/letter.css as a
# sibling of backend/) — without this, that copy would drag in node_modules (~750MB), .git,
# and every build/output directory, defeating the point of a "lean" image.
.git
node_modules
dist
storybook-static
backend/**/bin
backend/**/obj
backend/**/bigregister.db*
backend/openzaak
documentation.json
+6
View File
@@ -5,13 +5,19 @@ updates:
schedule:
interval: weekly
open-pull-requests-limit: 10
cooldown:
default-days: 7
- package-ecosystem: nuget
directory: /backend
schedule:
interval: weekly
cooldown:
default-days: 7
- package-ecosystem: github-actions
directory: /
schedule:
interval: weekly
cooldown:
default-days: 7
+153 -26
View File
@@ -16,34 +16,111 @@ concurrency:
cancel-in-progress: true
jobs:
frontend:
# WP-30: computes which side changed so the jobs below can skip their real work on a
# narrow PR. Deliberately conservative ("skip steps, not jobs" — see each job's `if:`):
# every job below still runs and reports a status even when its side is untouched, just
# with checkout as its only step. This avoids a required-status-check ever waiting on a
# job that never started, at the cost of still paying job-startup overhead on a skip.
# `.github/workflows/**` counts as BOTH sides, so a CI change itself always gets a full run.
changes:
runs-on: ubuntu-latest
timeout-minutes: 15
timeout-minutes: 5
outputs:
frontend: ${{ steps.filter.outputs.frontend }}
backend: ${{ steps.filter.outputs.backend }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- uses: dorny/paths-filter@7b450fff21473bca461d4b92ce414b9d0420d706 # v4.0.2
id: filter
with:
filters: |
frontend:
- 'src/**'
- 'public/**'
- 'e2e/**'
- 'scripts/**'
- 'angular.json'
- 'package*.json'
- 'tsconfig*.json'
- '.storybook/**'
- 'eslint.config.mjs'
- '.dependency-cruiser.js'
- '.github/workflows/**'
backend:
- 'backend/**'
- '.github/workflows/**'
# WP-30: split out of `frontend` so lint/format/token failures report in ~1 min instead of
# waiting on the full test:coverage/ng build below — depends on the node_modules cache
# above landing first, else this duplicates a full npm ci for no reason.
lint:
needs: changes
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- if: needs.changes.outputs.frontend == 'true'
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: 24
cache: npm
- id: node-modules-cache
if: needs.changes.outputs.frontend == 'true'
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: node_modules
key: node-modules-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
- run: npm ci --prefer-offline --no-audit --no-fund
if: needs.changes.outputs.frontend == 'true' && steps.node-modules-cache.outputs.cache-hit != 'true'
- run: npm run lint
if: needs.changes.outputs.frontend == 'true'
- run: npm run format:check
if: needs.changes.outputs.frontend == 'true'
- run: npm run check:tokens
if: needs.changes.outputs.frontend == 'true'
frontend:
needs: changes
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- if: needs.changes.outputs.frontend == 'true'
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: 24
cache: npm
# WP-30: on a hit, npm ci is skipped entirely (not just faster) — caching node_modules
# alone doesn't help since npm ci always deletes-then-reinstalls unconditionally.
- id: node-modules-cache
if: needs.changes.outputs.frontend == 'true'
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: node_modules
key: node-modules-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
- run: npm ci --prefer-offline --no-audit --no-fund
if: needs.changes.outputs.frontend == 'true' && steps.node-modules-cache.outputs.cache-hit != 'true'
# Bounded-context + atomic-layer boundaries (WP-38, dependency-cruiser).
- run: npm run dep:check
if: needs.changes.outputs.frontend == 'true'
# Showcase snippets must match their real source regions (WP-39, no drift).
- run: npm run gen:snippets && git diff --exit-code src/app/showcase/snippets.generated.ts
- run: npm run format:check
- run: npm run check:tokens
if: needs.changes.outputs.frontend == 'true'
# Runs the full suite AND reports coverage (WP-46, report-only — no thresholds, so
# it can't fail on coverage; it still fails on a failing test, like `npm test` did).
- run: npm run test:coverage
if: needs.changes.outputs.frontend == 'true'
# --localize builds every configured locale (nl + en, angular.json's i18n
# block) in one pass; i18nMissingTranslation:"error" (angular.json) fails
# this step if messages.en.xlf is missing a unit the source (WP-20) gains.
- run: npx ng build --localize
if: needs.changes.outputs.frontend == 'true'
# The shipped bundle must stay clean; dev-only advisories are excluded.
- run: npm audit --omit=dev
if: needs.changes.outputs.frontend == 'true'
storybook-a11y:
needs: changes
# Axe runs against every story in the static build; a violation fails the build.
runs-on: ubuntu-latest
# Hard resource ceiling so a runaway test-storybook (one headless Chromium per Jest
@@ -62,39 +139,59 @@ jobs:
options: --cpus=2 --memory=4g --memory-swap=4g
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- if: needs.changes.outputs.frontend == 'true'
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: 24
cache: npm
- id: node-modules-cache
if: needs.changes.outputs.frontend == 'true'
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: node_modules
key: node-modules-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
- run: npm ci --prefer-offline --no-audit --no-fund
if: needs.changes.outputs.frontend == 'true' && steps.node-modules-cache.outputs.cache-hit != 'true'
# Cache the chromium download across runs; `install --with-deps` then only
# runs the (fast, idempotent) apt deps check on a hit.
- uses: actions/cache@v4
- if: needs.changes.outputs.frontend == 'true'
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: ~/.cache/ms-playwright
key: playwright-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
- run: npx playwright install --with-deps chromium
if: needs.changes.outputs.frontend == 'true'
- run: npm run build-storybook
if: needs.changes.outputs.frontend == 'true'
- run: npm run test-storybook:ci
if: needs.changes.outputs.frontend == 'true'
backend:
needs: changes
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
- uses: actions/setup-dotnet@v4
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- if: needs.changes.outputs.backend == 'true'
uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4.3.1
with:
dotnet-version: 10.0.x
- uses: actions/cache@v4
- if: needs.changes.outputs.backend == 'true'
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: ~/.nuget/packages
key: nuget-${{ runner.os }}-${{ hashFiles('**/*.csproj') }}
restore-keys: nuget-${{ runner.os }}-
- run: dotnet format backend/BigRegister.slnx --verify-no-changes
- run: dotnet test backend/BigRegister.slnx
if: needs.changes.outputs.backend == 'true'
# Category=Integration (WP-54, OpenZaakIntegrationTests) needs a live OpenZaak — opt-in,
# run manually against backend/openzaak/ (see its README), never in CI.
- run: dotnet test backend/BigRegister.slnx --filter "Category!=Integration"
if: needs.changes.outputs.backend == 'true'
e2e:
needs: changes
# Smoke-level Playwright run against the REAL FE+backend (WP-19) — a fresh
# runner checkout per run, so there's no bigregister.db (WP-22, gitignored)
# left over from a prior run to leak state in; the backend creates + migrates
@@ -107,28 +204,42 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- if: needs.changes.outputs.frontend == 'true' || needs.changes.outputs.backend == 'true'
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: 24
cache: npm
- uses: actions/setup-dotnet@v4
- if: needs.changes.outputs.frontend == 'true' || needs.changes.outputs.backend == 'true'
uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4.3.1
with:
dotnet-version: 10.0.x
- uses: actions/cache@v4
- if: needs.changes.outputs.frontend == 'true' || needs.changes.outputs.backend == 'true'
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: ~/.nuget/packages
key: nuget-${{ runner.os }}-${{ hashFiles('**/*.csproj') }}
restore-keys: nuget-${{ runner.os }}-
- id: node-modules-cache
if: needs.changes.outputs.frontend == 'true' || needs.changes.outputs.backend == 'true'
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: node_modules
key: node-modules-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
- run: npm ci --prefer-offline --no-audit --no-fund
- uses: actions/cache@v4
if: (needs.changes.outputs.frontend == 'true' || needs.changes.outputs.backend == 'true') && steps.node-modules-cache.outputs.cache-hit != 'true'
- if: needs.changes.outputs.frontend == 'true' || needs.changes.outputs.backend == 'true'
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: ~/.cache/ms-playwright
key: playwright-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
- run: npx playwright install --with-deps chromium
if: needs.changes.outputs.frontend == 'true' || needs.changes.outputs.backend == 'true'
- run: npm run e2e
if: needs.changes.outputs.frontend == 'true' || needs.changes.outputs.backend == 'true'
semgrep:
needs: changes
# SAST for both sides — replaces CodeQL, which is GitHub-only (its analyze step uploads
# SARIF to GitHub's code-scanning API) and can't run on this Gitea instance. Semgrep OSS
# is a plain CLI: no account, no external platform API. Findings print in the job log.
@@ -139,39 +250,55 @@ jobs:
# semgrep's deps (e.g. PyJWT) are already present as apt-managed packages, which pip cannot
# uninstall ("RECORD file not found") — this flag installs fresh without uninstalling, so it
# never touches the Debian copies. Don't drop it.
# ponytail: report-only for now (no `--error`, so the job stays green while the initial
# findings are triaged); flip to `--error` to make it a blocking gate. See WP-30.
# WP-30: initial findings triaged (dependabot cooldown, npm min-release-age, GH Actions
# pinned to SHA, 2 nosemgrep'd ReDoS false positives on non-attacker-controlled input) —
# `--error` below makes this a real blocking gate, not report-only.
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- run: python3 -m pip install --break-system-packages --ignore-installed semgrep
if: needs.changes.outputs.frontend == 'true' || needs.changes.outputs.backend == 'true'
# p/default = curated cross-language security (covers JS/TS); p/csharp = the backend.
# Anonymous registry fetch; --metrics=off disables telemetry (not `auto`, which uploads
# project metadata).
- run: semgrep scan --config p/default --config p/csharp --metrics=off
- run: semgrep scan --config p/default --config p/csharp --metrics=off --error
if: needs.changes.outputs.frontend == 'true' || needs.changes.outputs.backend == 'true'
api-client-drift:
needs: changes
# The committed typed client must match the backend OpenAPI doc.
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- if: needs.changes.outputs.frontend == 'true' || needs.changes.outputs.backend == 'true'
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: 24
cache: npm
- uses: actions/setup-dotnet@v4
- if: needs.changes.outputs.frontend == 'true' || needs.changes.outputs.backend == 'true'
uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4.3.1
with:
# 8.0 for the bundled NSwag runtime, 10.0 to build/emit the spec.
dotnet-version: |
8.0.x
10.0.x
- uses: actions/cache@v4
- if: needs.changes.outputs.frontend == 'true' || needs.changes.outputs.backend == 'true'
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: ~/.nuget/packages
key: nuget-${{ runner.os }}-${{ hashFiles('**/*.csproj') }}
restore-keys: nuget-${{ runner.os }}-
- id: node-modules-cache
if: needs.changes.outputs.frontend == 'true' || needs.changes.outputs.backend == 'true'
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: node_modules
key: node-modules-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
- run: npm ci --prefer-offline --no-audit --no-fund
if: (needs.changes.outputs.frontend == 'true' || needs.changes.outputs.backend == 'true') && steps.node-modules-cache.outputs.cache-hit != 'true'
- run: npm run gen:api
if: needs.changes.outputs.frontend == 'true' || needs.changes.outputs.backend == 'true'
- run: git diff --exit-code src/app/shared/infrastructure/api-client.ts backend/swagger.json
if: needs.changes.outputs.frontend == 'true' || needs.changes.outputs.backend == 'true'
+3
View File
@@ -51,3 +51,6 @@ storybook-static
/playwright-report
/blob-report
/playwright/.cache
# WP-54: bootstrap-catalogus.sh's own record of what it seeded into a local OpenZaak run
backend/openzaak/seeded.env
+4
View File
@@ -1,3 +1,7 @@
# @storybook/angular's peer range lags Angular 22; the builder works fine at runtime.
# ponytail: one flag beats pinning the whole app back to Angular 21.
legacy-peer-deps=true
# WP-30: wait 7 days before resolving newly-published package versions (semgrep
# npm-missing-minimum-release-age) — guards against a freshly-published malicious/unstable release.
min-release-age=7
+15
View File
@@ -0,0 +1,15 @@
# WP-30: lean deployable image (optional — not used by the dev demo, which keeps the SDK
# image in the root docker-compose.yml for `dotnet run` hot-reload). Build from the repo
# root: `docker build -f backend/Dockerfile .`
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
WORKDIR /src
COPY . .
RUN dotnet publish backend/src/BigRegister.Api -c Release -o /app
FROM mcr.microsoft.com/dotnet/aspnet:10.0
WORKDIR /app
COPY --from=build /app .
# LetterHtml.Render (WP-25) walks up from AppContext.BaseDirectory looking for a sibling
# public/letter.css (the FE⇄BE letter contract) — this keeps that lookup working here too.
COPY public ./public
ENTRYPOINT ["dotnet", "BigRegister.Api.dll"]
+74
View File
@@ -0,0 +1,74 @@
# OpenZaak integration harness (WP-54)
A real OpenZaak, for developing/testing the ZGW seam (`backend/src/BigRegister.Api/Zgw/`)
against something that isn't a fixture or a stub `HttpMessageHandler`. Deliberately **not**
part of the root `docker-compose.yml` and **not** wired into `npm run ci` / CI — see
[docs/reference/openzaak-integration.md](../../docs/reference/openzaak-integration.md) for the
full picture; this is just "how to run it".
## Bring it up
```bash
cd backend/openzaak
docker compose -f docker-compose.openzaak.yml up -d # postgres, redis, migrate+configure, OpenZaak
./bootstrap-catalogus.sh # seeds a catalogus/zaaktype/zaak to read back
```
`bootstrap-catalogus.sh` waits for OpenZaak to answer, then over plain REST + a hand-rolled
HS256 JWT (same shape as `ZgwTokenProvider.cs`, matching the `bigregister-test` client
`setup_configuration/data.yaml` creates): a catalogus, a published zaaktype ("Herregistratie
arts", with the statustypen/resultaattype/roltype OpenZaak requires before a zaaktype can be
published), and one zaak (`BIG-2026-000123`) with an initiator rol for the seeded BSN
(`111222333` — the same fixture BSN `OpenZaakZaakSourceTests.cs` uses). It writes what it
seeded to `seeded.env` (gitignored) and prints a summary.
**Not idempotent** — re-running against the same (already-seeded) instance fails on OpenZaak's
`domein`+`rsin` uniqueness constraint for the catalogus. Reset with:
```bash
docker compose -f docker-compose.openzaak.yml down -v && docker compose -f docker-compose.openzaak.yml up -d
```
## Run the integration test against it
```bash
cd backend
dotnet test --filter Category=Integration
```
`OpenZaakIntegrationTests.cs` points a `WebApplicationFactory<Program>` at
`Zgw:Enabled=true` + `http://localhost:8000` with the harness's credentials, hits
`GET /api/v1/admin/cases`, and asserts the seeded zaak comes back — through the real HTTP +
JWT + Catalogi-label-resolution path, not a mock. This test is tagged `Category=Integration`
and is **excluded** from the default `dotnet test` run and from CI (`ci.yml`,
`scripts/ci-local.sh` both filter `Category!=Integration`) — it only passes with this harness
up, so it never runs where the harness doesn't exist.
## Tear down
```bash
docker compose -f docker-compose.openzaak.yml down -v
```
## What's in here / what isn't
- `docker-compose.openzaak.yml` — postgres (postgis), redis, a one-shot `web-init` (runs
Django migrations then `setup_configuration` against `setup_configuration/data.yaml`), and
`web` (the OpenZaak API on `:8000`). Pinned to `openzaak/open-zaak:1.29.1`. No
celery/celery-beat/celery-flower/nginx — trimmed for a lean, fast-booting harness; add them
back only if a later WP needs a real async notification delivery round-trip here (WP-52's
webhook is already covered by fixture tests against no live instance).
`NOTIFICATIONS_DISABLED=true` is required, not optional: without it, OpenZaak 500s (and
**rolls back the whole create**) on any notified resource — see the compose file's comment.
- `setup_configuration/data.yaml` — the declarative, scripted alternative to clicking through
the Django admin (upstream's own documented `setup_configuration` CLI mechanism): creates the
one `bigregister-test` client (`heeft_alle_autorisaties: true` — this instance never exists
for anything but this harness, so there's no least-privilege boundary worth modeling).
- `bootstrap-catalogus.sh` — the business content (catalogus/zaaktype/zaak/…) `setup_configuration`
has no YAML for; every field value here was checked against OpenZaak's own OpenAPI spec and a
live run of this exact script, not guessed (two OpenZaak quirks it works around: a zaaktype
needs ≥1 resultaattype and 2 statustypen before it can be published, and its
`selectielijstklasse` and the zaaktype's `selectielijstProcestype` must reference the same
`procesType` on the public VNG selectielijst API).
- **Not here**: Documenten (DRC) / Notificaties (NRC) content — add if a later WP needs to prove
those round-trips against a live instance too (WP-51/52 are fixture-tested today).
+164
View File
@@ -0,0 +1,164 @@
#!/usr/bin/env bash
# WP-54 — seeds business content (catalogus/zaaktype/statustype/roltype/zaak/status/rol) into
# the OpenZaak harness started by docker-compose.openzaak.yml. `setup_configuration/data.yaml`
# only covers infra config (the JWTSecret + Applicatie); Catalogi/Zaken content has no
# declarative-YAML equivalent upstream, so this script does it the same way the BFF itself
# does at runtime — plain REST calls with a hand-rolled HS256 JWT (see ZgwTokenProvider.cs,
# mirrored here in bash+openssl so this script has no extra dependency beyond curl/openssl).
#
# Idempotent-ish: re-running creates duplicate catalogus/zaaktype rows (OpenZaak doesn't
# dedupe by name) — meant to be run once per fresh `docker compose up`, not repeatedly against
# a long-lived instance. Prints the seeded zaak's `identificatie` + `url` on success; also
# writes them to seeded.env (repo-ignored) for OpenZaakIntegrationTests.cs to assert against.
set -euo pipefail
cd "$(dirname "${BASH_SOURCE[0]}")"
BASE="http://localhost:8000"
CLIENT_ID="bigregister-test"
SECRET="bigregister-test-secret"
RSIN="123443210" # elfproef-valid RSIN, already used as the fixture Bronorganisatie
# in OpenZaakZaakSourceTests.cs — reused here for consistency.
BSN="111222333" # elfproef-valid BSN, already used as the fixture caller BSN.
ZAAK_REF="BIG-2026-000123"
b64url() { openssl base64 -A | tr '+/' '-_' | tr -d '='; }
jwt() {
local header='{"alg":"HS256","typ":"JWT"}'
local payload
payload=$(printf '{"iss":"%s","iat":%d,"client_id":"%s","user_id":"%s","user_representation":"%s"}' \
"$CLIENT_ID" "$(date +%s)" "$CLIENT_ID" "$CLIENT_ID" "bootstrap")
local h p signing_input sig
h=$(printf '%s' "$header" | b64url)
p=$(printf '%s' "$payload" | b64url)
signing_input="$h.$p"
sig=$(printf '%s' "$signing_input" | openssl dgst -sha256 -hmac "$SECRET" -binary | b64url)
printf '%s.%s' "$signing_input" "$sig"
}
# $1 = method, $2 = path, $3 = JSON body (optional)
oz() {
local method="$1" path="$2" body="${3:-}"
# Content-Crs/Accept-Crs: every ZGW write must declare a coordinate reference system even
# when no geometry is involved — OpenZaak 412s without it.
local args=(-sS -X "$method" -H "Authorization: Bearer $(jwt)" -H "Content-Type: application/json" \
-H "Content-Crs: EPSG:4326" -H "Accept-Crs: EPSG:4326")
[ -n "$body" ] && args+=(-d "$body")
local response
response=$(curl "${args[@]}" -w $'\n%{http_code}' "$BASE$path")
local http_code="${response##*$'\n'}"
local json="${response%$'\n'*}"
if [[ ! "$http_code" =~ ^2 ]]; then
echo "FAILED $method $path -> $http_code: $json" >&2
exit 1
fi
echo "$json"
}
echo "Waiting for OpenZaak..."
until curl -sS -o /dev/null -w '%{http_code}' "$BASE/catalogi/api/v1/catalogussen" | grep -q '^2\|^401\|^403'; do
sleep 2
done
echo "Creating catalogus..."
catalogus=$(oz POST /catalogi/api/v1/catalogussen "$(printf '{"domein":"BIGR","rsin":"%s","contactpersoonBeheerNaam":"BIG Register"}' "$RSIN")")
catalogus_url=$(echo "$catalogus" | python3 -c 'import json,sys; print(json.load(sys.stdin)["url"])')
echo " $catalogus_url"
echo "Creating zaaktype (concept)..."
zaaktype=$(oz POST /catalogi/api/v1/zaaktypen "$(python3 -c '
import json, sys
print(json.dumps({
"identificatie": "ZT-HERREG",
"omschrijving": "Herregistratie arts",
"vertrouwelijkheidaanduiding": "openbaar",
"doel": "Herregistratie in het BIG-register",
"aanleiding": "Aanvraag door de zorgverlener",
"indicatieInternOfExtern": "extern",
"handelingInitiator": "indienen",
"onderwerp": "Herregistratie",
"handelingBehandelaar": "behandelen",
"doorlooptijd": "P30D",
"opschortingEnAanhoudingMogelijk": False,
"verlengingMogelijk": False,
"publicatieIndicatie": False,
"productenOfDiensten": ["https://example.com/producten/herregistratie"],
"referentieproces": {"naam": "Herregistratie"},
"verantwoordelijke": "CIBG",
"catalogus": sys.argv[1],
"beginGeldigheid": "2026-01-01",
"versiedatum": "2026-01-01",
"besluittypen": [],
"gerelateerdeZaaktypen": [],
# Must belong to the same procestype as the resultaattype selectielijstklasse below
# (OpenZaak cross-checks this against the public VNG selectielijst API).
"selectielijstProcestype": "https://selectielijst.openzaak.nl/api/v1/procestypen/e1b73b12-b2f6-4c4e-8929-94f84dd2a57d",
}))
' "$catalogus_url")")
zaaktype_url=$(echo "$zaaktype" | python3 -c 'import json,sys; print(json.load(sys.stdin)["url"])')
echo " $zaaktype_url"
echo "Creating statustypen (publish needs a begin AND an end status)..."
statustype=$(oz POST /catalogi/api/v1/statustypen "$(printf '{"zaaktype":"%s","omschrijving":"Ontvangen","volgnummer":1}' "$zaaktype_url")")
echo " $(echo "$statustype" | python3 -c 'import json,sys; print(json.load(sys.stdin)["url"])')"
statustype_eind=$(oz POST /catalogi/api/v1/statustypen "$(printf '{"zaaktype":"%s","omschrijving":"Afgehandeld","volgnummer":2}' "$zaaktype_url")")
echo " $(echo "$statustype_eind" | python3 -c 'import json,sys; print(json.load(sys.stdin)["url"])')"
echo "Creating resultaattype (publish needs at least one)..."
# The two URLs below are real reference-list entries on the public VNG selectielijst API
# (selectielijst.openzaak.nl) — OpenZaak validates both by fetching them, same as it does
# for a zaaktype URL, so a made-up URL 404s here.
resultaattype=$(oz POST /catalogi/api/v1/resultaattypen "$(printf '{"zaaktype":"%s","omschrijving":"Afgehandeld","resultaattypeomschrijving":"https://selectielijst.openzaak.nl/api/v1/resultaattypeomschrijvingen/7cb315fb-4f7b-4a43-aca1-e4522e4c73b3","selectielijstklasse":"https://selectielijst.openzaak.nl/api/v1/resultaten/cc5ae4e3-a9e6-4386-bcee-46be4986a829","archiefnominatie":"blijvend_bewaren"}' "$zaaktype_url")")
echo " $(echo "$resultaattype" | python3 -c 'import json,sys; print(json.load(sys.stdin)["url"])')"
echo "Creating roltype (initiator)..."
roltype=$(oz POST /catalogi/api/v1/roltypen "$(printf '{"zaaktype":"%s","omschrijving":"Initiator","omschrijvingGeneriek":"initiator"}' "$zaaktype_url")")
echo " $(echo "$roltype" | python3 -c 'import json,sys; print(json.load(sys.stdin)["url"])')"
echo "Publishing zaaktype..."
zaaktype_uuid=$(echo "$zaaktype_url" | sed 's#.*/##')
oz POST "/catalogi/api/v1/zaaktypen/$zaaktype_uuid/publish" >/dev/null
echo "Creating zaak..."
zaak=$(oz POST /zaken/api/v1/zaken "$(python3 -c '
import json, sys
print(json.dumps({
"zaaktype": sys.argv[1],
"bronorganisatie": sys.argv[2],
"verantwoordelijkeOrganisatie": sys.argv[2],
"startdatum": "2026-07-28",
"identificatie": sys.argv[3],
}))
' "$zaaktype_url" "$RSIN" "$ZAAK_REF")")
zaak_url=$(echo "$zaak" | python3 -c 'import json,sys; print(json.load(sys.stdin)["url"])')
echo " $zaak_url"
echo "Creating status..."
statustype_url=$(echo "$statustype" | python3 -c 'import json,sys; print(json.load(sys.stdin)["url"])')
oz POST /zaken/api/v1/statussen "$(python3 -c '
import json, sys
print(json.dumps({"zaak": sys.argv[1], "statustype": sys.argv[2], "datumStatusGezet": "2026-07-28T12:00:00Z"}))
' "$zaak_url" "$statustype_url")" >/dev/null
echo "Creating rol (initiator, seeded BSN)..."
roltype_url=$(echo "$roltype" | python3 -c 'import json,sys; print(json.load(sys.stdin)["url"])')
oz POST /zaken/api/v1/rollen "$(python3 -c '
import json, sys
print(json.dumps({
"zaak": sys.argv[1],
"betrokkeneType": "natuurlijk_persoon",
"roltype": sys.argv[2],
"roltoelichting": "Initiator",
"betrokkeneIdentificatie": {"inpBsn": sys.argv[3]},
}))
' "$zaak_url" "$roltype_url" "$BSN")" >/dev/null
cat > seeded.env <<EOF
ZAAK_REFERENTIE=$ZAAK_REF
ZAAK_URL=$zaak_url
ZAAKTYPE_LABEL=Herregistratie arts
CALLER_BSN=$BSN
EOF
echo
echo "Seed complete. $ZAAK_REF ($zaak_url) — see seeded.env"
@@ -0,0 +1,72 @@
# WP-54 — a real OpenZaak to develop/test the ZGW seam against, kept OUT of the root
# docker-compose.yml on purpose (see backend/openzaak/README.md): OpenZaak is a full Django
# stack (postgres + redis), heavy compared to this repo's own FE+BFF, and nobody who isn't
# touching the ZGW slice should have to pull/boot it.
#
# ponytail: trimmed vs. open-zaak's own published compose — no celery/celery-beat/celery-flower
# (async notification delivery, never asserted by the integration test) and no nginx (the test
# hits web's port directly). Add them back only if a later WP needs an actual notification
# round-trip against this harness (NRC delivery is already covered by fixture tests, WP-52).
services:
db:
image: postgis/postgis:17-3.5
environment:
- POSTGRES_HOST_AUTH_METHOD=trust
- POSTGRES_DB=openzaak
- POSTGRES_USER=openzaak
healthcheck:
test: ['CMD-SHELL', 'pg_isready -U openzaak']
interval: 5s
timeout: 5s
retries: 10
redis:
image: redis:8
healthcheck:
test: ['CMD', 'redis-cli', 'ping']
interval: 5s
timeout: 5s
retries: 10
# One-shot: migrate the schema, then apply setup_configuration/data.yaml (JWTSecret +
# Applicatie for the bootstrap script below) — the documented, scripted alternative to
# clicking through the Django admin (see openzaak_config_cli in upstream docs).
web-init:
image: openzaak/open-zaak:1.29.1
environment: &app-env
DJANGO_SETTINGS_MODULE: openzaak.conf.docker
SECRET_KEY: wp-54-local-harness-not-for-prod
DB_HOST: db
DB_NAME: openzaak
DB_USER: openzaak
IS_HTTPS: 'no'
SITE_DOMAIN: localhost:8000
ALLOWED_HOSTS: localhost,127.0.0.1,web
CACHE_DEFAULT: redis:6379/0
CACHE_AXES: redis:6379/0
DISABLE_2FA: 'true'
CELERY_BROKER_URL: redis://redis:6379/0
CELERY_RESULT_BACKEND: redis://redis:6379/0
RUN_SETUP_CONFIG: 'true'
# No celery worker in this trimmed harness (see the top-of-file note) to actually
# deliver a notification — without this, OpenZaak 500s (and rolls back!) every create
# on a notified resource (zaaktype, zaak, ...) because NotificationsConfig has no
# client configured (see notifications_api_common.viewsets.NotificationMixin.notify).
NOTIFICATIONS_DISABLED: 'true'
command: /setup_configuration.sh
volumes:
- ./setup_configuration:/app/setup_configuration:ro
depends_on:
db:
condition: service_healthy
redis:
condition: service_healthy
web:
image: openzaak/open-zaak:1.29.1
environment: *app-env
ports:
- '8000:8000'
depends_on:
web-init:
condition: service_completed_successfully
@@ -0,0 +1,27 @@
# Applied by web-init (RUN_SETUP_CONFIG=true → `manage.py setup_configuration`, upstream's
# documented CLI config mechanism — see docker-compose.openzaak.yml) instead of the Django
# admin. Creates the ONE application the bootstrap script + integration test authenticate as.
#
# ponytail: heeft_alle_autorisaties (all scopes) rather than a granular per-component/scope
# list — this instance only ever exists for this harness/test, never a shared or prod
# OpenZaak, so there's no least-privilege boundary worth modeling here.
sites_config_enable: true
sites_config:
items:
- domain: localhost:8000
name: OpenZaak (WP-54 harness)
vng_api_common_credentials_config_enable: true
vng_api_common_credentials:
items:
- identifier: bigregister-test
secret: bigregister-test-secret
vng_api_common_applicaties_config_enable: true
vng_api_common_applicaties:
items:
- uuid: 5a09b3c9-6a54-4b2b-8f3c-1f9b6b6a3a01
client_ids:
- bigregister-test
label: BIG-register BFF (WP-54 test harness)
heeft_alle_autorisaties: true
@@ -38,5 +38,11 @@ internal sealed class ZgwHttpClient(HttpClient http, ZgwTokenProvider tokens)
{
req.Headers.Authorization = new AuthenticationHeaderValue("Bearer", caller is null ? tokens.Mint() : tokens.Mint(caller));
req.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
// Every ZGW request must declare a coordinate reference system, even when no geometry is
// involved (Zaak has an optional zaakgeometrie) — a real OpenZaak 412s ("Content-Crs
// header ontbreekt") without it. Only surfaced by WP-54's live harness: the fixture/stub
// tests never modelled this header, so this bug shipped unnoticed since WP-49/50.
req.Headers.Add("Accept-Crs", "EPSG:4326");
if (req.Content is not null) req.Content.Headers.Add("Content-Crs", "EPSG:4326");
}
}
@@ -0,0 +1,56 @@
using System.Net.Http.Json;
using BigRegister.Api.Contracts;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Testing;
namespace BigRegister.Tests;
/// <summary>
/// WP-54: the one test that proves the BFF actually talks to a REAL OpenZaak — auth accepted,
/// real response shapes, real pagination/zaaktype resolution — rather than the stub
/// HttpMessageHandler every other Zgw test (<see cref="ZgwZaakMapperTests"/>,
/// <see cref="OpenZaakZaakSourceTests"/>) uses. Requires the harness in <c>backend/openzaak/</c>
/// to be up and seeded first (see its README); tagged Category=Integration so it's excluded
/// from the default `dotnet test` run and from CI (`ci.yml`, `scripts/ci-local.sh` both filter
/// it out) — nobody without a live OpenZaak should see it fail.
///
/// Not an <see cref="IClassFixture{TFixture}"/> off <see cref="TestWebApplicationFactory"/>:
/// that fixture hardcodes <c>Zgw:Enabled=false</c> (offline default) for every other test class,
/// so this one builds its own <see cref="WebApplicationFactory{TEntryPoint}"/> layering the
/// harness's URLs/credentials (matching <c>backend/openzaak/setup_configuration/data.yaml</c>
/// and <c>bootstrap-catalogus.sh</c>) on top.
/// </summary>
[Trait("Category", "Integration")]
public class OpenZaakIntegrationTests
{
private static WebApplicationFactory<Program> Factory()
{
var dbPath = Path.Combine(Path.GetTempPath(), $"bigregister-oz-integration-{Guid.NewGuid():N}.db");
return new WebApplicationFactory<Program>().WithWebHostBuilder(builder => builder
.UseSetting("ConnectionStrings:AppDb", $"Data Source={dbPath}")
.UseSetting("Zgw:Enabled", "true")
.UseSetting("Zgw:ZrcBaseUrl", "http://localhost:8000/zaken/api/v1")
.UseSetting("Zgw:ZtcBaseUrl", "http://localhost:8000/catalogi/api/v1")
.UseSetting("Zgw:ClientId", "bigregister-test")
.UseSetting("Zgw:Secret", "bigregister-test-secret")
.UseSetting("Zgw:UserId", "bigregister-test")
.UseSetting("Zgw:UserRepresentation", "WP-54 integration test"));
}
[Fact]
public async Task Admin_cases_returns_the_seeded_zaak_mapped_through_real_HTTP_and_JWT()
{
using var factory = Factory();
using var client = factory.CreateClient();
client.DefaultRequestHeaders.Add("X-Role", "admin"); // CasesAdmin gate (cases:manage)
var cases = await client.GetFromJsonAsync<List<ApplicationSummaryDto>>("/api/v1/admin/cases");
Assert.NotNull(cases);
// bootstrap-catalogus.sh seeds exactly one zaak, identificatie BIG-2026-000123, under a
// zaaktype whose omschrijving is "Herregistratie arts" — see backend/openzaak/README.md.
var seeded = Assert.Single(cases!, c => c.Status.Referentie == "BIG-2026-000123");
Assert.Equal("Herregistratie arts", seeded.Type);
Assert.Equal("InBehandeling", seeded.Status.Tag);
}
}
+14
View File
@@ -0,0 +1,14 @@
# WP-30: optional lean deployable backend image — NOT used by the dev demo (docker-compose.yml
# keeps the SDK image there for `dotnet run` hot-reload; rewriting that one into a prod image
# would lose the bind-mount hot-reload it's for). This is an additive artifact only, not wired
# into CI. `docker compose -f docker-compose.prod.yml up --build` → Swagger at :5000/swagger.
services:
api:
build:
context: .
dockerfile: backend/Dockerfile
environment:
- ASPNETCORE_ENVIRONMENT=Production
- ASPNETCORE_URLS=http://+:8080
ports:
- '5000:8080'
+56 -56
View File
@@ -49,62 +49,62 @@ WP-19's own file), so it's a separate manual/CI step, not chained into the other
Gates land before the work they cover; each lint rule lands in the same WP as the fixes
for its existing violations, so every WP ends green.
| WP | Title | Phase | Status |
| ------------------------------------------------------ | ---------------------------------------------------------------------------------- | --------------------------- | ------ |
| [WP-01](WP-01-axe-ci-gate.md) | Axe-on-every-story CI gate | 0 · gates | done |
| [WP-02](WP-02-check-tokens.md) | Harden `check:tokens` + fix what it catches | 0 · gates | done |
| [WP-03](WP-03-contracts-purity.md) | Boundaries I: contracts purity + ApiClient confinement | 0 · gates | done |
| [WP-04](WP-04-ui-not-infrastructure.md) | Boundaries II: `ui ↛ infrastructure` + showcase sanction | 0 · gates | done |
| [WP-05](WP-05-parse-boundaries.md) | Parse-don't-validate closure + MDX | 1 · FP/DDD | done |
| [WP-06](WP-06-typed-async.md) | Generic async template contexts — kill `$any()` | 1 · FP/DDD | done |
| [WP-07](WP-07-brief-idioms.md) | Brief on the shared idioms + RemoteData MDX | 1 · FP/DDD | done |
| [WP-08](WP-08-store-idiom.md) | One store idiom + machine naming + TEA MDX | 1 · FP/DDD | done |
| [WP-09](WP-09-pure-logic.md) | Pure-logic closure: dates + missing command specs | 1 · FP/DDD | done |
| [WP-10](WP-10-button-fidelity.md) | CIBG button fidelity | 2 · CIBG | done |
| [WP-11](WP-11-markup-fidelity.md) | CIBG markup fidelity: application-link + absent-class triage | 2 · CIBG | done |
| [WP-12](WP-12-datablock.md) | CIBG Datablock for application data | 2 · CIBG | done |
| [WP-13](WP-13-cibg-gap-register.md) | CIBG-gap register + hygiene + MDX | 2 · CIBG | done |
| [WP-14](WP-14-storybook-taxonomy.md) | Storybook taxonomy reorg + Layers MDX | 3 · Storybook | done |
| [WP-15](WP-15-missing-stories.md) | Missing stories: shell + brief components | 3 · Storybook | done |
| [WP-16](WP-16-component-a11y.md) | Component a11y: description wiring + alert role | 4 · a11y | done |
| [WP-17](WP-17-app-a11y.md) | App-level a11y: route focus, template lint, WCAG checklist | 4 · a11y | done |
| [WP-18](WP-18-abac-capability-spine.md) | ABAC capability spine (Principal + capabilities, phase P1) | 5 · productie-volwassenheid | done |
| [WP-19](WP-19-e2e-smoke.md) | Playwright e2e smoke | 5 · productie-volwassenheid | done |
| [WP-20](WP-20-second-locale.md) | Second locale proof | 5 · productie-volwassenheid | done |
| [WP-21](WP-21-resilience-seams.md) | Resilience seams (correlation-id, idempotency, retry) | 5 · productie-volwassenheid | done |
| [WP-22](WP-22-durable-persistence.md) | Durable persistence (optional tier) | 5 · productie-volwassenheid | done |
| [WP-23](WP-23-org-template-backend.md) | Org-template backend + admin role | 6 · Brief v2 | done |
| [WP-24](WP-24-letter-canvas.md) | Letter canvas (edit on the letter) | 6 · Brief v2 | done |
| [WP-25](WP-25-letter-preview-html.md) | Server-rendered letter preview (HTML; PDF deferred) | 6 · Brief v2 | done |
| [WP-26](WP-26-org-template-editor.md) | Admin org-template editor | 6 · Brief v2 | done |
| [WP-27](WP-27-brief-ux-layer.md) | Brief UX layer (undo/redo, standaardbrief, diff) | 6 · Brief v2 | done |
| [WP-28](WP-28-brief-v2-demo-polish.md) | Brief v2 demo polish (scenarios, e2e, docs) | 6 · Brief v2 | done |
| [WP-29](WP-29-stamdata-beheer-editor.md) | Stamdata beheer editor (low-code, PR-emitting) | follow-on · ADR-0004 | done |
| [WP-30](WP-30-ci-perf-followups.md) | CI performance follow-ups (node_modules cache, runner image, path filters) | follow-on · CI/infra | todo |
| [WP-31](WP-31-shared-store-helpers.md) | Shared store helpers (ActionState/SaveState, history, debounced-save, RemoteData) | 7 · refinements | done |
| WP-32 | Undo/redo in the stamdata editor (folded into WP-31 — no separate file) | 7 · refinements | done |
| [WP-33](WP-33-dev-switchers.md) | In-app dev switchers (scenario + role) | 7 · refinements | done |
| [WP-34](WP-34-adres-phone-brp-readonly.md) | Adres: phone field + BRP address read-only | 7 · refinements | done |
| [WP-35](WP-35-one-concept-per-type.md) | One Concept per case type (server-enforced) | 7 · refinements | done |
| [WP-36](WP-36-admin-cases.md) | Admin cases page + admin delete | 7 · refinements | done |
| [WP-37](WP-37-dev-switcher-reset.md) | Dev-switcher reset fix (scenario/role URL param) | 8 · platform/DX/showcase | done |
| [WP-38](WP-38-dependency-graph-boundaries.md) | Dependency graph + declarative boundaries (visualize + enforce) | 8 · platform/DX/showcase | done |
| [WP-39](WP-39-showcase-snippets-animations.md) | Showcase: linked code snippets + teaching animations | 8 · platform/DX/showcase | done |
| [WP-40](WP-40-pii-kernel.md) | PII kernel: branded `Bsn` VO (elfproef) + masked-value atom | 8 · platform/DX/showcase | done |
| [WP-41](WP-41-persisted-authz-audit.md) | Persisted, queryable authz/PII-reveal audit (no PII) | 8 · platform/DX/showcase | done |
| [WP-42](WP-42-privacy-security-showcase.md) | Privacy & security showcase page (mask + no-PII log) | 8 · platform/DX/showcase | done |
| [WP-43](WP-43-scaffold-generators.md) | Runnable generators: value-object / form-machine (plop; ui-component/bff = skills) | 8 · platform/DX/showcase | done |
| [WP-44](WP-44-context-generator.md) | Runnable generator: `gen:context` | 8 · platform/DX/showcase | done |
| [WP-45](WP-45-create-ssp-generator.md) | `create-ssp` bootstrap generator (mechanise new-ssp) | 8 · platform/DX/showcase | done |
| [WP-46](WP-46-vitest-coverage.md) | Vitest coverage (report + report-only thresholds) | 8 · platform/DX/showcase | done |
| [WP-47](WP-47-feature-flags.md) | Runtime feature flags (catalog-in-code, admin toggle, FE+backend) | 8 · platform/DX/showcase | done |
| [WP-48](WP-48-stamdata-deletion-protection.md) | Stamdata deletion protection (CI referential gate + editor expire/warn) | 8 · platform/DX/showcase | done |
| [WP-49](WP-49-openzaak-zaken-read-seam.md) | OpenZaak zaken read seam (IZaakSource + ZGW client, config-gated, offline default) | 9 · OpenZaak/ZGW | done |
| [WP-50](WP-50-openzaak-create-zaak.md) | OpenZaak create-zaak (first write slice) | 9 · OpenZaak/ZGW | done |
| [WP-51](WP-51-openzaak-documenten.md) | OpenZaak Documenten (DRC) upload + zaak link | 9 · OpenZaak/ZGW | done |
| [WP-52](WP-52-openzaak-notificaties.md) | OpenZaak Notificaties (NRC) live status via webhook | 9 · OpenZaak/ZGW | done |
| [WP-53](WP-53-inbound-identity-and-citizen-scoping.md) | Inbound identity seam + citizen-scoping (per-request BSN, ZGW audit claims) | 9 · OpenZaak/ZGW | done |
| [WP-54](WP-54-openzaak-integration-harness.md) | Docker OpenZaak integration-test harness (opt-in, live round-trip) | 9 · OpenZaak/ZGW | todo |
| WP | Title | Phase | Status |
| ------------------------------------------------------ | ---------------------------------------------------------------------------------- | --------------------------- | ----------- |
| [WP-01](WP-01-axe-ci-gate.md) | Axe-on-every-story CI gate | 0 · gates | done |
| [WP-02](WP-02-check-tokens.md) | Harden `check:tokens` + fix what it catches | 0 · gates | done |
| [WP-03](WP-03-contracts-purity.md) | Boundaries I: contracts purity + ApiClient confinement | 0 · gates | done |
| [WP-04](WP-04-ui-not-infrastructure.md) | Boundaries II: `ui ↛ infrastructure` + showcase sanction | 0 · gates | done |
| [WP-05](WP-05-parse-boundaries.md) | Parse-don't-validate closure + MDX | 1 · FP/DDD | done |
| [WP-06](WP-06-typed-async.md) | Generic async template contexts — kill `$any()` | 1 · FP/DDD | done |
| [WP-07](WP-07-brief-idioms.md) | Brief on the shared idioms + RemoteData MDX | 1 · FP/DDD | done |
| [WP-08](WP-08-store-idiom.md) | One store idiom + machine naming + TEA MDX | 1 · FP/DDD | done |
| [WP-09](WP-09-pure-logic.md) | Pure-logic closure: dates + missing command specs | 1 · FP/DDD | done |
| [WP-10](WP-10-button-fidelity.md) | CIBG button fidelity | 2 · CIBG | done |
| [WP-11](WP-11-markup-fidelity.md) | CIBG markup fidelity: application-link + absent-class triage | 2 · CIBG | done |
| [WP-12](WP-12-datablock.md) | CIBG Datablock for application data | 2 · CIBG | done |
| [WP-13](WP-13-cibg-gap-register.md) | CIBG-gap register + hygiene + MDX | 2 · CIBG | done |
| [WP-14](WP-14-storybook-taxonomy.md) | Storybook taxonomy reorg + Layers MDX | 3 · Storybook | done |
| [WP-15](WP-15-missing-stories.md) | Missing stories: shell + brief components | 3 · Storybook | done |
| [WP-16](WP-16-component-a11y.md) | Component a11y: description wiring + alert role | 4 · a11y | done |
| [WP-17](WP-17-app-a11y.md) | App-level a11y: route focus, template lint, WCAG checklist | 4 · a11y | done |
| [WP-18](WP-18-abac-capability-spine.md) | ABAC capability spine (Principal + capabilities, phase P1) | 5 · productie-volwassenheid | done |
| [WP-19](WP-19-e2e-smoke.md) | Playwright e2e smoke | 5 · productie-volwassenheid | done |
| [WP-20](WP-20-second-locale.md) | Second locale proof | 5 · productie-volwassenheid | done |
| [WP-21](WP-21-resilience-seams.md) | Resilience seams (correlation-id, idempotency, retry) | 5 · productie-volwassenheid | done |
| [WP-22](WP-22-durable-persistence.md) | Durable persistence (optional tier) | 5 · productie-volwassenheid | done |
| [WP-23](WP-23-org-template-backend.md) | Org-template backend + admin role | 6 · Brief v2 | done |
| [WP-24](WP-24-letter-canvas.md) | Letter canvas (edit on the letter) | 6 · Brief v2 | done |
| [WP-25](WP-25-letter-preview-html.md) | Server-rendered letter preview (HTML; PDF deferred) | 6 · Brief v2 | done |
| [WP-26](WP-26-org-template-editor.md) | Admin org-template editor | 6 · Brief v2 | done |
| [WP-27](WP-27-brief-ux-layer.md) | Brief UX layer (undo/redo, standaardbrief, diff) | 6 · Brief v2 | done |
| [WP-28](WP-28-brief-v2-demo-polish.md) | Brief v2 demo polish (scenarios, e2e, docs) | 6 · Brief v2 | done |
| [WP-29](WP-29-stamdata-beheer-editor.md) | Stamdata beheer editor (low-code, PR-emitting) | follow-on · ADR-0004 | done |
| [WP-30](WP-30-ci-perf-followups.md) | CI performance follow-ups (node_modules cache, runner image, path filters) | follow-on · CI/infra | in-progress |
| [WP-31](WP-31-shared-store-helpers.md) | Shared store helpers (ActionState/SaveState, history, debounced-save, RemoteData) | 7 · refinements | done |
| WP-32 | Undo/redo in the stamdata editor (folded into WP-31 — no separate file) | 7 · refinements | done |
| [WP-33](WP-33-dev-switchers.md) | In-app dev switchers (scenario + role) | 7 · refinements | done |
| [WP-34](WP-34-adres-phone-brp-readonly.md) | Adres: phone field + BRP address read-only | 7 · refinements | done |
| [WP-35](WP-35-one-concept-per-type.md) | One Concept per case type (server-enforced) | 7 · refinements | done |
| [WP-36](WP-36-admin-cases.md) | Admin cases page + admin delete | 7 · refinements | done |
| [WP-37](WP-37-dev-switcher-reset.md) | Dev-switcher reset fix (scenario/role URL param) | 8 · platform/DX/showcase | done |
| [WP-38](WP-38-dependency-graph-boundaries.md) | Dependency graph + declarative boundaries (visualize + enforce) | 8 · platform/DX/showcase | done |
| [WP-39](WP-39-showcase-snippets-animations.md) | Showcase: linked code snippets + teaching animations | 8 · platform/DX/showcase | done |
| [WP-40](WP-40-pii-kernel.md) | PII kernel: branded `Bsn` VO (elfproef) + masked-value atom | 8 · platform/DX/showcase | done |
| [WP-41](WP-41-persisted-authz-audit.md) | Persisted, queryable authz/PII-reveal audit (no PII) | 8 · platform/DX/showcase | done |
| [WP-42](WP-42-privacy-security-showcase.md) | Privacy & security showcase page (mask + no-PII log) | 8 · platform/DX/showcase | done |
| [WP-43](WP-43-scaffold-generators.md) | Runnable generators: value-object / form-machine (plop; ui-component/bff = skills) | 8 · platform/DX/showcase | done |
| [WP-44](WP-44-context-generator.md) | Runnable generator: `gen:context` | 8 · platform/DX/showcase | done |
| [WP-45](WP-45-create-ssp-generator.md) | `create-ssp` bootstrap generator (mechanise new-ssp) | 8 · platform/DX/showcase | done |
| [WP-46](WP-46-vitest-coverage.md) | Vitest coverage (report + report-only thresholds) | 8 · platform/DX/showcase | done |
| [WP-47](WP-47-feature-flags.md) | Runtime feature flags (catalog-in-code, admin toggle, FE+backend) | 8 · platform/DX/showcase | done |
| [WP-48](WP-48-stamdata-deletion-protection.md) | Stamdata deletion protection (CI referential gate + editor expire/warn) | 8 · platform/DX/showcase | done |
| [WP-49](WP-49-openzaak-zaken-read-seam.md) | OpenZaak zaken read seam (IZaakSource + ZGW client, config-gated, offline default) | 9 · OpenZaak/ZGW | done |
| [WP-50](WP-50-openzaak-create-zaak.md) | OpenZaak create-zaak (first write slice) | 9 · OpenZaak/ZGW | done |
| [WP-51](WP-51-openzaak-documenten.md) | OpenZaak Documenten (DRC) upload + zaak link | 9 · OpenZaak/ZGW | done |
| [WP-52](WP-52-openzaak-notificaties.md) | OpenZaak Notificaties (NRC) live status via webhook | 9 · OpenZaak/ZGW | done |
| [WP-53](WP-53-inbound-identity-and-citizen-scoping.md) | Inbound identity seam + citizen-scoping (per-request BSN, ZGW audit claims) | 9 · OpenZaak/ZGW | done |
| [WP-54](WP-54-openzaak-integration-harness.md) | Docker OpenZaak integration-test harness (opt-in, live round-trip) | 9 · OpenZaak/ZGW | done |
Sequencing dependencies (stated in the WPs too): 01 before 10–15 (axe covers story churn);
03/04 before 05–09 (boundaries stop new violations during refactors); 06 before 07 (typed
@@ -1,6 +1,7 @@
# WP-30 — CI performance follow-ups
Status: todo
Status: in-progress (5 of 6 items implemented + committed; pending a real Gitea run to confirm —
see "Status update" below)
Phase: follow-on · CI/infra
## Why
@@ -51,11 +52,37 @@ runner image, set on the act_runner host).
tighter ruleset than `p/default` if the GitHub-Actions-policy rules aren't wanted), then add
`--error` to `semgrep scan` so it's a real gate.
## Status update (2026-07-30)
Items 1, 3, 4, 5, 6 implemented, each as its own commit (item 6 `526da76`, item 1 `e46b87b`,
item 4 `e02e8ce`, item 3 `e7db69d`, item 5 see `git log -- backend/Dockerfile`): triaged real
local semgrep findings (25, not the 27 this
file remembered — dependabot cooldown, npm min-release-age, every GH Action pinned to SHA, 2
nosemgrep'd ReDoS false positives) and flipped the gate to `--error`; `node_modules` cache
(skips `npm ci` entirely on a hit) across all 4 npm jobs; a new fast-fail `lint` job split out
of `frontend`; a `changes` job (`dorny/paths-filter`) gating every downstream job's real steps
(not the whole job — the safer "skip steps" variant, so a required-status-check never waits on
a job that never ran) on which side changed; an optional `backend/Dockerfile` +
`docker-compose.prod.yml` (additive, unused by CI or the dev demo).
**Item 2 (smaller runner image) deliberately skipped this round** — the real fix needs
act_runner admin access (unconfirmed), and the repo-only partial (`node:24-slim` on
`frontend`/`storybook-a11y`) conflicts with `storybook-a11y`'s deliberately-chosen
`node:24-bookworm` + memory-cap container (verified against a real OOM risk). Revisit once
act_runner access is confirmed.
**Cannot self-certify GREEN**: per this WP's own constraint, CI timing/behavior isn't
observable from the agent's environment. Everything above was checked as far as locally
possible (YAML parse, `actionlint` 0 issues, `npm run ci`, a real `docker build`) but the actual
speedup and the path-filter's interaction with any required-status-check config need a watched
Gitea run before this WP can be marked fully `done`.
## Acceptance criteria
- [ ] Each chosen item verified GREEN on a real Gitea run (watched, since it's not observable
from the agent env), landed as its own revertable commit.
- [ ] `npm run ci` still passes locally after any workflow/script change.
- [x] `npm run ci` still passes locally after any workflow/script change (confirmed 2026-07-30,
full run including backend `dotnet test`/`dotnet format` and both drift checks).
## Out of scope
@@ -115,8 +115,8 @@ session sync (CLAUDE.md out-of-scope list).
## Risks
- Missing a `DemoOwner` call site → a citizen sees another's data. Mitigated: grep gate (clean)
+ `ApplicationTests.Applications_are_scoped_to_the_caller_bsn` (two `X-Subject` identities,
HTTP end-to-end) proving neither sees the other's cases.
- `ApplicationTests.Applications_are_scoped_to_the_caller_bsn` (two `X-Subject` identities,
HTTP end-to-end) proving neither sees the other's cases.
- ZGW rol filter param name is exact and version-sensitive; asserted in
`OpenZaakZaakSourceTests.ListMyCases_filters_by_the_callers_bsn`.
@@ -1,6 +1,6 @@
# WP-54 — Docker OpenZaak integration-test harness
Status: todo
Status: done
Phase: 9 — OpenZaak / ZGW integration
## Why
@@ -87,17 +87,19 @@ OpenZaak facts that shape the harness (from the ZGW research):
## Acceptance criteria
- [ ] `docker compose -f backend/openzaak/docker-compose.openzaak.yml up` yields a reachable
- [x] `docker compose -f backend/openzaak/docker-compose.openzaak.yml up` yields a reachable
OpenZaak with the seeded catalogus + zaak, and credentials matching `ZgwOptions`.
- [ ] The Category=Integration test passes against it; the BFF returns the seeded zaak mapped to
- [x] The Category=Integration test passes against it; the BFF returns the seeded zaak mapped to
`ApplicationSummaryDto` through the real HTTP + JWT path.
- [ ] Default `dotnet test` and `npm run ci` are unaffected (integration test excluded, no docker
- [x] Default `dotnet test` and `npm run ci` are unaffected (integration test excluded, no docker
needed); `docker compose up` (root) is unchanged.
## Verification
`docker compose -f backend/openzaak/docker-compose.openzaak.yml up -d` →
`dotnet test --filter Category=Integration` → green; then teardown.
`./backend/openzaak/bootstrap-catalogus.sh` → `dotnet test --filter Category=Integration` →
green; then teardown. Actually run (not just planned) during this WP — see Deviations below
for what that surfaced.
## Out of scope
@@ -112,3 +114,32 @@ always-on CI job (keep it opt-in/manual — OpenZaak startup is slow), performan
keep the fixture in the repo.
- Bootstrap client scopes must include `catalogi.lezen` or zaaktype resolution 403s — cover in
the setup script.
## Deviations from the original plan
- **`heeft_alle_autorisaties: true` instead of granular scopes.** The plan called out
`zaken.lezen`/`catalogi.lezen` specifically; in practice OpenZaak's scripted config
(`vng_api_common_applicaties_config`, upstream's own documented `setup_configuration` YAML
mechanism) exposes an all-scopes flag on the one `Applicatie` this harness ever creates. Since
that application exists for nothing but this throwaway test instance, granular scopes would
add YAML-schema risk for no real least-privilege benefit — took the simpler, equally-scripted
option.
- **A live run found a real production bug, not just a harness wrinkle**: `ZgwHttpClient.cs`
never sent `Content-Crs`/`Accept-Crs` on any ZGW call. Every ZGW write 412s ("Content-Crs
header ontbreekt") without it — a real OpenZaak enforces this; the stub `HttpMessageHandler`
every prior Zgw test used never modelled header requirements, so nothing from WP-49/50 caught
it before now. Fixed in `ZgwHttpClient.cs` alongside the harness (see
`docs/reference/openzaak-integration.md`) — this is precisely the class of bug this WP exists
to catch.
- **Publishing a zaaktype needs more seed data than the plan anticipated**: OpenZaak refuses to
publish a zaaktype with fewer than one resultaattype or fewer than two statustypen (begin +
eind), and a resultaattype's `selectielijstklasse` must share a `procesType` with the
zaaktype's own `selectielijstProcestype` — both cross-checked live against the public VNG
selectielijst API (`selectielijst.openzaak.nl`). `bootstrap-catalogus.sh` seeds all of this;
see its comments for the exact values used and why.
- **No celery/celery-beat/nginx in the harness**, unlike upstream's own compose — trimmed for a
faster-booting, single-purpose harness (this test never asserts on notification delivery,
which is celery's job). `NOTIFICATIONS_DISABLED=true` is required as a consequence: without a
celery worker, `NotificationsConfig` has no client, and OpenZaak's `notify()` hook otherwise
raises inside the same DB transaction as the create — turning a missing-worker problem into a
500 that rolls back the create it was supposed to just notify about.
+29 -8
View File
@@ -101,7 +101,11 @@ confidentiality level would matter for production but isn't needed to prove the
`user_representation`). No refresh flow — OpenZaak expires tokens 1h past `iat`, so per-call
minting is the recommended pattern. Hand-rolled (no `Microsoft.IdentityModel.*` dependency).
- `ZgwHttpClient.cs` — shared GET/POST-with-bearer-JWT plumbing used by both
`OpenZaakZaakSource` and `OpenZaakDocumentSource`.
`OpenZaakZaakSource` and `OpenZaakDocumentSource`. Every request also carries
`Accept-Crs`/`Content-Crs: EPSG:4326` — every ZGW call must declare a coordinate reference
system even when no geometry is involved, or a real OpenZaak 412s ("Content-Crs header
ontbreekt"). This was missing until WP-54's live harness caught it — the stub-handler tests
never modelled the header, so it had shipped silently since WP-49/50.
- `ZgwZaakMapper.cs` — the anti-corruption map: ZGW Zaak → `ApplicationSummaryDto`. This is
where **URL identity** becomes the trailing uuid and the **zaaktype URL** is resolved to a
human label (the cross-service join).
@@ -169,7 +173,7 @@ CallerIdentity.cs`):
existing admin-only `ListCases(now)`. `LocalZaakSource` filters `ApplicationStore.List(bsn)`
(unchanged local behaviour); `OpenZaakZaakSource` appends ZGW's
`rol__betrokkeneIdentificatie__natuurlijkPersoon__inpBsn=<bsn>` query filter to `GET
{ZrcBaseUrl}/zaken`. `GET /applications` (the citizen's own dashboard) now routes through this
{ZrcBaseUrl}/zaken`. `GET /applications` (the citizen's own dashboard) now routes through this
instead of calling `ApplicationStore` directly — the last "reads a static store directly" gap
the ACL caveat below used to flag for a citizen-facing endpoint.
@@ -204,6 +208,22 @@ FE or the contract. Watch the **sync-over-async** `ponytail:` note in `OpenZaakZ
its `OpenZaakDocumentSource` sibling) — make the read/write paths async if OpenZaak becomes the
default.
## Run against real OpenZaak (WP-54)
Everything above was, until WP-54, only proven against fixtures + a stub `HttpMessageHandler` —
no live OpenZaak. `backend/openzaak/` is a **separate**, opt-in docker-compose harness (never
merged into the root `docker-compose.yml`, which stays FE+BFF-only) that brings up a real
OpenZaak, seeds a minimal catalogus/zaaktype/zaak via a bootstrap script, and backs one
xunit test (`OpenZaakIntegrationTests.cs`, tagged `Category=Integration`) that points the BFF at
it with `Zgw:Enabled=true`. See `backend/openzaak/README.md` for the exact commands; the test is
excluded from the default `dotnet test` run and from CI (`--filter Category!=Integration`) since
it only passes with the harness up.
This is also where the `Content-Crs`/`Accept-Crs` header gap above was found: a real OpenZaak
enforces ZGW's geo-header requirement in a way no stub-based test could catch, since a stub
never rejects an unexpected (or missing) header. That is the harness's whole point — proving
the seam against real protocol behaviour, not just the shapes we already assumed.
## Config
```jsonc
@@ -270,15 +290,16 @@ Caveat: `IZaakSource` covers the cases **read (admin + citizen-scoped) + create*
(WP-49/50/53), `IDocumentSource` covers **upload + zaak-link** (WP-51), the inbound
`POST /zgw/notificaties` webhook (WP-52) closes the read/write/document/notify arc, and WP-53
threaded a real per-request `CallerIdentity` through all of it (ownership + the ZGW audit
claims). Other BFF endpoints (reference data like `SeedData`'s BRP/DUO mimics) still read static
stores directly — ACL-ready (the DTO seam exists) but not yet swappable, and not part of this
arc. What's left is **WP-54**: a docker OpenZaak harness + opt-in integration test — today
everything is fixture/mock-tested against no live instance.
claims), and WP-54 added a docker OpenZaak harness + opt-in integration test proving the seam
against a live instance (and, in doing so, caught the missing `Content-Crs`/`Accept-Crs`
headers noted above). Other BFF endpoints (reference data like `SeedData`'s BRP/DUO mimics)
still read static stores directly — ACL-ready (the DTO seam exists) but not yet swappable, and
not part of this arc. That closes the phase-9 OpenZaak/ZGW arc (WP-49..54).
## See also
- [ADR-0005 — OpenZaak behind the BFF](architecture/0005-openzaak-behind-bff.md) — the decision.
- [ADR-0001 — BFF-lite + decision DTOs](architecture/0001-bff-lite-decision-dtos.md) — why the FE doesn't change.
- [WP-49](../project/backlog/WP-49-openzaak-zaken-read-seam.md) (this), WP-50/51 (CRUD arc so far), WP-52 (notificaties), WP-53 (identity seam + citizen-scoping), WP-54 (integration harness, open).
- `backend/src/BigRegister.Api/Zgw/` — the client; `Data/IZaakSource.cs`/`Data/IDocumentSource.cs` — the seams.
- [WP-49](../project/backlog/WP-49-openzaak-zaken-read-seam.md) (this), WP-50/51 (CRUD arc so far), WP-52 (notificaties), WP-53 (identity seam + citizen-scoping), [WP-54](../project/backlog/WP-54-openzaak-integration-harness.md) (integration harness).
- `backend/src/BigRegister.Api/Zgw/` — the client; `Data/IZaakSource.cs`/`Data/IDocumentSource.cs` — the seams; `backend/openzaak/` — the live-OpenZaak test harness (WP-54).
- [ZGW standard (VNG)](https://vng-realisatie.github.io/gemma-zaken/) · [OpenZaak auth docs](https://open-zaak.readthedocs.io/en/stable/client-development/authentication.html).
+6 -1
View File
@@ -92,7 +92,12 @@ test('drafter composes → approver sends; admin republishes appearance', async
// --- Restore: put the org template's appearance back the way this test found it ---
await page.goto('/brief/huisstijl?role=admin');
await page
.locator('.history-row', { hasText: new RegExp(`Versie ${beforeVersion} ·`) })
.locator('.history-row', {
// beforeVersion is a number this test itself captured earlier, never external/attacker input
// (detect-non-literal-regexp false positive — the reported check_id doesn't match what
// `nosemgrep` compares against for this rule, confirmed by trial; bare form suppresses it).
hasText: new RegExp(`Versie ${beforeVersion} ·`), // nosemgrep
})
.getByRole('button', { name: 'Terugzetten in concept' })
.click();
await expect(orgNameInput).not.toHaveValue(unique);
+1 -1
View File
@@ -18,7 +18,7 @@ step "check:tokens"; npm run check:tokens
step "test (vitest + coverage)"; npm run test:coverage
step "build --localize (nl+en)"; npx ng build --localize
step "npm audit (shipped deps)"; npm audit --omit=dev
step "backend format + tests"; ( cd backend && dotnet format BigRegister.slnx --verify-no-changes && dotnet test BigRegister.slnx )
step "backend format + tests"; ( cd backend && dotnet format BigRegister.slnx --verify-no-changes && dotnet test BigRegister.slnx --filter "Category!=Integration" )
step "showcase snippets drift"; npm run gen:snippets && git diff --exit-code src/app/showcase/snippets.generated.ts
step "api-client drift"; npm run gen:api && git diff --exit-code src/app/shared/infrastructure/api-client.ts backend/swagger.json
+6 -2
View File
@@ -131,7 +131,9 @@ function stripContexts(names, args) {
function pruneDependencyCruiser(name, args) {
const file = '.dependency-cruiser.js';
const content = readFile(file);
const re = new RegExp(`^\\s*${name}:\\s*(?:\\[[^\\]]*\\]|null),.*\\n`, 'm');
// name is this local generator's own CLI arg (the operator's own context name), never
// external/attacker input.
const re = new RegExp(`^\\s*${name}:\\s*(?:\\[[^\\]]*\\]|null),.*\\n`, 'm'); // nosemgrep
const next = content.replace(re, '');
if (next === content) {
console.log(` (no CONTEXT_ALLOWED entry for '${name}' in ${file} — already gone?)`);
@@ -143,7 +145,9 @@ function pruneDependencyCruiser(name, args) {
function pruneTsconfig(name, args) {
const file = 'tsconfig.json';
const content = readFile(file);
const re = new RegExp(`^\\s*"@${name}/\\*":\\s*\\["src/app/${name}/\\*"\\],\\n`, 'm');
// name is this local generator's own CLI arg (the operator's own context name), never
// external/attacker input.
const re = new RegExp(`^\\s*"@${name}/\\*":\\s*\\["src/app/${name}/\\*"\\],\\n`, 'm'); // nosemgrep
const next = content.replace(re, '');
if (next === content) {
console.log(` (no @${name}/* alias in ${file} — already gone?)`);