test(backend): cover WP-68 F2/F6/T3 (besluit concurrency, toelichting rule, transitions)
- 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 <noreply@anthropic.com>
This commit is contained in:
@@ -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]
|
[Fact]
|
||||||
public async Task Unknown_id_404s_and_zorgverlener_is_forbidden()
|
public async Task Unknown_id_404s_and_zorgverlener_is_forbidden()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -193,4 +193,49 @@ public class BeoordelingRuleTests
|
|||||||
[InlineData(AanvraagStatusTag.Afgewezen, false)]
|
[InlineData(AanvraagStatusTag.Afgewezen, false)]
|
||||||
public void Only_open_statuses_are_decidable(AanvraagStatusTag tag, bool expected) =>
|
public void Only_open_statuses_are_decidable(AanvraagStatusTag tag, bool expected) =>
|
||||||
Assert.Equal(expected, BeoordelingRules.CanDecide(tag));
|
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));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user