ADR-0001's own canonical "config value" example was unenforced: GET
/intake/policy echoed ScholingThreshold, but no request DTO carried a
scholing answer, so the server had nothing to re-validate. A crafted
POST could skip a requirement the wizard presents as mandatory.
IntakePolicy.RejectIncompleteScholing is the authority — three-valued
completeness (below threshold an answer is required; "nee" is legal and
still submits; punten only belong to a followed scholing), living in the
class that owns the constant so scripts/check-seam.sh keeps guarding the
FE/BE literal pair. Both submit paths call it; a violation 400s with
ProblemDetails and leaves the aanvraag a Concept. Gated on
Type == "intake" (the endpoint's switch lumps herregistratie with
intake, which has no scholing question), and guarded by `reject is null`
so a zero-uren submission is still decided on its merits.
Also fixes a live FE bug in the same rule: validateStep required punten
whenever scholingGevolgd was 'ja' regardless of lageUren, while the
template renders those fields only when lageUren — so answering 'ja'
then raising uren either blocked the user on an invisible field or
emitted aanvullendeScholing: undefined alongside punten. punten now
derives from aanvullendeScholing, so that combination is unrepresentable
in ValidIntake.
Note: EndpointTests' Worked_hours_submission_succeeds was itself
asserting the vulnerable payload ({ uren: 40 }, no answer) and needed a
complete answer added; the zero-hours rows are the ordering regression
net and are unmodified.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
44 lines
1.9 KiB
C#
44 lines
1.9 KiB
C#
using BigRegister.Domain.Intake;
|
|
|
|
namespace BigRegister.Tests.Domain;
|
|
|
|
public class IntakeRuleTests
|
|
{
|
|
// The arguments ARE the Given (WP-69/bdd.mdx) — these degenerate to When/Then.
|
|
|
|
[Fact]
|
|
public void Below_threshold_with_no_answer_is_incomplete() =>
|
|
Assert.NotNull(IntakePolicy.RejectIncompleteScholing(999, aanvullendeScholing: null, scholingPunten: null));
|
|
|
|
[Fact]
|
|
public void At_the_threshold_no_answer_is_required() =>
|
|
// Pins `<` vs `<=` — lageUren's own boundary.
|
|
Assert.Null(IntakePolicy.RejectIncompleteScholing(1000, aanvullendeScholing: null, scholingPunten: null));
|
|
|
|
[Fact]
|
|
public void Niet_gevolgd_is_a_complete_answer_below_threshold() =>
|
|
// "nee" is legal — this WP is completeness, not merit (§1's scope).
|
|
Assert.Null(IntakePolicy.RejectIncompleteScholing(500, aanvullendeScholing: false, scholingPunten: null));
|
|
|
|
[Fact]
|
|
public void Gevolgd_without_punten_is_incomplete() =>
|
|
Assert.NotNull(IntakePolicy.RejectIncompleteScholing(500, aanvullendeScholing: true, scholingPunten: null));
|
|
|
|
[Fact]
|
|
public void Gevolgd_with_zero_punten_is_valid() =>
|
|
Assert.Null(IntakePolicy.RejectIncompleteScholing(500, aanvullendeScholing: true, scholingPunten: 0));
|
|
|
|
[Fact]
|
|
public void Gevolgd_with_negative_punten_is_refused() =>
|
|
Assert.NotNull(IntakePolicy.RejectIncompleteScholing(500, aanvullendeScholing: true, scholingPunten: -1));
|
|
|
|
[Theory]
|
|
[InlineData(null)] // stale-punten shape (§6): raising uren above threshold left an unanswered
|
|
// question but punten still set from when it was visible
|
|
[InlineData(false)]
|
|
public void Punten_without_gevolgd_is_refused(bool? aanvullendeScholing) =>
|
|
// uren ABOVE threshold so the "missing answer" branch can't also explain the rejection —
|
|
// this row isolates the "stale punten" rule on its own.
|
|
Assert.NotNull(IntakePolicy.RejectIncompleteScholing(1500, aanvullendeScholing, scholingPunten: 150));
|
|
}
|