Applications, documents (+ audit log) and the brief move off static in-memory Dictionaries onto a real SQLite file via EF Core, so demo data survives a process restart or `docker compose restart api` for the first time. The three stores (ApplicationStore/DocumentStore/BriefStore) keep their exact public signatures and static-class shape — no DI, no async ripple into Program.cs's minimal-API handlers — each method just opens a short-lived AppDbContext via Db.Create() under the same lock it already had. Opaque nested shapes (a wizard's draft snapshot, a brief's sections/placeholders/status) are stored as JSON text columns rather than redesigned into relational tables, matching the existing "don't interpret it" posture. Found two things the WP's own text got wrong, corrected in docs/backlog/WP-22-durable-persistence.md's Deviations section: SeedData never seeded these three stores (only the read-only BRP/DUO-mimicking GETs, which stay in-memory) so there's no seed step; and no new docker-compose volume is needed since the existing bind mount already covers the SQLite file — verified against this environment's real podman-backed compose stack, not just by reading the file. Also: pinned SQLitePCLRaw.bundle_e_sqlite3 to 3.0.3 (EF Core Sqlite's own transitive default bundles a pre-3.50.2 SQLite with a known high-severity memory-corruption advisory); found and fixed a real xUnit test race where concurrent test-class hosts stomped a shared static connection-string field, fixed by disabling cross-class test parallelization rather than adding DI the stores don't otherwise need. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
143 lines
5.5 KiB
C#
143 lines
5.5 KiB
C#
using System.Net;
|
|
using System.Net.Http.Json;
|
|
using BigRegister.Api.Contracts;
|
|
using BigRegister.Api.Data;
|
|
using Microsoft.AspNetCore.Mvc.Testing;
|
|
|
|
namespace BigRegister.Tests;
|
|
|
|
public class ApplicationTests(TestWebApplicationFactory factory) : IClassFixture<TestWebApplicationFactory>
|
|
{
|
|
private readonly HttpClient _client = factory.CreateClient();
|
|
|
|
private async Task<ApplicationDetailDto> Create(string type = "registratie")
|
|
{
|
|
var res = await _client.PostAsJsonAsync("/api/v1/applications", new { type });
|
|
Assert.Equal(HttpStatusCode.Created, res.StatusCode);
|
|
return (await res.Content.ReadFromJsonAsync<ApplicationDetailDto>())!;
|
|
}
|
|
|
|
private Task<List<ApplicationSummaryDto>?> List() =>
|
|
_client.GetFromJsonAsync<List<ApplicationSummaryDto>>("/api/v1/applications");
|
|
|
|
// --- Lifecycle over HTTP ---
|
|
|
|
[Fact]
|
|
public async Task Create_then_list_shows_a_concept_with_step_progress()
|
|
{
|
|
var a = await Create();
|
|
await _client.PutAsJsonAsync($"/api/v1/applications/{a.Id}",
|
|
new { draft = new { beroep = "arts" }, stepIndex = 1, stepCount = 4 });
|
|
|
|
var mine = (await List())!.Single(x => x.Id == a.Id);
|
|
Assert.Equal("Concept", mine.Status.Tag);
|
|
Assert.Equal(1, mine.Status.StepIndex);
|
|
Assert.Equal(4, mine.Status.StepCount);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Draft_sync_is_readable_back_from_detail()
|
|
{
|
|
var a = await Create();
|
|
await _client.PutAsJsonAsync($"/api/v1/applications/{a.Id}",
|
|
new { draft = new { beroep = "verpleegkundige" }, stepIndex = 2, stepCount = 4 });
|
|
|
|
var detail = await _client.GetFromJsonAsync<ApplicationDetailDto>($"/api/v1/applications/{a.Id}");
|
|
Assert.NotNull(detail!.Draft);
|
|
Assert.Equal("verpleegkundige", detail.Draft!.Value.GetProperty("beroep").GetString());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Submit_duo_registratie_is_in_behandeling_and_auto()
|
|
{
|
|
var a = await Create("registratie");
|
|
var res = await _client.PostAsJsonAsync($"/api/v1/applications/{a.Id}/submit", new { diplomaHerkomst = "duo" });
|
|
res.EnsureSuccessStatusCode();
|
|
var body = (await res.Content.ReadFromJsonAsync<SubmitApplicationResponse>())!;
|
|
Assert.StartsWith("BIG-2026-", body.Referentie);
|
|
Assert.Equal("InBehandeling", body.Status.Tag);
|
|
Assert.False(body.Status.Manual); // auto-approvable → not a manual case
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Submit_handmatig_registratie_succeeds_as_manual_case()
|
|
{
|
|
var a = await Create("registratie");
|
|
var res = await _client.PostAsJsonAsync($"/api/v1/applications/{a.Id}/submit", new { diplomaHerkomst = "handmatig" });
|
|
res.EnsureSuccessStatusCode(); // no longer a 422
|
|
var body = (await res.Content.ReadFromJsonAsync<SubmitApplicationResponse>())!;
|
|
Assert.Equal("InBehandeling", body.Status.Tag);
|
|
Assert.True(body.Status.Manual);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Submit_herregistratie_with_zero_uren_is_afgewezen()
|
|
{
|
|
var a = await Create("herregistratie");
|
|
var res = await _client.PostAsJsonAsync($"/api/v1/applications/{a.Id}/submit", new { uren = 0 });
|
|
res.EnsureSuccessStatusCode(); // the submission is accepted...
|
|
var body = (await res.Content.ReadFromJsonAsync<SubmitApplicationResponse>())!;
|
|
Assert.Equal("Afgewezen", body.Status.Tag); // ...but resolves to rejected
|
|
Assert.NotNull(body.Status.Reden);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Submitting_twice_conflicts()
|
|
{
|
|
var a = await Create("registratie");
|
|
(await _client.PostAsJsonAsync($"/api/v1/applications/{a.Id}/submit", new { diplomaHerkomst = "duo" })).EnsureSuccessStatusCode();
|
|
var again = await _client.PostAsJsonAsync($"/api/v1/applications/{a.Id}/submit", new { diplomaHerkomst = "duo" });
|
|
Assert.Equal(HttpStatusCode.Conflict, again.StatusCode);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Cancel_concept_removes_it()
|
|
{
|
|
var a = await Create();
|
|
Assert.Equal(HttpStatusCode.NoContent, (await _client.DeleteAsync($"/api/v1/applications/{a.Id}")).StatusCode);
|
|
Assert.Equal(HttpStatusCode.NotFound, (await _client.GetAsync($"/api/v1/applications/{a.Id}")).StatusCode);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Cancel_submitted_aanvraag_conflicts()
|
|
{
|
|
var a = await Create("registratie");
|
|
(await _client.PostAsJsonAsync($"/api/v1/applications/{a.Id}/submit", new { diplomaHerkomst = "duo" })).EnsureSuccessStatusCode();
|
|
Assert.Equal(HttpStatusCode.Conflict, (await _client.DeleteAsync($"/api/v1/applications/{a.Id}")).StatusCode);
|
|
}
|
|
|
|
// --- Auto-approval is computed on read: exercise the window boundary without waiting. ---
|
|
|
|
private static Aanvraag Accepted(bool autoApprovable) => new()
|
|
{
|
|
Id = "x",
|
|
Type = "registratie",
|
|
Owner = "test",
|
|
Submitted = true,
|
|
AutoApprovable = autoApprovable,
|
|
Referentie = "BIG-2026-1",
|
|
SubmittedAt = DateTimeOffset.UtcNow,
|
|
CreatedAt = DateTimeOffset.UtcNow,
|
|
UpdatedAt = DateTimeOffset.UtcNow,
|
|
};
|
|
|
|
[Fact]
|
|
public void AutoApprovable_flips_to_goedgekeurd_after_the_window()
|
|
{
|
|
var a = Accepted(autoApprovable: true);
|
|
var t0 = a.SubmittedAt!.Value;
|
|
Assert.Equal("InBehandeling", a.ToStatusDto(t0 + ApplicationStore.ProcessingWindow - TimeSpan.FromSeconds(1)).Tag);
|
|
Assert.Equal("Goedgekeurd", a.ToStatusDto(t0 + ApplicationStore.ProcessingWindow + TimeSpan.FromSeconds(1)).Tag);
|
|
}
|
|
|
|
[Fact]
|
|
public void Manual_case_never_auto_advances()
|
|
{
|
|
var a = Accepted(autoApprovable: false);
|
|
var far = a.SubmittedAt!.Value + ApplicationStore.ProcessingWindow + TimeSpan.FromDays(1);
|
|
var status = a.ToStatusDto(far);
|
|
Assert.Equal("InBehandeling", status.Tag);
|
|
Assert.True(status.Manual);
|
|
}
|
|
}
|