test(backend): type-state Aanvraag builder, illegal fixtures unrepresentable (WP-70)
RuleTests/OpenZaakZaakSourceTests hand-built Aanvraag fixtures by initializer, keeping Submitted/Referentie/SubmittedAt/BesluitStatus consistent by hand. A type-state builder (Given.Concept().Submitted().Decided()) makes an illegal sequence a compile error instead, and delegates the toelichting-required rule to the real BeoordelingRules so it can't drift from production. Adds BesluitLifecycleTests covering the WP-68 besluit invariants end to end. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,111 @@
|
||||
using System.Net;
|
||||
using System.Net.Http.Json;
|
||||
using BigRegister.Api.Contracts;
|
||||
using BigRegister.Api.Data;
|
||||
using BigRegister.Domain.Applications;
|
||||
using BigRegister.Domain.Beoordeling;
|
||||
using BigRegister.Tests.Builders;
|
||||
|
||||
namespace BigRegister.Tests.Acceptance;
|
||||
|
||||
/// <summary>
|
||||
/// Behaviour-level tests for the besluit lifecycle (WP-65b/66/68), built through the
|
||||
/// <see cref="Given"/> type-state builder (WP-70) rather than the full wizard/upload dance
|
||||
/// <see cref="BeoordelingTests"/> uses — a fixture that's already Submitted (or already
|
||||
/// Decided) is a two-line Given, not fifteen. Each test persists its own Given-built
|
||||
/// <see cref="Aanvraag"/> straight into the isolated per-class SQLite file (no HTTP round trip
|
||||
/// needed to create it) and exercises the real write path from there.
|
||||
/// </summary>
|
||||
public class BesluitLifecycleTests(TestWebApplicationFactory factory) : IClassFixture<TestWebApplicationFactory>
|
||||
{
|
||||
// Booting the client (once, here) is what makes Db.ConnectionString point at THIS class's
|
||||
// throwaway file and runs its migrations — see TestWebApplicationFactory's own docs.
|
||||
private readonly HttpClient _client = factory.CreateClient();
|
||||
|
||||
private static void Persist(Aanvraag aanvraag)
|
||||
{
|
||||
using var db = Db.Create();
|
||||
db.Applications.Add(aanvraag);
|
||||
db.SaveChanges();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void A_terminal_besluit_is_frozen()
|
||||
{
|
||||
// Given a case already decided Goedgekeurd — terminal, per BeoordelingRules.CanDecide.
|
||||
var aanvraag = Given.Concept(type: "registratie").Submitted().Decided(Besluit.Goedkeuren).Build();
|
||||
Persist(aanvraag);
|
||||
|
||||
// When a behandelaar tries to record a further besluit on it...
|
||||
var (outcome, updated) = ApplicationStore.RecordBesluit(aanvraag.Id, Besluit.Afwijzen, "te laat", DateTimeOffset.UtcNow);
|
||||
|
||||
// Then the write is refused, and the original decision still stands.
|
||||
Assert.Equal(ApplicationStore.RecordBesluitOutcome.Conflict, outcome);
|
||||
Assert.Null(updated);
|
||||
var stillGoedgekeurd = ApplicationStore.GetAny(aanvraag.Id)!.StatusAt(DateTimeOffset.UtcNow);
|
||||
Assert.Equal(AanvraagStatusTag.Goedgekeurd, stillGoedgekeurd.Tag);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MeerInfoGevraagd_can_be_decided_again()
|
||||
{
|
||||
// Given a case a behandelaar sent back for more information — not terminal.
|
||||
var aanvraag = Given.Concept(type: "registratie").Submitted().Decided(Besluit.MeerInfoOpvragen, "stuur een geldig diploma").Build();
|
||||
Persist(aanvraag);
|
||||
|
||||
// When a further besluit is recorded on it...
|
||||
var (outcome, updated) = ApplicationStore.RecordBesluit(aanvraag.Id, Besluit.Goedkeuren, null, DateTimeOffset.UtcNow);
|
||||
|
||||
// Then, unlike a terminal decision, it succeeds and advances the status.
|
||||
Assert.Equal(ApplicationStore.RecordBesluitOutcome.Ok, outcome);
|
||||
Assert.Equal(AanvraagStatusTag.Goedgekeurd, updated!.StatusAt(DateTimeOffset.UtcNow).Tag);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void A_recorded_decision_wins_over_the_auto_approve_computation()
|
||||
{
|
||||
// Given an auto-approvable submission that a behandelaar decides (Afwijzen) before the
|
||||
// auto-approve window would otherwise have closed it as Goedgekeurd.
|
||||
var aanvraag = Given.Concept(type: "registratie").Submitted(autoApprovable: true).Build();
|
||||
Persist(aanvraag);
|
||||
var (outcome, _) = ApplicationStore.RecordBesluit(aanvraag.Id, Besluit.Afwijzen, "diploma niet erkend", DateTimeOffset.UtcNow);
|
||||
Assert.Equal(ApplicationStore.RecordBesluitOutcome.Ok, outcome);
|
||||
|
||||
// When the status is read long after the auto-approve window has passed — the instant an
|
||||
// undecided auto-approvable case of the same shape WOULD read Goedgekeurd (see
|
||||
// ApplicationTests.AutoApprovable_flips_to_goedgekeurd_after_the_window)...
|
||||
var longAfterTheWindow = aanvraag.SubmittedAt!.Value + ApplicationStore.ProcessingWindow + TimeSpan.FromDays(1);
|
||||
var status = ApplicationStore.GetAny(aanvraag.Id)!.StatusAt(longAfterTheWindow);
|
||||
|
||||
// Then the recorded decision still wins — Afgewezen, never Goedgekeurd.
|
||||
Assert.Equal(AanvraagStatusTag.Afgewezen, status.Tag);
|
||||
}
|
||||
|
||||
private Task<HttpResponseMessage> PostBesluit(string id, object body)
|
||||
{
|
||||
var req = new HttpRequestMessage(HttpMethod.Post, $"/api/v1/beoordeling/{id}/besluit") { Content = JsonContent.Create(body) };
|
||||
req.Headers.Add("X-Medewerker", "medewerker-1");
|
||||
return _client.SendAsync(req);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Afwijzen_requires_a_toelichting()
|
||||
{
|
||||
// Given an open, decidable case (no decision recorded yet).
|
||||
var aanvraag = Given.Concept(type: "registratie").Submitted().Build();
|
||||
Persist(aanvraag);
|
||||
|
||||
// When a behandelaar posts Afwijzen with no toelichting...
|
||||
var missing = await PostBesluit(aanvraag.Id, new { besluit = "Afwijzen" });
|
||||
|
||||
// Then the request is rejected — the wire boundary enforces the same rule
|
||||
// (BeoordelingRules.RequiresToelichting) the builder enforces for a built fixture.
|
||||
Assert.Equal(HttpStatusCode.BadRequest, missing.StatusCode);
|
||||
|
||||
// And the identical request WITH a toelichting succeeds.
|
||||
var withToelichting = await PostBesluit(aanvraag.Id, new { besluit = "Afwijzen", toelichting = "Diploma niet erkend" });
|
||||
withToelichting.EnsureSuccessStatusCode();
|
||||
var body = (await withToelichting.Content.ReadFromJsonAsync<RecordBesluitResponse>())!;
|
||||
Assert.Equal("Afgewezen", body.Status.Tag);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user