diff --git a/Makefile b/Makefile index 40b9403..a0c22bc 100644 --- a/Makefile +++ b/Makefile @@ -43,7 +43,7 @@ export DOCKER_HOST := unix://$(PODMAN_SOCK) endif endif -.PHONY: ci lint build unit mutation frontend integration verify verify-up verify-acl verify-nrc verify-projection verify-bff verify-domain verify-notifications smoke up down local local-down changelog openzaak-up openzaak-smoke openzaak-seed openzaak-down stack-up stack-smoke stack-down keycloak-up keycloak-smoke keycloak-down flowable-up flowable-smoke flowable-down help +.PHONY: ci lint build unit mutation frontend integration verify verify-up verify-acl verify-nrc verify-projection verify-bff verify-domain verify-notifications smoke up down local verify-local local-down changelog openzaak-up openzaak-smoke openzaak-seed openzaak-down stack-up stack-smoke stack-down keycloak-up keycloak-smoke keycloak-down flowable-up flowable-smoke flowable-down help ## ci: run the full pipeline — lint, build, unit, mutation, frontend, verify (mirrors Gitea Actions) ## `verify` is the live-stack stage (full stack up once → ACL + notification checks). @@ -114,6 +114,11 @@ local: docker compose -f $(LOCAL_COMPOSE) up -d --build WAIT_TIMEOUT=420 bash infra/wait-healthy.sh $(WAIT_SVCS) +## verify-local: acceptance check for the local stack (S-B04) — a fresh `make local` completes the +## whole flow (zaaktype seeded + DMN deployed + NRC abonnement) with NO manual seeding. +verify-local: + bash infra/run-local-flow-check.sh + ## local-down: stop and remove the bind-mount stack local-down: docker compose -f $(LOCAL_COMPOSE) down --volumes diff --git a/docs/architecture/adr-0020-local-stack-self-seeds.md b/docs/architecture/adr-0020-local-stack-self-seeds.md new file mode 100644 index 0000000..f2805fe --- /dev/null +++ b/docs/architecture/adr-0020-local-stack-self-seeds.md @@ -0,0 +1,92 @@ +# ADR-0020: The local stack self-seeds the zaaktype, DMN, and NRC abonnement at bring-up + +- **Status:** Accepted +- **Date:** 2026-07-22 +- **Deciders:** Respellion engineering +- **Relates to:** S-B04 (#110). Local-stack twin of the seeding the verify-* scripts do for CI + (`infra/run-domain-check.sh`, `infra/verify-notification-driver.py`). Superseded in part by S-27 + (#113), which would let the ACL resolve its zaaktype by identificatie and remove the URL injection. + +## Context + +`infra/docker-compose.local.yml` is the host-browser-friendly stack (`make local`) — the one a +developer clicks through the portals with. It had drifted behind three slices, so a fresh bring-up +could not complete the flow: + +1. The ACL pointed at a placeholder zaaktype (`…/00000000-…`), so zaak creation failed with OpenZaak + `400` and the registratie process stuck at `OpenZaakAanmaken` (S-05). +2. `flowable-init` deployed only `registratie.bpmn`, not `diploma-eligibility.dmn`, so completing + `WachtOpDocumenten` 404'd on the missing decision and never reached `Beoordelen` (S-10a/S-13). +3. No NRC abonnement was registered, so notifications reached NRC and went nowhere — the projection + and the openbaar register stayed empty (S-06). + +The CI stack (`infra/docker-compose.yml`) does not hit this because its `verify-*` scripts seed the +zaaktype, deploy the DMN, and register the abonnement at *test* time. The local stack has no such +harness — a developer just runs `make local` and browses. The non-obvious wrinkle is (1): the +zaaktype **UUID is assigned by OpenZaak at creation**, so the ACL's zaaktype URL is not knowable when +the compose file is written and cannot be a static value. + +## Decision + +**Make the local stack self-seed at bring-up via one-shot init containers, and hand the ACL its +server-assigned zaaktype URL through a shared-volume env file it sources on startup.** + +- **DMN (gap 2).** `flowable-init` now deploys `diploma-eligibility.dmn` to the DMN engine + (`/flowable-rest/dmn-api/dmn-repository/deployments`) as a separate deployment alongside the BPMN — + identical to the CI `flowable-init`. Idempotent. +- **Zaaktype + ACL wiring (gap 1).** A `local-seed` one-shot runs the existing + `infra/openzaak/seed_catalogus.py` (`OZ_PUBLISH=1`) against OpenZaak and writes the resulting + `Acl__Defaults__ZaaktypeUrl` / `…InformatieobjecttypeUrl` / `Acl__OpenZaak__BaseUrl` into + `seed-env:/out/acl.env`. The ACL mounts that volume read-only and overrides its entrypoint to + `sh -c 'set -a; . /seed/acl.env; set +a; exec dotnet Acl.Api.dll'`, so the real values override the + compose placeholders before the app reads config. The ACL `depends_on: local-seed + (service_completed_successfully)`. +- **Abonnement (gap 3).** A `nrc-subscribe` one-shot registers an abonnement on the `zaken` kanaal + pointing at the event-subscriber's `/notifications` callback (`infra/local/register-abonnement.py`). + It is a leaf — nothing depends on it — so it can wait for the event-subscriber without forming a + cycle with the ACL bootstrap. +- **Reach OpenZaak/NRC by container IP, not service name.** Both the seed's ZTC calls and the + abonnement's `callbackUrl` are validated by Django's URLValidator, which rejects a single-label host + like `openzaak` / `event-subscriber`. The scripts resolve the target's container IP at runtime (as + `infra/run-domain-check.sh` does), keeping the seeded URLs valid **and** host-consistent — the ACL's + base URL is set to the same OpenZaak IP that owns the zaaktype URL. +- **Acceptance.** `make verify-local` (`infra/run-local-flow-check.sh`) submits against a fresh stack + and asserts the zaak opens, the case reaches the werkbak after documents, and the reference appears + in the openbaar register — the red-to-green test for all three gaps. + +## Consequences + +**Positive** + +- A fresh `make local` completes the full demo (submit → werkbak → openbaar) with no manual seeding — + the slice's stated outcome. +- Reuses the proven CI mechanisms (`seed_catalogus.py`, the DMN deploy, the abonnement driver) rather + than inventing new ones; the only genuinely new piece is the entrypoint-sourced env file. +- No service code changes — the fix is entirely in `infra/` (compose + two small scripts), so the ACL + image and the CI stack are untouched. + +**Negative / costs** + +- The two compose files diverge further: the CI stack seeds at test time, the local stack at bring-up. + Mitigated by reusing the same underlying scripts and cross-referencing them. +- The ACL entrypoint override couples the local ACL to the seed-written file path (`/seed/acl.env`); + if the seed fails, the ACL fails to start (loud, healthcheck-visible — preferred over silently + running with a placeholder). +- Container-IP-based URLs are re-derived on each bring-up; a keep-volumes restart with a changed + OpenZaak IP relies on OpenZaak rebuilding hyperlinked URLs from the request host (it does) so the + idempotent re-seed reports current-IP URLs. + +## Alternatives considered + +- **ACL resolves its zaaktype by identificatie (`BIG-REGISTRATIE`) at startup.** The cleaner, + less-brittle design — no server-assigned URL to capture — and it would help the CI stack too. But it + changes a service's runtime behaviour and its config contract, needs new ACL tests + mutation + coverage, and still needs a seed step to *create* the zaaktype. Deliberately split out as its own + slice with its own ADR (S-27 / #113) rather than folded into this infra-only fix. +- **A documented `make local-seed` step run after `make local`.** Smallest change, but it fails the + slice's "no manual seeding" outcome — the local stack is exactly the one meant to just work in a + browser. Rejected. +- **Fixed zaaktype UUID via OpenZaak `setup_configuration`/fixtures.** OpenZaak assigns UUIDs on POST; + declaratively creating a fully *published* zaaktype (statustypen + resultaattypen validated against + the Selectielijst + roltypen + iot relations) is not something `setup_configuration` supports + cleanly in 1.28.2. Rejected as more fragile than reusing `seed_catalogus.py`. diff --git a/docs/demo-script.md b/docs/demo-script.md index 20e3420..9191f05 100644 --- a/docs/demo-script.md +++ b/docs/demo-script.md @@ -5,6 +5,33 @@ copy-pasteable walkthrough against a local `make up` stack. --- +## S-B04 — `make local` completes the whole flow with no manual seeding (#110, ADR-0020) + +**Outcome:** the host-browser stack (`make local`) now self-seeds at bring-up — it publishes the BIG +zaaktype and wires the ACL to it, deploys the `diploma-eligibility` DMN, and registers the NRC +abonnement — so a fresh bring-up runs submit → werkbak → openbaar without the manual seeding the +`verify-*` scripts do for CI. (Previously the process stuck at `OpenZaakAanmaken`, the werkbak stayed +empty, and the openbaar register showed nothing.) + +```bash +# 1. Fresh bring-up (self-seeding init containers: local-seed, nrc-subscribe; DMN in flowable-init). +make local + +# 2. Assert the whole flow works with no manual seeding — submit opens a zaak, documents route it to +# the werkbak, and the reference appears in the openbaar register: +make verify-local # → "OK — a fresh local stack completed the flow with no manual seeding ..." + +# 3. Or by hand in the browser: log in at http://localhost:8140 (jan-burger / test123), submit + +# upload a PDF, then approve it in the werkbak at http://localhost:8142 (merel-behandelaar / +# test123); it shows as INGESCHREVEN in the openbaar register at http://localhost:8141. +``` + +> The zaaktype UUID is server-assigned, so `local-seed` writes the real URL into a shared volume as +> `acl.env` and the ACL sources it on startup (ADR-0020). The cleaner long-term fix — the ACL +> resolving its zaaktype by `identificatie` — is tracked separately as S-27 (#113). + +--- + ## S-08d — Walking skeleton complete: browser → submit, end-to-end **Outcome:** the self-service portal is served in the stack and the full front-of-house happy path diff --git a/infra/docker-compose.local.yml b/infra/docker-compose.local.yml index a58da5d..beafbb8 100644 --- a/infra/docker-compose.local.yml +++ b/infra/docker-compose.local.yml @@ -1,7 +1,12 @@ # LOCAL development stack — runs with a plain `docker compose up`, no make / no -# seed step / no bash. Use this on a local engine (Docker Desktop on Windows or +# external seed step / no bash. Use this on a local engine (Docker Desktop on Windows or # macOS, or rootless Podman on Linux). # +# Self-seeding (S-B04, #110, ADR-0020): unlike the CI stack — where the verify-* scripts seed the +# zaaktype and register the NRC abonnement at test time — this stack does that itself, via one-shot +# init containers (local-seed, nrc-subscribe) + a DMN deploy in flowable-init, so a fresh bring-up +# completes the whole flow with no manual steps. `make verify-local` asserts it. +# # docker compose -f infra/docker-compose.local.yml up -d --build # podman # docker compose -f infra/docker-compose.local.yml up -d --build --wait # Docker Desktop # docker compose -f infra/docker-compose.local.yml down --volumes @@ -257,28 +262,65 @@ services: restart: "no" volumes: - ../workflows/registratie.bpmn:/work/registratie.bpmn:ro,z + - ../workflows/diploma-eligibility.dmn:/work/diploma-eligibility.dmn:ro,z command: - sh - -c - | - base=http://flowable-rest:8080/flowable-rest/service/repository/deployments - until curl -sf -u rest-admin:test "$$base" >/dev/null 2>&1; do echo "waiting for flowable-rest..."; sleep 3; done - if curl -s -u rest-admin:test "$$base?name=registratie" | grep -q '"name":"registratie"'; then - echo "registratie already deployed; skip" + svc=http://flowable-rest:8080/flowable-rest/service/repository/deployments + dmn=http://flowable-rest:8080/flowable-rest/dmn-api/dmn-repository/deployments + until curl -sf -u rest-admin:test "$$svc" >/dev/null 2>&1; do echo "waiting for flowable-rest..."; sleep 3; done + # Deploy the DMN to the DMN engine and the BPMN to the process engine as SEPARATE deployments: + # flowable-rest does NOT cascade a .dmn bundled in a process .bar into the DMN engine, so the DMN + # must go via dmn-api. The registratie process's DMN service task then resolves the decision across + # deployments by key (S-13, ADR-0016). Without this the WachtOpDocumenten completion 404s on the + # missing decision and the case never reaches Beoordelen (S-B04). Both steps are idempotent. + if curl -s -u rest-admin:test "$$dmn" | grep -q '"name":"diploma-eligibility.dmn"'; then + echo "diploma-eligibility DMN already deployed; skip" else - curl -sf -u rest-admin:test -F 'file=@/work/registratie.bpmn;filename=registratie.bpmn' "$$base" >/dev/null && echo "deployed registratie" + curl -sf -u rest-admin:test -F 'file=@/work/diploma-eligibility.dmn;filename=diploma-eligibility.dmn' "$$dmn" >/dev/null && echo "deployed diploma-eligibility DMN" + fi + if curl -s -u rest-admin:test "$$svc?name=registratie" | grep -q '"name":"registratie"'; then + echo "registratie BPMN already deployed; skip" + else + curl -sf -u rest-admin:test -F 'file=@/work/registratie.bpmn;filename=registratie.bpmn' "$$svc" >/dev/null && echo "deployed registratie BPMN" fi depends_on: flowable-rest: condition: service_started networks: [cg] + # ── Local bootstrap: seed the zaaktype + wire the ACL (S-B04, #110, ADR-0020) ───────────────── + # The zaaktype UUID is assigned by OpenZaak at creation, so it can't be a static value in this + # file. This one-shot seeds + publishes the BIG zaaktype (and the Diploma informatieobjecttype) + # and writes their server-assigned URLs into a shared volume as acl.env, which the ACL sources on + # startup (below). It is the local-stack equivalent of what infra/run-domain-check.sh does for CI. + # Reaches OpenZaak by its container IP because a single-label host fails OpenZaak's URLValidator. + local-seed: + image: docker.io/library/python:3-slim + restart: "no" + volumes: + - ./openzaak/seed_catalogus.py:/work/seed_catalogus.py:ro,z + - ./local/seed-zaaktype.sh:/work/seed-zaaktype.sh:ro,z + - seed-env:/out + command: ["sh", "/work/seed-zaaktype.sh"] + depends_on: + openzaak: + condition: service_healthy + networks: [cg] + # ── ACL ────────────────────────────────────────────────────────────────── acl: build: context: ../services/acl dockerfile: Dockerfile image: register-referentie/acl:dev + # The base/zaaktype/informatieobjecttype below are PLACEHOLDERS. The real, server-assigned + # values are written by the local-seed one-shot into seed-env:/seed/acl.env, which the entrypoint + # sources (set -a) so they override these before the app starts (S-B04, #110, ADR-0020). Sourcing + # a runtime-generated env file is why we override the entrypoint here rather than use `env_file:` + # (which compose reads at parse time, before the seed has run). + entrypoint: ["/bin/sh", "-c", "set -a; . /seed/acl.env; set +a; exec dotnet Acl.Api.dll"] environment: Acl__OpenZaak__BaseUrl: http://openzaak:8000/ Acl__OpenZaak__ClientId: big-reference-seed @@ -286,9 +328,12 @@ services: Acl__Defaults__Bronorganisatie: "517439943" Acl__Defaults__VerantwoordelijkeOrganisatie: "517439943" Acl__Defaults__Vertrouwelijkheidaanduiding: openbaar - Acl__Defaults__ZaaktypeUrl: ${ACL_ZAAKTYPE_URL:-http://openzaak:8000/catalogi/api/v1/zaaktypen/00000000-0000-0000-0000-000000000000} + Acl__Defaults__ZaaktypeUrl: http://openzaak:8000/catalogi/api/v1/zaaktypen/00000000-0000-0000-0000-000000000000 + Acl__Defaults__InformatieobjecttypeUrl: http://openzaak:8000/catalogi/api/v1/informatieobjecttypen/00000000-0000-0000-0000-000000000000 ports: - "8100:8080" + volumes: + - seed-env:/seed:ro healthcheck: test: ["CMD", "curl", "-fsS", "http://localhost:8080/health"] interval: 5s @@ -298,6 +343,8 @@ services: depends_on: openzaak: condition: service_healthy + local-seed: + condition: service_completed_successfully networks: [cg] # ── BFF ────────────────────────────────────────────────────────────────── @@ -400,6 +447,31 @@ services: condition: service_healthy networks: [cg] + # ── Local bootstrap: register the NRC abonnement (S-B04, #110, ADR-0020) ────────────────────── + # Without a subscription, OpenZaak's notifications reach NRC and are delivered nowhere, so the + # projection (and the openbaar register) stay empty. This one-shot registers an abonnement on the + # `zaken` kanaal pointing at the event-subscriber's /notifications callback — the CI equivalent is + # infra/verify-notification-driver.py. The callback uses the event-subscriber's container IP (a + # single-label host fails NRC's URLValidator). It is a leaf (nothing depends on it), so it can wait + # for the event-subscriber without creating a cycle with the ACL bootstrap. + nrc-subscribe: + image: docker.io/library/python:3-slim + restart: "no" + volumes: + - ./local/register-abonnement.py:/work/register-abonnement.py:ro,z + environment: + NRC_BASE: http://nrc-web:8000 + SINK_HOST: event-subscriber + SINK_PORT: "8080" + SINK_AUTH: ${NOTIFICATION_WEBHOOK_TOKEN:-Bearer big-reference-notifications} + command: ["python", "/work/register-abonnement.py"] + depends_on: + nrc-web: + condition: service_healthy + event-subscriber: + condition: service_started + networks: [cg] + projection-api: build: context: .. @@ -492,6 +564,8 @@ volumes: nrc-db: flowable-db: projection-db: + # Carries the seed-generated acl.env (server-assigned zaaktype URLs) from local-seed to the ACL. + seed-env: networks: cg: diff --git a/infra/local/register-abonnement.py b/infra/local/register-abonnement.py new file mode 100755 index 0000000..8ce50c1 --- /dev/null +++ b/infra/local/register-abonnement.py @@ -0,0 +1,78 @@ +#!/usr/bin/env python3 +"""Local-stack bootstrap (S-B04, #110, ADR-0020) — register the NRC abonnement. + +Runs as the `nrc-subscribe` init container of infra/docker-compose.local.yml. Registers an +abonnement on the `zaken` kanaal pointing at the event-subscriber's /notifications callback, so +OpenZaak's notifications (zaak create + status set) reach the projection — without this the openbaar +(public) register stays empty. This is what infra/verify-notification-driver.py does for CI (minus +the test zaak it also creates). + +The callback host is the event-subscriber's resolved **container IP**, not `event-subscriber`, because +NRC validates callbackUrl with Django's URLValidator (a single-label host is rejected — same reason the +zaaktype seed uses OpenZaak's IP). Idempotent + restart-safe: it removes any stale /notifications +abonnement first, then registers one for the current IP. Stdlib only. + +Env: NRC_BASE, SINK_HOST, SINK_PORT, SINK_AUTH, OZ_CLIENT_ID, OZ_SECRET. +""" +import base64, hashlib, hmac, json, os, socket, sys, time, urllib.error, urllib.request + +NRC = os.environ.get("NRC_BASE", "http://nrc-web:8000").rstrip("/") +SINK_HOST = os.environ.get("SINK_HOST", "event-subscriber") +SINK_PORT = os.environ.get("SINK_PORT", "8080") +SINK_AUTH = os.environ.get("SINK_AUTH", "Bearer big-reference-notifications") +CID = os.environ.get("OZ_CLIENT_ID", "big-reference-seed") +SECRET = os.environ.get("OZ_SECRET", "insecure-dev-secret-change-me") + + +def token(): + b64 = lambda b: base64.urlsafe_b64encode(b).rstrip(b"=") + seg = ( + b64(json.dumps({"alg": "HS256", "typ": "JWT"}, separators=(",", ":")).encode()) + + b"." + + b64(json.dumps( + {"iss": CID, "iat": int(time.time()), "client_id": CID, + "user_id": "local-seed", "user_representation": "local-seed"}, + separators=(",", ":")).encode()) + ) + return (seg + b"." + b64(hmac.new(SECRET.encode(), seg, hashlib.sha256).digest())).decode() + + +def call(method, url, body=None): + data = json.dumps(body).encode() if body is not None else None + req = urllib.request.Request(url, data=data, method=method, headers={ + "Authorization": "Bearer " + token(), + "Content-Type": "application/json", "Accept": "application/json"}) + try: + with urllib.request.urlopen(req, timeout=30) as r: + raw = r.read() + return r.status, (json.loads(raw) if raw else None) + except urllib.error.HTTPError as e: + raw = e.read() + return e.code, (json.loads(raw) if raw else None) + + +def main(): + ip = socket.gethostbyname(SINK_HOST) + callback = f"http://{ip}:{SINK_PORT}/notifications" + + # Restart-safe: drop any prior /notifications abonnement (its IP may be stale) before creating a + # fresh one for the current event-subscriber IP. + status, body = call("GET", f"{NRC}/api/v1/abonnement") + for ab in (body or []) if status == 200 else []: + if str(ab.get("callbackUrl", "")).endswith("/notifications"): + if ab.get("callbackUrl") == callback: + print(f"abonnement already current: {ab['url']}") + return + call("DELETE", ab["url"]) + print(f"removed stale abonnement {ab['url']}") + + status, ab = call("POST", f"{NRC}/api/v1/abonnement", { + "callbackUrl": callback, "auth": SINK_AUTH, + "kanalen": [{"naam": "zaken", "filters": {}}]}) + if status != 201: + sys.exit(f"create abonnement -> {status}: {json.dumps(ab)}") + print(f"abonnement registered: {ab['url']} -> {callback}") + + +if __name__ == "__main__": + main() diff --git a/infra/local/seed-zaaktype.sh b/infra/local/seed-zaaktype.sh new file mode 100755 index 0000000..5773ad4 --- /dev/null +++ b/infra/local/seed-zaaktype.sh @@ -0,0 +1,35 @@ +#!/bin/sh +# Local-stack bootstrap (S-B04, #110, ADR-0020) — the "seed zaaktype + wire the ACL" step. +# +# Runs as the `local-seed` init container of infra/docker-compose.local.yml. It seeds + publishes +# the BIG zaaktype (and the Diploma informatieobjecttype) into OpenZaak, then writes the resulting +# **server-assigned** URLs into /out/acl.env, which the ACL entrypoint sources before starting. This +# is the local-stack equivalent of what infra/run-domain-check.sh does for CI: the zaaktype UUID is +# assigned by OpenZaak at creation, so it can't be a static value in the compose file. +# +# Why the container IP and not the `openzaak` service name: OpenZaak validates URL query params +# (e.g. ?catalogus=) with Django's URLValidator, which rejects a single-label host like `openzaak`. +# Seeding against the resolved IP keeps the seeded URLs valid AND host-consistent with the ACL, which +# we point at the same IP below. See docs/runbooks/gitea-actions-gotchas.md and ADR-0020. +set -eu + +oz_ip="$(python3 -c "import socket;print(socket.gethostbyname('openzaak'))")" +OZ_BASE="http://${oz_ip}:8000" +export OZ_BASE OZ_PUBLISH=1 + +echo ">> seeding + publishing the BIG zaaktype at ${OZ_BASE} (idempotent)" +out="$(python3 /work/seed_catalogus.py)" +echo "$out" + +zt="$(printf '%s\n' "$out" | sed -n 's/^ZAAKTYPE_URL //p' | head -1)" +iot="$(printf '%s\n' "$out" | sed -n 's/^INFORMATIEOBJECTTYPE_URL //p' | head -1)" +[ -n "$zt" ] || { echo "ERROR: seed did not report a ZAAKTYPE_URL" >&2; exit 1; } +[ -n "$iot" ] || { echo "ERROR: seed did not report an INFORMATIEOBJECTTYPE_URL" >&2; exit 1; } + +# The ACL entrypoint sources this; these keys override the placeholder defaults in the compose file. +cat > /out/acl.env <> wrote /out/acl.env (base=${OZ_BASE}/ zaaktype=${zt})" diff --git a/infra/run-local-flow-check.sh b/infra/run-local-flow-check.sh new file mode 100755 index 0000000..bfbf0ab --- /dev/null +++ b/infra/run-local-flow-check.sh @@ -0,0 +1,67 @@ +#!/usr/bin/env bash +# +# Acceptance check for the local stack (S-B04, #110): a fresh `make local` must complete the whole +# flow with NO manual seeding. Run against an already-up local stack (infra/docker-compose.local.yml) +# via the host-published ports. It exercises, and thereby covers, the three bring-up gaps the slice +# fixes: +# +# 1. zaaktype seeded + ACL wired -> a submitted registration opens a zaak (zaakUrl gets filled). +# 2. diploma-eligibility DMN deployed -> providing documents completes WachtOpDocumenten, routes +# through the DMN, and the case lands on Beoordelen (visible in the behandel werkbak). +# 3. NRC abonnement registered -> the zaak shows up in the openbaar (public) register. +# +# Before the fix this fails at step 1 (ACL points at a placeholder zaaktype -> OpenZaak 400). +set -euo pipefail + +DOM=${DOM:-http://localhost:8130} # domain +BFF=${BFF:-http://localhost:8080} # bff (openbaar register) +BSN=${BSN:-123456782} +# A minimal, valid PDF, base64-encoded (the diploma upload). +PDF_B64="$(printf '%%PDF-1.4\n1 0 obj<>endobj\ntrailer<>\n%%%%EOF\n' | base64 | tr -d '\n')" + +echo ">> 1. submit a registration (no manual seeding expected)" +loc="$(curl -fsS -D - -o /dev/null -X POST "$DOM/registrations" \ + -H 'Content-Type: application/json' -d "{\"bsn\":\"$BSN\"}" \ + | sed -n 's/\r$//; s/^[Ll]ocation: //p' | head -1)" +[ -n "$loc" ] || { echo "FAIL: POST /registrations returned no Location" >&2; exit 1; } +id="${loc##*/}" +echo " accepted: $id" + +echo ">> 2. poll until the ACL opens the zaak (proves the zaaktype is seeded + wired)" +zaak="" +for _ in $(seq 1 30); do + zaak="$(curl -fsS "$DOM$loc" | python3 -c 'import sys,json;print(json.load(sys.stdin).get("zaakUrl") or "")' 2>/dev/null || true)" + [ -n "$zaak" ] && break + sleep 3 +done +[ -n "$zaak" ] || { echo "FAIL: zaak never opened — ACL zaaktype not wired (gap 1)" >&2; exit 1; } +echo " zaak opened: $zaak" + +echo ">> 3. provide documents (proves the diploma-eligibility DMN is deployed)" +code="$(curl -s -o /dev/null -w '%{http_code}' -X POST "$DOM/registrations/$id/documents" \ + -H 'Content-Type: application/json' \ + -d "{\"bsn\":\"$BSN\",\"contentBase64\":\"$PDF_B64\",\"fileName\":\"diploma.pdf\",\"contentType\":\"application/pdf\"}")" +[ "$code" = "204" ] || { echo "FAIL: provide documents -> $code (DMN missing routes WachtOpDocumenten to a 404 — gap 2)" >&2; exit 1; } +echo " documents accepted (204)" + +echo ">> 4. poll the werkbak until the registration awaits beoordeling (reached Beoordelen)" +in_werkbak="" +for _ in $(seq 1 20); do + in_werkbak="$(curl -fsS "$DOM/behandel/werkbak" | python3 -c "import sys,json;print(any(r.get('registrationId')=='$id' for r in json.load(sys.stdin)))" 2>/dev/null || true)" + [ "$in_werkbak" = "True" ] && break + sleep 3 +done +[ "$in_werkbak" = "True" ] || { echo "FAIL: registration never reached the werkbak (gap 2)" >&2; exit 1; } +echo " in the werkbak" + +echo ">> 5. poll the openbaar register until the reference is publicly visible (proves NRC abonnement)" +public="" +for _ in $(seq 1 30); do + public="$(curl -fsS "$BFF/openbaar/register" | python3 -c "import sys,json;print(any(r.get('reference')=='$id' for r in json.load(sys.stdin)))" 2>/dev/null || true)" + [ "$public" = "True" ] && break + sleep 3 +done +[ "$public" = "True" ] || { echo "FAIL: reference never appeared in the openbaar register — NRC abonnement not registered (gap 3)" >&2; exit 1; } +echo " visible in the openbaar register" + +echo "OK — a fresh local stack completed the flow with no manual seeding (zaaktype + DMN + abonnement)"