From d3c0ad5d115fa6e92b75f440769a415c2cbf3835 Mon Sep 17 00:00:00 2001 From: Niek Otten Date: Tue, 21 Jul 2026 14:46:43 +0200 Subject: [PATCH] test(acl,verify): cancellation reaches Geannuleerd live (refs #106) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds an ACL↔OpenZaak integration test asserting SetZaakToCancellationStatusAsync records the Geannuleerd status + a resultaat against real OpenZaak, and extends the domain verify script to confirm a timed-out registration's zaak is cancelled to Geannuleerd end-to-end. Co-Authored-By: Claude Opus 4.8 (1M context) --- infra/run-domain-check.sh | 51 +++++++++++++++++-- .../Acl.IntegrationTests/OpenZaakFixture.cs | 13 +++++ .../OpenZaakGatewayIntegrationTests.cs | 33 ++++++++++++ 3 files changed, 93 insertions(+), 4 deletions(-) diff --git a/infra/run-domain-check.sh b/infra/run-domain-check.sh index 359f65c..4455ba3 100755 --- a/infra/run-domain-check.sh +++ b/infra/run-domain-check.sh @@ -309,10 +309,11 @@ done [ -n "$escalated" ] || { echo "FAIL — Beoordelen task not reassigned to teamlead (candidate groups: '$groups')" >&2; docker logs "$dom" 2>&1 | tail -15 >&2; exit 1; } echo "OK — the 14-day timer escalated the still-open Beoordelen task to the teamlead" -# ── S-10a: document timeout. A registration parks at WachtOpDocumenten and — unlike every block above — -# its documents never arrive. We fire its 30-day boundary timer early via the management API; the -# INTERRUPTING timer cancels the wait and routes a token to the RegistratieVerlopen external task. The -# domain's timeout worker acquires it and expires the registration to VERLOPEN (ADR-0017). ──────────── +# ── S-10a/S-10c: document timeout. A registration parks at WachtOpDocumenten and — unlike every block +# above — its documents never arrive. We fire its 30-day boundary timer early via the management API; +# the INTERRUPTING timer cancels the wait and routes a token to the RegistratieVerlopen external task. +# The domain's timeout worker acquires it, cancels the ZGW zaak via the ACL (S-10c), and expires the +# registration to VERLOPEN (ADR-0017). ───────────────────────────────────────────────────────────── echo ">> submitting a registration to let its document term lapse" locv="$(docker run --rm --network "$net" curlimages/curl:latest \ -fsS -D - -o /dev/null -X POST "http://$dom_ip:8080/registrations" \ @@ -354,4 +355,46 @@ for _ in $(seq 1 30); do done [ -n "$verlopen" ] || { echo "FAIL — registration $reg_idv not VERLOPEN after the document timer fired (body: $body)" >&2; docker logs "$dom" 2>&1 | tail -15 >&2; exit 1; } echo "OK — the 30-day document timer expired the registration to VERLOPEN" + +# S-10c: the worker cancels the ZGW zaak (ACL-first, before it expires the aggregate), so a VERLOPEN +# registration must carry a zaak whose current status is "Geannuleerd". Read it back from OpenZaak with +# a ZGW token minted like the seed's client (the same client OpenZaak trusts for this stack). +zaak_url_v="$(printf '%s' "$body" | grep -oiE 'http://[^"]*/zaken/api/v1/zaken/[a-f0-9-]+' | head -1)" +[ -n "$zaak_url_v" ] || { echo "FAIL — VERLOPEN registration $reg_idv exposes no zaak URL (body: $body)" >&2; exit 1; } +echo ">> confirming the zaak $zaak_url_v reached the Geannuleerd status in OpenZaak" + +read_zaak_status() { + docker run --rm --network "$net" \ + -e OZ_CLIENT_ID="${OZ_CLIENT_ID:-big-reference-seed}" \ + -e OZ_SECRET="${OZ_SECRET:-insecure-dev-secret-change-me}" \ + python:3-slim python - "$1" <<'PY' +import base64, hashlib, hmac, json, os, sys, time, urllib.request +cid, sec = os.environ["OZ_CLIENT_ID"], os.environ["OZ_SECRET"] +b64 = lambda b: base64.urlsafe_b64encode(b).rstrip(b"=") +def token(): + hdr = {"alg": "HS256", "typ": "JWT"} + pl = {"iss": cid, "iat": int(time.time()), "client_id": cid, "user_id": "verify", "user_representation": "verify"} + seg = b64(json.dumps(hdr, separators=(",", ":")).encode()) + b"." + b64(json.dumps(pl, separators=(",", ":")).encode()) + return (seg + b"." + b64(hmac.new(sec.encode(), seg, hashlib.sha256).digest())).decode() +def get(url): + req = urllib.request.Request(url, headers={ + "Authorization": "Bearer " + token(), "Accept": "application/json", "Accept-Crs": "EPSG:4326"}) + with urllib.request.urlopen(req, timeout=30) as r: + return json.loads(r.read()) +zaak = get(sys.argv[1]) +status_url = zaak.get("status") +if not status_url: + print(""); sys.exit(0) +print(get(get(status_url)["statustype"]).get("omschrijving", "")) +PY +} + +geannuleerd="" +for _ in $(seq 1 15); do + oms="$(read_zaak_status "$zaak_url_v" 2>/dev/null | tr -d '\r' || true)" + [ "$oms" = "Geannuleerd" ] && { geannuleerd=1; break; } + sleep 2 +done +[ -n "$geannuleerd" ] || { echo "FAIL — zaak $zaak_url_v not Geannuleerd after timeout (current status omschrijving: '$oms')" >&2; docker logs "$dom" 2>&1 | tail -15 >&2; exit 1; } +echo "OK — the timed-out registration's zaak was cancelled to Geannuleerd in OpenZaak" exit 0 diff --git a/services/acl/Acl.IntegrationTests/OpenZaakFixture.cs b/services/acl/Acl.IntegrationTests/OpenZaakFixture.cs index 32dece6..0407918 100644 --- a/services/acl/Acl.IntegrationTests/OpenZaakFixture.cs +++ b/services/acl/Acl.IntegrationTests/OpenZaakFixture.cs @@ -114,6 +114,19 @@ public sealed class OpenZaakFixture : IDisposable return fallback ?? throw new InvalidOperationException($"No statustypen for zaaktype {zaaktypeUrl}"); } + /// Resolve a statustype by its omschrijving (e.g. the S-10c "Geannuleerd" cancellation status). + public async Task FindStatustypeByOmschrijvingAsync(Uri zaaktypeUrl, string omschrijving, CancellationToken ct = default) + { + var query = new Uri(BaseUrl, + "/catalogi/api/v1/statustypen?status=alles&zaaktype=" + Uri.EscapeDataString(zaaktypeUrl.ToString())); + var page = await GetJsonAsync(query, ct); + foreach (var st in page.GetProperty("results").EnumerateArray()) + if (st.TryGetProperty("omschrijving", out var o) && o.GetString() == omschrijving) + return new Uri(st.GetProperty("url").GetString()!); + + throw new InvalidOperationException($"No '{omschrijving}' statustype for zaaktype {zaaktypeUrl}"); + } + // A ZGW (vng-api-common) HS256 JWT, mirroring the seed's client. Minted here // rather than reusing Acl.Infrastructure's internal minter to keep that internal. private string MintToken() diff --git a/services/acl/Acl.IntegrationTests/OpenZaakGatewayIntegrationTests.cs b/services/acl/Acl.IntegrationTests/OpenZaakGatewayIntegrationTests.cs index f9c4c13..d59d0d9 100644 --- a/services/acl/Acl.IntegrationTests/OpenZaakGatewayIntegrationTests.cs +++ b/services/acl/Acl.IntegrationTests/OpenZaakGatewayIntegrationTests.cs @@ -75,6 +75,39 @@ public sealed class OpenZaakGatewayIntegrationTests(OpenZaakFixture stack) Assert.Equal(eindstatustype.ToString(), status.GetProperty("statustype").GetString()); } + [Fact] + public async Task Cancelling_a_zaak_records_the_geannuleerd_status_and_a_resultaat() + { + var zaaktype = await stack.FindPublishedBigZaaktypeAsync(); + Assert.True(zaaktype is not null, + "No published BIG-REGISTRATIE zaaktype found in OpenZaak — bring the stack up and " + + "seed it with OZ_PUBLISH=1 (`make integration` does this)."); + + var gateway = new OpenZaakGateway(stack.Http, stack.Options); + var zaakUrl = await gateway.OpenZaakAsync(new ZaakRequest( + Bronorganisatie: "517439943", + VerantwoordelijkeOrganisatie: "517439943", + Vertrouwelijkheidaanduiding: "openbaar", + Zaaktype: zaaktype!, + Startdatum: DateOnly.FromDateTime(DateTime.UtcNow), + Identificatie: Guid.NewGuid().ToString())); + + await gateway.SetZaakToCancellationStatusAsync(zaakUrl, zaaktype!, DateOnly.FromDateTime(DateTime.UtcNow)); + + // The zaak's current status is the Geannuleerd statustype — distinct from the approval eindstatus. + var zaak = await stack.GetZaakAsync(zaakUrl); + var statusUrl = zaak.GetProperty("status").GetString(); + Assert.False(string.IsNullOrEmpty(statusUrl), "the cancelled zaak has no current status"); + + var status = await stack.GetJsonAsync(new Uri(statusUrl!)); + var geannuleerd = await stack.FindStatustypeByOmschrijvingAsync(zaaktype!, "Geannuleerd"); + Assert.Equal(geannuleerd.ToString(), status.GetProperty("statustype").GetString()); + + // ...and a resultaat is recorded (OpenZaak requires it before a closing/terminal status). + Assert.False(string.IsNullOrEmpty(zaak.GetProperty("resultaat").GetString()), + "the cancelled zaak has no resultaat"); + } + [Fact] public async Task Storing_a_diploma_creates_a_real_informatieobject_related_to_the_zaak() {