From 5add817c1072bd70e4a8fc9cb8665c0aed147c48 Mon Sep 17 00:00:00 2001 From: Niek Otten Date: Mon, 20 Jul 2026 09:43:13 +0200 Subject: [PATCH] test(domain): RegistratieVerlopen worker expires the correlated registration (refs #102) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit RED: ExpireRegistrationWorker loads the registration a RegistratieVerlopen job correlates to and expires it (idempotent on redelivery, throws on unknown so the job is redelivered); RegistratieVerlopenProcessor drains the parked jobs and completes each, leaving a failing one un-completed (§8.6). Mirrors the OpenZaak and escalation worker/processor pairs. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../ExpireRegistrationWorkerTests.cs | 65 +++++++++++++++ .../RegistratieVerlopenProcessorTests.cs | 79 +++++++++++++++++++ 2 files changed, 144 insertions(+) create mode 100644 services/domain/Big.Tests/ExpireRegistrationWorkerTests.cs create mode 100644 services/domain/Big.Tests/RegistratieVerlopenProcessorTests.cs diff --git a/services/domain/Big.Tests/ExpireRegistrationWorkerTests.cs b/services/domain/Big.Tests/ExpireRegistrationWorkerTests.cs new file mode 100644 index 0000000..612139f --- /dev/null +++ b/services/domain/Big.Tests/ExpireRegistrationWorkerTests.cs @@ -0,0 +1,65 @@ +using Big.Application; +using Big.Domain; + +namespace Big.Tests; + +// S-10a (#102): the application handler behind the RegistratieVerlopen external-worker job. The 30-day +// document-wait timer fired, so the correlated registration is expired to VERLOPEN. Mirrors +// OpenZaakWorker — pure application logic over ports, idempotent under at-least-once delivery (§8.6). +public class ExpireRegistrationWorkerTests +{ + private const string Bsn = "123456782"; + + private static Registration Submitted(string processInstanceId = "proc-1") + { + var registration = Registration.Submit(Bsn); + registration.RecordProcessStarted(processInstanceId); + return registration; + } + + [Fact] + public async Task Expires_the_registration_the_job_correlates_to() + { + var store = new FakeRegistrationStore(); + var registration = Submitted(); + store.Seed(registration); + + await new ExpireRegistrationWorker(store).HandleAsync( + new RegistratieVerlopenJob("job-7", registration.Id)); + + var saved = await store.GetAsync(registration.Id); + Assert.Equal(RegistrationStatus.Verlopen, saved!.Status); + Assert.Equal(1, store.SaveCount); + } + + [Fact] + public async Task An_already_verlopen_registration_is_not_persisted_again() + { + // A redelivered job (§8.6) finds the aggregate already VERLOPEN: a no-op, not saved again. + var store = new FakeRegistrationStore(); + var registration = Submitted(); + registration.Expire(); + store.Seed(registration); + + await new ExpireRegistrationWorker(store).HandleAsync( + new RegistratieVerlopenJob("job-7", registration.Id)); + + Assert.Equal(0, store.SaveCount); + Assert.Equal(RegistrationStatus.Verlopen, (await store.GetAsync(registration.Id))!.Status); + } + + [Fact] + public async Task An_unknown_registration_throws_so_the_job_is_redelivered() + { + var store = new FakeRegistrationStore(); + + await Assert.ThrowsAsync(() => + new ExpireRegistrationWorker(store).HandleAsync( + new RegistratieVerlopenJob("job-7", RegistrationId.New()))); + } + + [Fact] + public async Task Rejects_a_null_job() + => await Assert.ThrowsAsync(() => + new ExpireRegistrationWorker(new FakeRegistrationStore()).HandleAsync(null!)); +} diff --git a/services/domain/Big.Tests/RegistratieVerlopenProcessorTests.cs b/services/domain/Big.Tests/RegistratieVerlopenProcessorTests.cs new file mode 100644 index 0000000..e13c950 --- /dev/null +++ b/services/domain/Big.Tests/RegistratieVerlopenProcessorTests.cs @@ -0,0 +1,79 @@ +using Big.Application; +using Big.Infrastructure; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; + +namespace Big.Tests; + +// S-10a (#102): the document-timeout drain loop. Mirrors BeoordelingEscalatieProcessor — acquire the +// parked RegistratieVerlopen jobs (the tokens the 30-day boundary timer on WachtOpDocumenten spawns), +// expire each correlated registration via the ExpireRegistrationWorker, then complete the job. A job +// whose expiry fails is logged and left un-completed for Flowable to redeliver (§8.6). +public class RegistratieVerlopenProcessorTests +{ + /// A fake client scripting the jobs to acquire and recording completions. + private sealed class FakeVerlopenClient(params RegistratieVerlopenJob[] jobs) : IRegistratieVerlopenClient + { + public int AcquireCount { get; private set; } + public List Completed { get; } = []; + + public Task> AcquireRegistratieVerlopenJobsAsync(int maxJobs, CancellationToken ct = default) + { + AcquireCount++; + return Task.FromResult>(jobs.Take(maxJobs).ToList()); + } + + public Task CompleteRegistratieVerlopenJobAsync(string jobId, CancellationToken ct = default) + { + Completed.Add(jobId); + return Task.CompletedTask; + } + } + + private static ExpireRegistrationWorker Worker(FakeRegistrationStore store) => new(store); + + [Fact] + public async Task Acquires_a_job_expires_the_registration_and_completes_the_job() + { + var store = new FakeRegistrationStore(); + var registration = Domain.Registration.Submit("123456782"); + store.Seed(registration); + var client = new FakeVerlopenClient(new RegistratieVerlopenJob("job-9", registration.Id)); + + var acquired = await new RegistratieVerlopenProcessor( + client, Worker(store), NullLogger.Instance).PumpOnceAsync(5); + + Assert.Equal(1, acquired); + Assert.Equal(Domain.RegistrationStatus.Verlopen, (await store.GetAsync(registration.Id))!.Status); + Assert.Equal("job-9", Assert.Single(client.Completed)); + } + + [Fact] + public async Task A_failing_expiry_is_left_uncompleted_for_flowable_to_redeliver() + { + // Unknown registration → the worker throws → the job is left for redelivery, error logged. + var store = new FakeRegistrationStore(); + var client = new FakeVerlopenClient(new RegistratieVerlopenJob("job-9", Domain.RegistrationId.New())); + var logger = new CapturingLogger(); + + var acquired = await new RegistratieVerlopenProcessor(client, Worker(store), logger).PumpOnceAsync(5); + + Assert.Equal(1, acquired); + Assert.Empty(client.Completed); + var error = Assert.Single(logger.Entries, e => e.Level == LogLevel.Error); + Assert.Contains("job-9", error.Message); + } + + [Fact] + public async Task Does_nothing_but_poll_when_there_are_no_jobs() + { + var client = new FakeVerlopenClient(); + + var acquired = await new RegistratieVerlopenProcessor( + client, Worker(new FakeRegistrationStore()), NullLogger.Instance).PumpOnceAsync(5); + + Assert.Equal(0, acquired); + Assert.Equal(1, client.AcquireCount); + Assert.Empty(client.Completed); + } +}