test(event-subscriber): project zaak-created notifications into the read projection (refs #7)

Failing unit + acceptance tests for the Event Subscriber's NotificationProjector:
a zaken/zaak/create notification yields one INGEDIEND projection row, duplicate
deliveries collapse to one row, non-zaak/non-create notifications are ignored, and
a rebuild repopulates the projection from the durable notification log (PRD §8.4).

The projector is a no-op stub so the tests compile and fail on the assertions; the
implementation follows in the green commit. The notification log doubles as the
idempotency guard and rebuild source so a rebuild needs no OpenZaak access (§8.1).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-30 14:47:16 +02:00
parent 32c98f00db
commit c70840e5b7
13 changed files with 358 additions and 0 deletions

View File

@@ -18,6 +18,7 @@
<ItemGroup>
<ProjectReference Include="..\..\services\acl\Acl.Application\Acl.Application.csproj" />
<ProjectReference Include="..\..\services\acl\Acl.Infrastructure\Acl.Infrastructure.csproj" />
<ProjectReference Include="..\..\services\event-subscriber\EventSubscriber.Application\EventSubscriber.Application.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,19 @@
# 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.
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
Then the register projection contains a row for "11111111-1111-1111-1111-111111111111" with status "INGEDIEND"
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
Then the register projection contains exactly one row for "22222222-2222-2222-2222-222222222222"

View File

@@ -0,0 +1,48 @@
using Acceptance.Support;
using EventSubscriber.Application;
using Reqnroll;
using Xunit;
namespace Acceptance.Steps;
/// <summary>Bindings for <c>RegisterProjectieBijwerken.feature</c> (S-06). Reqnroll creates
/// one instance per scenario, so instance fields hold scenario-scoped state.</summary>
[Binding]
public sealed class RegisterProjectieBijwerkenSteps
{
private const string ZaakBase = "http://openzaak:8000/zaken/api/v1/zaken/";
private readonly InMemoryNotificationLog _log = new();
private readonly InMemoryProjectionStore _store = new();
private readonly NotificationProjector _projector;
private Notification? _notification;
public RegisterProjectieBijwerkenSteps() => _projector = new NotificationProjector(_log, _store);
[Given("a zaak is created in OpenZaak with id \"(.*)\"")]
public void GivenAZaakIsCreatedInOpenZaakWithId(string id)
=> _notification = new Notification("zaken", "zaak", "create", new Uri(ZaakBase + id));
[When("the NRC notification for that zaak is delivered to the event subscriber")]
public Task WhenTheNotificationIsDelivered()
=> _projector.HandleAsync(_notification!);
[When("the same NRC notification is delivered again")]
public Task WhenTheSameNotificationIsDeliveredAgain()
=> _projector.HandleAsync(_notification!);
[Then("the register projection contains a row for \"(.*)\" with status \"(.*)\"")]
public async Task ThenTheProjectionContainsARowWithStatus(string id, string status)
{
var entry = Assert.Single(await _store.AllAsync());
Assert.Equal(id, entry.Id);
Assert.Equal(status, entry.Status);
}
[Then("the register projection contains exactly one row for \"(.*)\"")]
public async Task ThenTheProjectionContainsExactlyOneRowFor(string id)
{
await _store.AllAsync();
Assert.Single(_store.RowsFor(id));
}
}

View File

@@ -0,0 +1,40 @@
using EventSubscriber.Application;
namespace Acceptance.Support;
/// <summary>In-memory stand-ins for the Event Subscriber's projection store and notification
/// log, so the S-06 acceptance scenario runs without Postgres (real delivery is a live-stack
/// check). Mirrors the <c>InMemoryZaakGateway</c> approach used for the ACL scenario.</summary>
public sealed class InMemoryNotificationLog : INotificationLog
{
private readonly Dictionary<string, RecordedNotification> _byKey = [];
public Task<bool> TryRecordAsync(RecordedNotification notification, CancellationToken ct = default)
=> Task.FromResult(_byKey.TryAdd(notification.Key, notification));
public Task<IReadOnlyList<RecordedNotification>> AllAsync(CancellationToken ct = default)
=> Task.FromResult<IReadOnlyList<RecordedNotification>>([.. _byKey.Values]);
}
public sealed class InMemoryProjectionStore : IProjectionStore
{
private readonly Dictionary<string, RegisterEntry> _byId = [];
public Task UpsertAsync(RegisterEntry entry, CancellationToken ct = default)
{
_byId[entry.Id] = entry;
return Task.CompletedTask;
}
public Task ClearAsync(CancellationToken ct = default)
{
_byId.Clear();
return Task.CompletedTask;
}
public Task<IReadOnlyList<RegisterEntry>> AllAsync(CancellationToken ct = default)
=> Task.FromResult<IReadOnlyList<RegisterEntry>>([.. _byId.Values]);
public IReadOnlyList<RegisterEntry> RowsFor(string id)
=> [.. _byId.Values.Where(e => e.Id == id)];
}