diff --git a/infra/local/register-abonnement.py b/infra/local/register-abonnement.py
index 8ce50c1..57b462f 100755
--- a/infra/local/register-abonnement.py
+++ b/infra/local/register-abonnement.py
@@ -2,10 +2,10 @@
"""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).
+abonnement on the `objecten` kanaal pointing at the event-subscriber's /notifications callback, so
+the register writes the ACL makes (INGEDIEND on submit, INGESCHREVEN on approval) reach the
+projection — without this the openbaar (public) register stays empty. Since S-19b-2 the projection
+is sourced from the register in Objecten, not from ZGW zaak events (ADR-0030).
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
@@ -22,6 +22,8 @@ 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")
+# The projection is sourced from the register in Objecten, not from ZGW zaak events (S-19b-2).
+KANAAL = "objecten"
def token():
@@ -60,7 +62,10 @@ def main():
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:
+ # The kanaal is part of "current": an abonnement left over from before S-19b-2 points at
+ # the right callback but listens on `zaken`, and would never be replaced on IP alone.
+ kanalen = [k.get("naam") for k in ab.get("kanalen", [])]
+ if ab.get("callbackUrl") == callback and kanalen == [KANAAL]:
print(f"abonnement already current: {ab['url']}")
return
call("DELETE", ab["url"])
@@ -68,7 +73,7 @@ def main():
status, ab = call("POST", f"{NRC}/api/v1/abonnement", {
"callbackUrl": callback, "auth": SINK_AUTH,
- "kanalen": [{"naam": "zaken", "filters": {}}]})
+ "kanalen": [{"naam": KANAAL, "filters": {}}]})
if status != 201:
sys.exit(f"create abonnement -> {status}: {json.dumps(ab)}")
print(f"abonnement registered: {ab['url']} -> {callback}")
diff --git a/infra/run-projection-check.sh b/infra/run-projection-check.sh
index 6499277..2ea12ac 100755
--- a/infra/run-projection-check.sh
+++ b/infra/run-projection-check.sh
@@ -1,15 +1,19 @@
#!/usr/bin/env bash
#
-# Verify the end-to-end read-projection path (S-06) against an ALREADY-RUNNING full stack:
-# OpenZaak → NRC → Event Subscriber → projection → projection-api. Seeds a published BIG
-# zaaktype (idempotent), registers an abonnement on the `zaken` kanaal pointing at the real
-# Event Subscriber's /notifications callback (with the bearer it enforces), creates a zaak,
-# and asserts projection-api serves a row for that zaak with status INGEDIEND.
+# Verify the end-to-end read-projection path (S-06, re-sourced by S-19b-2) against an ALREADY-RUNNING
+# full stack: ACL → Objecten → NRC → Event Subscriber → projection → projection-api. Seeds a
+# published BIG zaaktype (idempotent), registers an abonnement on the `objecten` kanaal pointing at
+# the real Event Subscriber's /notifications callback (with the bearer it enforces), opens a zaak
+# *through the ACL*, and asserts projection-api serves a row for it with status INGEDIEND.
+#
+# The zaak is opened through the ACL, not straight against OpenZaak: since ADR-0030 the projection is
+# derived from the RegisterRecord in Objecten, and the ACL is what writes that record (INGEDIEND on
+# submit). A zaak created behind the ACL's back produces no register write and so no projection row —
+# which is the point of the re-source.
#
# All in-network, reaching services by container IP — single-label hosts aren't URL-valid and
-# the runner can't reach published ports (gitea-actions-gotchas.md §5/§6). Reuses the
-# notification driver to register the abonnement + create the zaak. Does NOT manage the stack
-# lifecycle (the caller owns bring-up + teardown). Plain docker primitives only. See ADR-0007/0008.
+# the runner can't reach published ports (gitea-actions-gotchas.md §5/§6). Does NOT manage the stack
+# lifecycle (the caller owns bring-up + teardown). Plain docker primitives only. See ADR-0007/0008/0030.
set -euo pipefail
here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
@@ -24,11 +28,13 @@ oz="$(docker ps -q --filter 'name=[-_]openzaak[-_]' | head -1)"
nrc="$(docker ps -q --filter 'name=nrc-web' | head -1)"
es="$(docker ps -q --filter 'name=event-subscriber' | head -1)"
proj="$(docker ps -q --filter 'name=projection-api' | head -1)"
+acl="$(docker ps -q --filter 'name=[-_]acl[-_]' | head -1)"
[ -n "$oz" ] && [ -n "$nrc" ] || { echo "ERROR: OpenZaak and/or NRC not running — bring the stack up first" >&2; exit 1; }
[ -n "$es" ] && [ -n "$proj" ] || { echo "ERROR: event-subscriber and/or projection-api not running — bring the stack up first" >&2; exit 1; }
+[ -n "$acl" ] || { echo "ERROR: acl not running — bring the stack up first" >&2; exit 1; }
net="$(docker inspect -f '{{range $k,$_ := .NetworkSettings.Networks}}{{$k}}{{"\n"}}{{end}}' "$oz" | head -1)"
-oz_ip="$(ip "$oz")"; nrc_ip="$(ip "$nrc")"; es_ip="$(ip "$es")"; proj_ip="$(ip "$proj")"
-echo ">> network=$net openzaak=$oz_ip nrc=$nrc_ip event-subscriber=$es_ip projection-api=$proj_ip"
+oz_ip="$(ip "$oz")"; nrc_ip="$(ip "$nrc")"; es_ip="$(ip "$es")"; proj_ip="$(ip "$proj")"; acl_ip="$(ip "$acl")"
+echo ">> network=$net openzaak=$oz_ip nrc=$nrc_ip event-subscriber=$es_ip projection-api=$proj_ip acl=$acl_ip"
echo ">> seeding a published BIG zaaktype (idempotent)"
sid="$(docker create --network "$net" -e "OZ_BASE=http://$oz_ip:8000" -e OZ_PUBLISH=1 \
@@ -37,19 +43,26 @@ docker cp "$here/openzaak/seed_catalogus.py" "$sid:/seed.py" >/dev/null
docker start -a "$sid"
docker rm -f "$sid" >/dev/null
-echo ">> registering abonnement at the Event Subscriber + creating a zaak"
+echo ">> registering the event-subscriber abonnement on the objecten kanaal"
docker rm -f rr-pverify >/dev/null 2>&1 || true
+# The same script the local stack uses (ADR-0020), so both paths register the identical abonnement.
drv="$(docker create --network "$net" --name rr-pverify \
- -e "OZ_BASE=http://$oz_ip:8000" -e "NRC_BASE=http://$nrc_ip:8000" \
- -e "SINK_CALLBACK=http://$es_ip:8080/notifications" -e "SINK_AUTH=$WEBHOOK_AUTH" \
- python:3-slim python /driver.py)"
-docker cp "$here/verify-notification-driver.py" "$drv:/driver.py" >/dev/null
+ -e "NRC_BASE=http://$nrc_ip:8000" \
+ -e "SINK_HOST=$es_ip" -e "SINK_PORT=8080" -e "SINK_AUTH=$WEBHOOK_AUTH" \
+ python:3-slim python /subscribe.py)"
+docker cp "$here/local/register-abonnement.py" "$drv:/subscribe.py" >/dev/null
docker start -a "$drv"
-zaak_url="$(docker logs rr-pverify 2>/dev/null | sed -n 's/^ZAAK_CREATED //p' | head -1)"
docker rm -f rr-pverify >/dev/null
-[ -n "$zaak_url" ] || { echo "ERROR: driver did not create a zaak" >&2; exit 1; }
+
+echo ">> opening a zaak through the ACL (which writes the INGEDIEND register record)"
+reference="PROJ-$(date +%s)"
+zaak_url="$(docker run --rm --network "$net" curlimages/curl:latest \
+ -fsS -X POST "http://$acl_ip:8080/zaken" -H 'Content-Type: application/json' \
+ -d "{\"bsn\":\"123456782\",\"reference\":\"$reference\"}" \
+ | sed -n 's/.*"zaakUrl":"\([^"]*\)".*/\1/p')"
+[ -n "$zaak_url" ] || { echo "ERROR: the ACL did not open a zaak" >&2; exit 1; }
zaak_uuid="${zaak_url##*/}"
-echo ">> zaak created: $zaak_url"
+echo ">> zaak created: $zaak_url (reference $reference)"
echo ">> polling projection-api for the projected row (status INGEDIEND)"
for _ in $(seq 1 30); do
@@ -63,6 +76,8 @@ for _ in $(seq 1 30); do
sleep 2
done
echo "FAIL — projection-api never served an INGEDIEND row for zaak $zaak_uuid" >&2
+echo " The chain is ACL → Objecten → NRC → event-subscriber → projection (ADR-0030)." >&2
echo "--- event-subscriber log ---" >&2; docker logs "$es" 2>&1 | tail -10 >&2
echo "--- projection-api log ---" >&2; docker logs "$proj" 2>&1 | tail -10 >&2
+echo "--- acl log ---" >&2; docker logs "$acl" 2>&1 | tail -10 >&2
exit 1
diff --git a/tests/acceptance/Features/RegisterProjectieBijwerken.feature b/tests/acceptance/Features/RegisterProjectieBijwerken.feature
index f9d1220..f31e802 100644
--- a/tests/acceptance/Features/RegisterProjectieBijwerken.feature
+++ b/tests/acceptance/Features/RegisterProjectieBijwerken.feature
@@ -1,19 +1,28 @@
# language: en
-# Drives S-06 (#7). On a zaak-created notification from NRC the Event Subscriber writes a
-# rebuildable read-projection row (PRD §8.4). This scenario exercises the use case against an
-# in-memory stand-in for the projection store and notification log; real OpenZaak → NRC →
-# subscriber delivery is verified by the live-stack check (verify-projection, ADR-0007/#58).
-Feature: Register-projectie bijwerken op een zaaknotificatie
- Als openbaar register wil ik dat een aangemaakte zaak in de projectie verschijnt
- zodat het register de ingediende registratie kan tonen.
+# Drives S-19b-2 (#153), re-sourcing S-06 (#7). The read projection is derived from the
+# RegisterRecord in Objecten (ADR-0030), not from ZGW zaak events: the ACL records a registration
+# in the register, Objecten notifies, and the Event Subscriber projects the record that
+# notification points at. This scenario exercises the use case against in-memory stand-ins for the
+# register, the projection store and the notification log; real Objecten → NRC → subscriber
+# delivery is verified by the live-stack check (verify-projection, ADR-0007/0030).
+Feature: Register-projectie bijwerken op een registerwijziging
+ Als openbaar register wil ik dat een registratie in de projectie verschijnt zodra zij
+ in het register is vastgelegd, zodat het register haar actuele status kan tonen.
- Scenario: Een zaaknotificatie levert een rij met status INGEDIEND
- Given a zaak is created in OpenZaak with id "11111111-1111-1111-1111-111111111111"
- When the NRC notification for that zaak is delivered to the event subscriber
+ Scenario: Een ingediende registratie levert een rij met status INGEDIEND
+ Given registration "11111111-1111-1111-1111-111111111111" is recorded in the register with status "INGEDIEND"
+ When the register notification is delivered to the event subscriber
Then the register projection contains a row for "11111111-1111-1111-1111-111111111111" with status "INGEDIEND"
+ Scenario: Een goedgekeurde registratie werkt dezelfde rij bij
+ Given registration "33333333-3333-3333-3333-333333333333" is recorded in the register with status "INGEDIEND"
+ And the register notification is delivered to the event subscriber
+ When registration "33333333-3333-3333-3333-333333333333" is recorded in the register with status "INGESCHREVEN"
+ And the register notification is delivered to the event subscriber
+ Then the register projection contains a row for "33333333-3333-3333-3333-333333333333" with status "INGESCHREVEN"
+
Scenario: Dezelfde notificatie tweemaal levert geen duplicaat
- Given a zaak is created in OpenZaak with id "22222222-2222-2222-2222-222222222222"
- When the NRC notification for that zaak is delivered to the event subscriber
- And the same NRC notification is delivered again
+ Given registration "22222222-2222-2222-2222-222222222222" is recorded in the register with status "INGEDIEND"
+ When the register notification is delivered to the event subscriber
+ And the same register notification is delivered again
Then the register projection contains exactly one row for "22222222-2222-2222-2222-222222222222"
diff --git a/tests/acceptance/Steps/RegisterProjectieBijwerkenSteps.cs b/tests/acceptance/Steps/RegisterProjectieBijwerkenSteps.cs
index 39f485b..b2e535c 100644
--- a/tests/acceptance/Steps/RegisterProjectieBijwerkenSteps.cs
+++ b/tests/acceptance/Steps/RegisterProjectieBijwerkenSteps.cs
@@ -5,31 +5,39 @@ using Xunit;
namespace Acceptance.Steps;
-/// Bindings for RegisterProjectieBijwerken.feature (S-06). Reqnroll creates
-/// one instance per scenario, so instance fields hold scenario-scoped state.
+/// Bindings for RegisterProjectieBijwerken.feature (S-06, re-sourced by S-19b-2).
+/// Reqnroll creates one instance per scenario, so instance fields hold scenario-scoped state.
[Binding]
public sealed class RegisterProjectieBijwerkenSteps
{
- private const string ZaakBase = "http://openzaak:8000/zaken/api/v1/zaken/";
+ private const string ObjectBase = "http://objecten.local:8000/api/v2/objects/";
private readonly InMemoryNotificationLog _log = new();
private readonly InMemoryProjectionStore _store = new();
+ private readonly InMemoryRegisterRecordClient _register = new();
private readonly NotificationProjector _projector;
private Notification? _notification;
public RegisterProjectieBijwerkenSteps()
- => _projector = new NotificationProjector(_log, _store, new InMemoryAclReferenceClient());
+ => _projector = new NotificationProjector(_log, _store, _register);
- [Given("a zaak is created in OpenZaak with id \"(.*)\"")]
- public void GivenAZaakIsCreatedInOpenZaakWithId(string id)
- => _notification = new Notification("zaken", "zaak", "create", new Uri(ZaakBase + id));
+ [Given("registration \"(.*)\" is recorded in the register with status \"(.*)\"")]
+ [When("registration \"(.*)\" is recorded in the register with status \"(.*)\"")]
+ public void RegistrationIsRecorded(string id, string status)
+ {
+ // The ACL upserts one object per registration, so submit and approval share an object URL.
+ var objectUrl = ObjectBase + id;
+ _register.Records[objectUrl] = new RegisterRecord(id, status, "REG-" + id);
+ _notification = new Notification("objecten", "object", "create", new Uri(objectUrl), new Uri(objectUrl));
+ }
- [When("the NRC notification for that zaak is delivered to the event subscriber")]
- public Task WhenTheNotificationIsDelivered()
+ [Given("the register notification is delivered to the event subscriber")]
+ [When("the register notification is delivered to the event subscriber")]
+ public Task TheNotificationIsDelivered()
=> _projector.HandleAsync(_notification!);
- [When("the same NRC notification is delivered again")]
- public Task WhenTheSameNotificationIsDeliveredAgain()
+ [When("the same register notification is delivered again")]
+ public Task TheSameNotificationIsDeliveredAgain()
=> _projector.HandleAsync(_notification!);
[Then("the register projection contains a row for \"(.*)\" with status \"(.*)\"")]
diff --git a/tests/acceptance/Support/InMemoryProjectionStores.cs b/tests/acceptance/Support/InMemoryProjectionStores.cs
index ade1106..9a100c0 100644
--- a/tests/acceptance/Support/InMemoryProjectionStores.cs
+++ b/tests/acceptance/Support/InMemoryProjectionStores.cs
@@ -39,10 +39,12 @@ public sealed class InMemoryProjectionStore : IProjectionStore
=> [.. _byId.Values.Where(e => e.Id == id)];
}
-/// A fake ACL client for the projection acceptance scenario: returns a reference derived
-/// from the zaak, so the projector can enrich rows without a running ACL (#78).
-public sealed class InMemoryAclReferenceClient : IAclClient
+/// An in-memory stand-in for the register the ACL reads back for the projector, so the
+/// scenario runs without a running ACL or Objecten (S-19b-2, ADR-0030).
+public sealed class InMemoryRegisterRecordClient : IAclClient
{
- public Task GetZaakReferenceAsync(Uri zaakUrl, CancellationToken ct = default)
- => Task.FromResult("REG-" + zaakUrl.Segments[^1].Trim('/'));
+ public Dictionary Records { get; } = [];
+
+ public Task GetRegisterRecordAsync(Uri objectUrl, CancellationToken ct = default)
+ => Task.FromResult(Records.TryGetValue(objectUrl.ToString(), out var record) ? record : null);
}