diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 26e1f4b..cccae28 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -92,7 +92,9 @@ jobs: 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 + # 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" e2e: # Smoke-level Playwright run against the REAL FE+backend (WP-19) — a fresh diff --git a/.gitignore b/.gitignore index 0958d99..0c79ad0 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/backend/openzaak/README.md b/backend/openzaak/README.md new file mode 100644 index 0000000..2ab446c --- /dev/null +++ b/backend/openzaak/README.md @@ -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` 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). diff --git a/backend/openzaak/bootstrap-catalogus.sh b/backend/openzaak/bootstrap-catalogus.sh new file mode 100755 index 0000000..e18c90f --- /dev/null +++ b/backend/openzaak/bootstrap-catalogus.sh @@ -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 < +/// 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 (, +/// ) uses. Requires the harness in backend/openzaak/ +/// 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 off : +/// that fixture hardcodes Zgw:Enabled=false (offline default) for every other test class, +/// so this one builds its own layering the +/// harness's URLs/credentials (matching backend/openzaak/setup_configuration/data.yaml +/// and bootstrap-catalogus.sh) on top. +/// +[Trait("Category", "Integration")] +public class OpenZaakIntegrationTests +{ + private static WebApplicationFactory Factory() + { + var dbPath = Path.Combine(Path.GetTempPath(), $"bigregister-oz-integration-{Guid.NewGuid():N}.db"); + return new WebApplicationFactory().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>("/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); + } +} diff --git a/docs/project/backlog/README.md b/docs/project/backlog/README.md index 518296d..056eb61 100644 --- a/docs/project/backlog/README.md +++ b/docs/project/backlog/README.md @@ -104,7 +104,7 @@ for its existing violations, so every WP ends green. | [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-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 diff --git a/docs/project/backlog/WP-54-openzaak-integration-harness.md b/docs/project/backlog/WP-54-openzaak-integration-harness.md index a5862b0..fd8c38d 100644 --- a/docs/project/backlog/WP-54-openzaak-integration-harness.md +++ b/docs/project/backlog/WP-54-openzaak-integration-harness.md @@ -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. diff --git a/docs/reference/openzaak-integration.md b/docs/reference/openzaak-integration.md index bb9e6c5..4424055 100644 --- a/docs/reference/openzaak-integration.md +++ b/docs/reference/openzaak-integration.md @@ -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). @@ -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). diff --git a/scripts/ci-local.sh b/scripts/ci-local.sh index e190ee3..d359e8d 100755 --- a/scripts/ci-local.sh +++ b/scripts/ci-local.sh @@ -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