From 31d4aa18489e50c9ae617d1fd22c6f1c5bead58c Mon Sep 17 00:00:00 2001 From: Edwin van den Houdt Date: Wed, 5 Aug 2026 15:38:30 +0200 Subject: [PATCH] test(backend): cover WP-68 F2/F6/T3 (besluit concurrency, toelichting rule, transitions) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Concurrent_besluiten_on_the_same_aanvraag_yield_exactly_one_success: races two besluiten on the same open aanvraag, asserts exactly one 200 and one 409 — the behavior F2's in-lock guard exists to guarantee. - Only_a_non_approval_requires_a_toelichting: unit test for BeoordelingRules.RequiresToelichting (F6). - A_terminal_decision_refuses_any_further_besluit / MeerInfoOpvragen_is_not_terminal_a_further_besluit_is_still_legal: the transition table at the aggregate level (T3) — an Aanvraag whose BesluitStatus already records a decision computes a terminal StatusAt, and CanDecide refuses a further besluit, independent of the endpoint-level equivalent. Co-Authored-By: Claude Opus 5 --- .../BigRegister.Tests/BeoordelingTests.cs | 22 +++++++++ backend/tests/BigRegister.Tests/RuleTests.cs | 45 +++++++++++++++++++ 2 files changed, 67 insertions(+) diff --git a/backend/tests/BigRegister.Tests/BeoordelingTests.cs b/backend/tests/BigRegister.Tests/BeoordelingTests.cs index de65c67..e38427e 100644 --- a/backend/tests/BigRegister.Tests/BeoordelingTests.cs +++ b/backend/tests/BigRegister.Tests/BeoordelingTests.cs @@ -215,6 +215,28 @@ public class BeoordelingTests(TestWebApplicationFactory factory) : IClassFixture } } + // WP-68 (F2): the transition-legality check now runs inside RecordBesluit's write lock, so + // two besluiten racing on the same still-open aanvraag can't both pass the check before + // either writes — exactly one commits, the other sees the now-terminal status. + [Fact] + public async Task Concurrent_besluiten_on_the_same_aanvraag_yield_exactly_one_success() + { + var (a, _) = await CreateManualCaseWithDocument(); + try + { + var results = await Task.WhenAll( + PostBesluit(a.Id, new { besluit = "Goedkeuren" }), + PostBesluit(a.Id, new { besluit = "Afwijzen", toelichting = "race" })); + + Assert.Single(results, r => r.StatusCode == HttpStatusCode.OK); + Assert.Single(results, r => r.StatusCode == HttpStatusCode.Conflict); + } + finally + { + await DeleteAsAdmin(a.Id); + } + } + [Fact] public async Task Unknown_id_404s_and_zorgverlener_is_forbidden() { diff --git a/backend/tests/BigRegister.Tests/RuleTests.cs b/backend/tests/BigRegister.Tests/RuleTests.cs index bd88643..c0cde38 100644 --- a/backend/tests/BigRegister.Tests/RuleTests.cs +++ b/backend/tests/BigRegister.Tests/RuleTests.cs @@ -193,4 +193,49 @@ public class BeoordelingRuleTests [InlineData(AanvraagStatusTag.Afgewezen, false)] public void Only_open_statuses_are_decidable(AanvraagStatusTag tag, bool expected) => Assert.Equal(expected, BeoordelingRules.CanDecide(tag)); + + // WP-68 (F6): the toelichting rule, moved here from an inline endpoint check. + [Theory] + [InlineData(Besluit.Goedkeuren, false)] + [InlineData(Besluit.Afwijzen, true)] + [InlineData(Besluit.MeerInfoOpvragen, true)] + public void Only_a_non_approval_requires_a_toelichting(Besluit besluit, bool expected) => + Assert.Equal(expected, BeoordelingRules.RequiresToelichting(besluit)); + + // WP-68 (T3): the transition table at the AGGREGATE level, not just against a bare tag — + // an Aanvraag whose BesluitStatus already records a terminal decision computes a terminal + // StatusAt, and CanDecide refuses a further besluit regardless of which one. Pins the + // domain statement "Afgewezen/Goedgekeurd → no further besluit" independent of the + // endpoint's own (integration-level) Already_decided_case_rejects_a_further_besluit. + private static Aanvraag Decided(Besluit besluit) => new() + { + Id = "x", + Type = "registratie", + Owner = "test", + Submitted = true, + Referentie = "BIG-2026-1", + SubmittedAt = DateTimeOffset.UtcNow, + CreatedAt = DateTimeOffset.UtcNow, + UpdatedAt = DateTimeOffset.UtcNow, + BesluitStatus = besluit, + BesluitToelichting = besluit == Besluit.Goedkeuren ? null : "toelichting", + }; + + [Theory] + [InlineData(Besluit.Goedkeuren)] + [InlineData(Besluit.Afwijzen)] + public void A_terminal_decision_refuses_any_further_besluit(Besluit recorded) + { + var now = DateTimeOffset.UtcNow; + var tag = Decided(recorded).StatusAt(now).Tag!.Value; + Assert.False(BeoordelingRules.CanDecide(tag)); + } + + [Fact] + public void MeerInfoOpvragen_is_not_terminal_a_further_besluit_is_still_legal() + { + var now = DateTimeOffset.UtcNow; + var tag = Decided(Besluit.MeerInfoOpvragen).StatusAt(now).Tag!.Value; + Assert.True(BeoordelingRules.CanDecide(tag)); + } }