The backend half of the sweep RD-18 did for the front end. git blame holds the provenance and stays correct when the code moves; the comment names a closed ticket and tells the reader nothing the sentence around it does not. public/letter.css and LetterHtml.golden.html change together, because the renderer inlines the CSS and the golden file snapshots the result. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
198 lines
8.3 KiB
C#
198 lines
8.3 KiB
C#
using System.Threading;
|
|
using BigRegister.Api.Data;
|
|
using BigRegister.Domain.Applications;
|
|
|
|
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"/>. "Build
|
|
/// test data through the same door production code uses" — <see cref="Aanvraag"/> itself is now
|
|
/// the closed Concept/Submitted/Decided union, so this builder no longer needs
|
|
/// to mirror production's guards (step-index bounds, "Afwijzen needs a toelichting") by hand —
|
|
/// it just calls the real nested constructors/required members, which enforce them. A call that
|
|
/// would build an illegal Aanvraag (e.g. deciding a still-Concept aanvraag, or an Afwijzen with
|
|
/// no toelichting) is refused the same way production refuses it: a still-Concept aanvraag has
|
|
/// no <c>.Decided(...)</c> to call in the first place, and a missing toelichting is a runtime
|
|
/// guard identical to <c>ApplicationStore.RecordBesluit</c>'s own. Start at
|
|
/// <see cref="Given.Concept"/>.
|
|
/// </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"/>.
|
|
/// Bounds are <see cref="Aanvraag.Concept"/>'s OWN constructor's to enforce, not this
|
|
/// builder's — an out-of-range pair fails at <see cref="Build"/>, the same
|
|
/// <see cref="ArgumentOutOfRangeException"/> production throws, not a guard restated here.
|
|
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 a fixture built this way can never hit the
|
|
/// null-forgiving derefs the earlier flat Aanvraag needed (there's nothing to force any
|
|
/// more: both are required, non-null members of <see cref="Aanvraag.Submitted"/>).
|
|
public SubmittedAanvraag Submitted(bool autoApprovable = false) => new(_type, _owner, autoApprovable);
|
|
|
|
public Aanvraag.Concept Build() => new(_stepIndex, _stepCount)
|
|
{
|
|
Id = Guid.NewGuid().ToString(),
|
|
Type = _type,
|
|
Owner = _owner,
|
|
DocumentIds = Array.Empty<string>(),
|
|
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 <see cref="ConceptAanvraag"/>.</summary>
|
|
public sealed class SubmittedAanvraag
|
|
{
|
|
private static int _referentieSeq;
|
|
|
|
private readonly string _type;
|
|
private readonly string _owner;
|
|
private readonly bool _autoApprovable;
|
|
private readonly string _referentie;
|
|
private readonly DateTimeOffset _submittedAt;
|
|
private string? _zaakUrl;
|
|
|
|
internal SubmittedAanvraag(string type, string owner, bool autoApprovable)
|
|
{
|
|
_type = type;
|
|
_owner = owner;
|
|
_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>Registers this aanvraag's already-known OpenZaak zaak URL — mirrors
|
|
/// <see cref="ApplicationStore.SetZaakUrl"/>, the one production writer of this field, so a
|
|
/// fixture that needs a pre-existing zaak doesn't reach past <c>Build()</c> to mutate the
|
|
/// result by hand.</summary>
|
|
public SubmittedAanvraag WithZaakUrl(string zaakUrl)
|
|
{
|
|
_zaakUrl = zaakUrl;
|
|
return this;
|
|
}
|
|
|
|
/// <summary>Records a behandelaar's decision. Unlike the earlier builder, there is no
|
|
/// hand-written toelichting guard mirroring <c>BeoordelingRules.RequiresToelichting</c> any
|
|
/// more — <see cref="Aanvraag.Decided.Afgewezen"/>/<see cref="Aanvraag.Decided.MeerInfoGevraagd"/>
|
|
/// simply have a `required string Toelichting` member; the null-coalescing throw below is the
|
|
/// one place a null has to turn into an exception (this method's own parameter is still the
|
|
/// nullable <c>string?</c> a wire request would carry), same failure production's own
|
|
/// <c>ApplicationStore.RecordBesluit</c> raises for the identical input.</summary>
|
|
public DecidedAanvraag Decided(Besluit besluit, string? toelichting = null) => new(BuildDecided(besluit, toelichting));
|
|
|
|
private Aanvraag.Decided BuildDecided(Besluit besluit, string? toelichting)
|
|
{
|
|
var (id, createdAt) = (Guid.NewGuid().ToString(), _submittedAt);
|
|
return besluit switch
|
|
{
|
|
Besluit.Goedkeuren => new Aanvraag.Decided.Goedgekeurd
|
|
{
|
|
Id = id,
|
|
Type = _type,
|
|
Owner = _owner,
|
|
DocumentIds = Array.Empty<string>(),
|
|
CreatedAt = createdAt,
|
|
UpdatedAt = createdAt,
|
|
ZaakUrl = _zaakUrl,
|
|
Referentie = _referentie,
|
|
SubmittedAt = _submittedAt,
|
|
},
|
|
Besluit.Afwijzen => new Aanvraag.Decided.Afgewezen
|
|
{
|
|
Id = id,
|
|
Type = _type,
|
|
Owner = _owner,
|
|
DocumentIds = Array.Empty<string>(),
|
|
CreatedAt = createdAt,
|
|
UpdatedAt = createdAt,
|
|
ZaakUrl = _zaakUrl,
|
|
Referentie = _referentie,
|
|
SubmittedAt = _submittedAt,
|
|
Toelichting = toelichting ?? throw new ArgumentException("Afwijzen requires a toelichting.", nameof(toelichting)),
|
|
},
|
|
Besluit.MeerInfoOpvragen => new Aanvraag.Decided.MeerInfoGevraagd
|
|
{
|
|
Id = id,
|
|
Type = _type,
|
|
Owner = _owner,
|
|
DocumentIds = Array.Empty<string>(),
|
|
CreatedAt = createdAt,
|
|
UpdatedAt = createdAt,
|
|
ZaakUrl = _zaakUrl,
|
|
Referentie = _referentie,
|
|
SubmittedAt = _submittedAt,
|
|
Toelichting = toelichting ?? throw new ArgumentException("MeerInfoOpvragen requires a toelichting.", nameof(toelichting)),
|
|
},
|
|
_ => throw new ArgumentOutOfRangeException(nameof(besluit), besluit, "Unknown besluit."),
|
|
};
|
|
}
|
|
|
|
public Aanvraag.Submitted Build() => new()
|
|
{
|
|
Id = Guid.NewGuid().ToString(),
|
|
Type = _type,
|
|
Owner = _owner,
|
|
DocumentIds = Array.Empty<string>(),
|
|
CreatedAt = _submittedAt,
|
|
UpdatedAt = _submittedAt,
|
|
ZaakUrl = _zaakUrl,
|
|
Referentie = _referentie,
|
|
SubmittedAt = _submittedAt,
|
|
AutoApprovable = _autoApprovable,
|
|
};
|
|
}
|
|
|
|
/// <summary>A submitted aanvraag with a behandelaar's decision already recorded — terminal in
|
|
/// the builder too, matching Goedgekeurd/Afgewezen being terminal in the domain
|
|
/// (<see cref="BigRegister.Domain.Beoordeling.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. Just a one-line
|
|
/// wrapper around the already-fully-built <see cref="Aanvraag.Decided"/> value — construction moved
|
|
/// all the actual construction (and its invariant enforcement) into
|
|
/// <see cref="SubmittedAanvraag.Decided"/> itself, so there's nothing left for this type to do
|
|
/// except keep <c>.Decided(...).Build()</c> a valid two-call chain for the existing test
|
|
/// suite.</summary>
|
|
public sealed class DecidedAanvraag(Aanvraag.Decided value)
|
|
{
|
|
public Aanvraag.Decided Build() => value;
|
|
}
|