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>
124 lines
4.5 KiB
C#
124 lines
4.5 KiB
C#
using System.Net;
|
|
using System.Net.Http.Json;
|
|
using BigRegister.Api.Contracts;
|
|
using BigRegister.Api.Data;
|
|
using BigRegister.Domain.Applications;
|
|
using BigRegister.Tests.Builders;
|
|
|
|
namespace BigRegister.Tests.Acceptance;
|
|
|
|
/// <summary>
|
|
/// Behaviour-level tests for the scholing-threshold enforcement over
|
|
/// <c>POST /aanvragen/{id}/submit</c> (the wizard's real path — a later change deleted the legacy
|
|
/// <c>POST /intakes</c> endpoint this once also covered). Built through the <see
|
|
/// cref="Given"/> type-state builder, mirroring <see cref="BesluitLifecycleTests"/> rather
|
|
/// than the full wizard/upload dance — the builder's default owner IS <see
|
|
/// cref="BigRegister.Api.Domain.Authorization.StubIdentityProvider"/>'s default caller, so
|
|
/// no header juggling.
|
|
/// </summary>
|
|
public class IntakeSubmissionTests(TestWebApplicationFactory factory) : IClassFixture<TestWebApplicationFactory>
|
|
{
|
|
private readonly HttpClient _client = factory.CreateClient();
|
|
|
|
private static void Persist(Aanvraag aanvraag)
|
|
{
|
|
using var db = Db.Create();
|
|
db.Applications.Add(aanvraag.ToEntity());
|
|
db.SaveChanges();
|
|
}
|
|
|
|
private Task<HttpResponseMessage> Submit(string id, object body) =>
|
|
_client.PostAsJsonAsync($"/api/v1/aanvragen/{id}/submit", body);
|
|
|
|
[Fact]
|
|
public async Task Below_threshold_without_an_answer_is_rejected_and_stays_a_concept()
|
|
{
|
|
// Given an intake-typed Concept aanvraag (not yet submitted).
|
|
var aanvraag = Given.Concept(type: "intake").Build();
|
|
Persist(aanvraag);
|
|
|
|
// When it is submitted with uren below the threshold and no scholing answer at all...
|
|
var res = await Submit(aanvraag.Id, new { uren = 500 });
|
|
|
|
// Then the request is rejected as a contract violation (400, not a merit rejection)...
|
|
Assert.Equal(HttpStatusCode.BadRequest, res.StatusCode);
|
|
|
|
// ...and the aanvraag is left a retryable Concept, never marked Submitted.
|
|
Assert.IsType<Aanvraag.Concept>(ApplicationStore.GetAny(aanvraag.Id));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Below_threshold_with_an_answer_succeeds()
|
|
{
|
|
// Given an intake-typed Concept.
|
|
var aanvraag = Given.Concept(type: "intake").Build();
|
|
Persist(aanvraag);
|
|
|
|
// When submitted below the threshold with "niet gevolgd" — a complete, legal answer...
|
|
var res = await Submit(aanvraag.Id, new { uren = 500, aanvullendeScholing = false });
|
|
|
|
// Then the submission succeeds.
|
|
res.EnsureSuccessStatusCode();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Above_threshold_needs_no_answer()
|
|
{
|
|
// Given an intake-typed Concept.
|
|
var aanvraag = Given.Concept(type: "intake").Build();
|
|
Persist(aanvraag);
|
|
|
|
// When submitted with uren at/above the threshold and no scholing answer...
|
|
var res = await Submit(aanvraag.Id, new { uren = 1000 });
|
|
|
|
// Then it succeeds — the question is moot above the threshold.
|
|
res.EnsureSuccessStatusCode();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Punten_without_gevolgd_is_rejected()
|
|
{
|
|
// Given an intake-typed Concept.
|
|
var aanvraag = Given.Concept(type: "intake").Build();
|
|
Persist(aanvraag);
|
|
|
|
// When submitted above the threshold with punten but no "gevolgd" answer — the stale
|
|
// shape §6 fixes on the frontend, still reachable as a crafted POST...
|
|
var res = await Submit(aanvraag.Id, new { uren = 1500, scholingPunten = 150 });
|
|
|
|
// Then it is rejected.
|
|
Assert.Equal(HttpStatusCode.BadRequest, res.StatusCode);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Herregistratie_is_unaffected_by_the_intake_only_gate()
|
|
{
|
|
// Given a herregistratie-typed Concept (no scholing question in that wizard).
|
|
var aanvraag = Given.Concept(type: "herregistratie").Build();
|
|
Persist(aanvraag);
|
|
|
|
// When submitted below the intake threshold with no scholing answer at all...
|
|
var res = await Submit(aanvraag.Id, new { uren = 500 });
|
|
|
|
// Then it still succeeds — the gate is intake-only.
|
|
res.EnsureSuccessStatusCode();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Zero_uren_is_still_afgewezen_not_a_400()
|
|
{
|
|
// Given an intake-typed Concept.
|
|
var aanvraag = Given.Concept(type: "intake").Build();
|
|
Persist(aanvraag);
|
|
|
|
// When submitted with zero uren and no scholing answer — completeness would also
|
|
// reject this, but the merit rejection (RejectZeroUren) must win (the ordering guard)...
|
|
var res = await Submit(aanvraag.Id, new { uren = 0 });
|
|
|
|
// Then the submission is accepted and resolves to Afgewezen — not a 400.
|
|
res.EnsureSuccessStatusCode();
|
|
var body = (await res.Content.ReadFromJsonAsync<AanvraagIndienenResponse>())!;
|
|
Assert.Equal("Afgewezen", body.Status.Tag);
|
|
}
|
|
}
|