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>
161 lines
6.3 KiB
C#
161 lines
6.3 KiB
C#
using System.Threading;
|
|
using BigRegister.Api.Data;
|
|
using BigRegister.Domain.Applications;
|
|
using BigRegister.Domain.Beoordeling;
|
|
|
|
namespace BigRegister.Tests.Builders;
|
|
|
|
/// <summary>Fixture identities test builders share across the suite.</summary>
|
|
public static class TestIdentities
|
|
{
|
|
/// The default owner for a builder-made <see cref="Aanvraag"/> — matches
|
|
/// <see cref="DocumentStore.DemoOwner"/> (the demo's only seeded user) so a fixture that
|
|
/// doesn't care about identity gets a realistic, elfproef-valid BSN for free.
|
|
public const string DemoBsn = DocumentStore.DemoOwner;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Type-state test-data builder for <see cref="Aanvraag"/> (WP-70). "Build test data through the
|
|
/// same door production code uses" — a Concept can only ever become Submitted, and only a
|
|
/// Submitted aanvraag can be Decided, so the compiler refuses a fixture built through an illegal
|
|
/// path (e.g. deciding a still-Concept aanvraag) instead of that being a runtime assertion nobody
|
|
/// wrote. Start at <see cref="Given.Concept"/>.
|
|
///
|
|
/// ponytail: <see cref="Aanvraag"/> itself stays exactly what it always was — a mutable,
|
|
/// EF-backed bag with no invariants of its own (that's Data/ApplicationStore.cs's job in
|
|
/// production, via its own lock + <see cref="BeoordelingRules"/> checks). This builder does not
|
|
/// refactor it into an immutable aggregate; it's the one enforced DOOR through which TEST code
|
|
/// builds one, so the invariants a real request path enforces don't quietly go missing from a
|
|
/// fixture assembled by hand.
|
|
/// </summary>
|
|
public static class Given
|
|
{
|
|
/// A fresh, unsubmitted wizard draft — step 0 of 0 until <see cref="ConceptAanvraag.AtStep"/>
|
|
/// says otherwise, exactly what <c>ApplicationStore.CreateConcept</c> hands back.
|
|
public static ConceptAanvraag Concept(string type = "registratie", string owner = TestIdentities.DemoBsn) =>
|
|
new(type, owner);
|
|
}
|
|
|
|
/// <summary>A not-yet-submitted aanvraag. The only next step is <see cref="Submitted"/> — there
|
|
/// is deliberately no <c>Decided</c> here, since only a submitted aanvraag can be decided.</summary>
|
|
public sealed class ConceptAanvraag
|
|
{
|
|
private readonly string _type;
|
|
private readonly string _owner;
|
|
private int _stepIndex;
|
|
private int _stepCount;
|
|
|
|
internal ConceptAanvraag(string type, string owner)
|
|
{
|
|
_type = type;
|
|
_owner = owner;
|
|
}
|
|
|
|
/// The wizard's current position — step <paramref name="index"/> of <paramref name="of"/>.
|
|
public ConceptAanvraag AtStep(int index, int of)
|
|
{
|
|
_stepIndex = index;
|
|
_stepCount = of;
|
|
return this;
|
|
}
|
|
|
|
/// Submits the draft — always assigns a Referentie AND SubmittedAt together (mirrors
|
|
/// <c>ApplicationStore.Submit</c>), so <c>Aanvraag.StatusAt</c>'s <c>Referentie!</c> is honest
|
|
/// for every fixture built this way, never a null-ref waiting to happen.
|
|
public SubmittedAanvraag Submitted(bool autoApprovable = false) =>
|
|
new(_type, _owner, _stepIndex, _stepCount, autoApprovable);
|
|
|
|
public Aanvraag Build() => new()
|
|
{
|
|
Id = Guid.NewGuid().ToString(),
|
|
Type = _type,
|
|
Owner = _owner,
|
|
StepIndex = _stepIndex,
|
|
StepCount = _stepCount,
|
|
CreatedAt = DateTimeOffset.UtcNow,
|
|
UpdatedAt = DateTimeOffset.UtcNow,
|
|
};
|
|
}
|
|
|
|
/// <summary>A submitted aanvraag, open for a behandelaar's decision. The only next step is
|
|
/// <see cref="Decided"/> — there is no way back to <c>ConceptAanvraag</c>.</summary>
|
|
public sealed class SubmittedAanvraag
|
|
{
|
|
private static int _referentieSeq;
|
|
|
|
private readonly string _type;
|
|
private readonly string _owner;
|
|
private readonly int _stepIndex;
|
|
private readonly int _stepCount;
|
|
private readonly bool _autoApprovable;
|
|
private readonly string _referentie;
|
|
private readonly DateTimeOffset _submittedAt;
|
|
|
|
internal SubmittedAanvraag(string type, string owner, int stepIndex, int stepCount, bool autoApprovable)
|
|
{
|
|
_type = type;
|
|
_owner = owner;
|
|
_stepIndex = stepIndex;
|
|
_stepCount = stepCount;
|
|
_autoApprovable = autoApprovable;
|
|
// A plausible reference in SubmissionRules.NewReference's shape ("BIG-2026-" + a number) —
|
|
// sequential (not random) so a fixture's value is reproducible across a test run.
|
|
_referentie = $"BIG-2026-{Interlocked.Increment(ref _referentieSeq)}";
|
|
_submittedAt = DateTimeOffset.UtcNow;
|
|
}
|
|
|
|
/// <summary>Records a behandelaar's decision — reusing <see cref="BeoordelingRules.RequiresToelichting"/>,
|
|
/// the SAME rule production's besluit endpoint runs, rather than restating it here where it
|
|
/// could quietly drift. Throws <see cref="ArgumentException"/> for an Afwijzen/MeerInfoOpvragen
|
|
/// with a null/blank <paramref name="toelichting"/> — exactly what that endpoint rejects with
|
|
/// a 400, just caught here at fixture-build time instead.</summary>
|
|
public DecidedAanvraag Decided(Besluit besluit, string? toelichting = null)
|
|
{
|
|
if (BeoordelingRules.RequiresToelichting(besluit) && string.IsNullOrWhiteSpace(toelichting))
|
|
throw new ArgumentException($"{besluit} requires a toelichting.", nameof(toelichting));
|
|
return new DecidedAanvraag(this, besluit, toelichting);
|
|
}
|
|
|
|
public Aanvraag Build() => new()
|
|
{
|
|
Id = Guid.NewGuid().ToString(),
|
|
Type = _type,
|
|
Owner = _owner,
|
|
StepIndex = _stepIndex,
|
|
StepCount = _stepCount,
|
|
Submitted = true,
|
|
Referentie = _referentie,
|
|
AutoApprovable = _autoApprovable,
|
|
SubmittedAt = _submittedAt,
|
|
CreatedAt = _submittedAt,
|
|
UpdatedAt = _submittedAt,
|
|
};
|
|
}
|
|
|
|
/// <summary>A submitted aanvraag with a behandelaar's decision already recorded. Terminal in the
|
|
/// builder too — there's nothing past <see cref="Build"/>, matching Goedgekeurd/Afgewezen being
|
|
/// terminal in the domain (<see cref="BeoordelingRules.CanDecide"/>); a fixture that needs a
|
|
/// SECOND besluit (the MeerInfoGevraagd "still decidable" case) builds fresh from
|
|
/// <see cref="Given.Concept"/> again, exactly as a real second request would.</summary>
|
|
public sealed class DecidedAanvraag
|
|
{
|
|
private readonly SubmittedAanvraag _submitted;
|
|
private readonly Besluit _besluit;
|
|
private readonly string? _toelichting;
|
|
|
|
internal DecidedAanvraag(SubmittedAanvraag submitted, Besluit besluit, string? toelichting)
|
|
{
|
|
_submitted = submitted;
|
|
_besluit = besluit;
|
|
_toelichting = toelichting;
|
|
}
|
|
|
|
public Aanvraag Build()
|
|
{
|
|
var aanvraag = _submitted.Build();
|
|
aanvraag.BesluitStatus = _besluit;
|
|
aanvraag.BesluitToelichting = _toelichting;
|
|
return aanvraag;
|
|
}
|
|
}
|