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:
eho
2026-08-05 15:38:30 +02:00
co-authored by Claude Opus 5
parent fc6e73806a
commit 31d4aa1848
2 changed files with 67 additions and 0 deletions
@@ -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()
{