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:
@@ -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>
|
||||
|
||||
19
tests/acceptance/Features/RegisterProjectieBijwerken.feature
Normal file
19
tests/acceptance/Features/RegisterProjectieBijwerken.feature
Normal 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"
|
||||
48
tests/acceptance/Steps/RegisterProjectieBijwerkenSteps.cs
Normal file
48
tests/acceptance/Steps/RegisterProjectieBijwerkenSteps.cs
Normal 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));
|
||||
}
|
||||
}
|
||||
40
tests/acceptance/Support/InMemoryProjectionStores.cs
Normal file
40
tests/acceptance/Support/InMemoryProjectionStores.cs
Normal 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)];
|
||||
}
|
||||
Reference in New Issue
Block a user