Two work packages in one commit because both edit Program.cs and splitting
them would leave a commit that does not build.
WP-72 — deletes POST /api/v1/intakes and /herregistraties. Both were dead
from the UI (the wizard submits via /applications/{id}/submit) and strictly
less capable: they minted a bare reference and wrote no Aanvraag, made no
ZGW call, and did no document-ownership check. The shared Submit(...) helper
survives — /registrations and /change-requests still use it. WP-69 hardened
/intakes with a 400 last session; removing the surface is the stronger fix,
and WP-69's /applications/{id}/submit enforcement is untouched.
WP-73 — RegistrationStatus becomes an abstract record with three sealed
variants behind a private base ctor, so only Geregistreerd carries a
herregistratie deadline and reden is required on Geschorst/Doorgehaald
(matching the FE union, which was already right). HerregistratieRule
.IsStatusConsistent and its test are deleted: the type now guarantees what
the runtime check was for, and the test could no longer construct the
illegal state it existed to catch.
Aanvraag splits into a Concept | Submitted | Decided union with the EF row
demoted to AanvraagEntity behind a two-way mapper. Submitted carries a
non-null Referentie and SubmittedAt, and Decided.Afgewezen/MeerInfoGevraagd
require a Toelichting — so the five Referentie! null-forgiving derefs in
StatusAt are gone, not merely suppressed. IZaakSource.CreateZaak narrows to
Aanvraag.Submitted, removing the same class of deref in both zaak sources.
Draft is now cleared on submit rather than lingering: ApplicationStore's
doc-comment claimed "Concept only" but Submit never cleared it. Verified
nothing reads a submitted aanvraag's draft (draft-sync's applyResume only
resumes unsubmitted wizards), so the comment is now true instead of
aspirational.
No migration, no schema change, no wire change — RegistrationStatusDto and
the application DTOs are byte-identical, confirmed against a live swagger.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
244 lines
9.5 KiB
C#
244 lines
9.5 KiB
C#
using System.Net;
|
|
using System.Net.Http.Headers;
|
|
using System.Net.Http.Json;
|
|
using BigRegister.Api.Contracts;
|
|
using BigRegister.Api.Data;
|
|
using Microsoft.AspNetCore.Mvc.Testing;
|
|
|
|
namespace BigRegister.Tests;
|
|
|
|
public class EndpointTests(TestWebApplicationFactory factory) : IClassFixture<TestWebApplicationFactory>
|
|
{
|
|
private readonly HttpClient _client = factory.CreateClient();
|
|
|
|
[Fact]
|
|
public async Task DashboardView_computes_eligibility_decision()
|
|
{
|
|
var dto = await _client.GetFromJsonAsync<DashboardViewDto>("/api/v1/dashboard-view");
|
|
Assert.NotNull(dto);
|
|
Assert.Equal("19012345601", dto.Registration.BigNummer);
|
|
Assert.Equal("Geregistreerd", dto.Registration.Status.Tag);
|
|
// seed deadline 2027-03-01 is within 12 months of "today" (2026) → eligible
|
|
Assert.True(dto.Decisions.EligibleForHerregistratie);
|
|
Assert.NotNull(dto.Decisions.HerregistratieReason);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Notes_returns_seeded_aantekeningen()
|
|
{
|
|
var notes = await _client.GetFromJsonAsync<List<AantekeningDto>>("/api/v1/notes");
|
|
Assert.NotNull(notes);
|
|
Assert.Equal(3, notes.Count);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Brp_returns_address()
|
|
{
|
|
var dto = await _client.GetFromJsonAsync<BrpAddressDto>("/api/v1/brp/address");
|
|
Assert.NotNull(dto);
|
|
Assert.True(dto.Gevonden);
|
|
Assert.NotNull(dto.Adres);
|
|
Assert.Equal("2514 EA", dto.Adres.Postcode);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Duo_lookup_carries_server_decided_questions_and_professions()
|
|
{
|
|
var dto = await _client.GetFromJsonAsync<DuoLookupDto>("/api/v1/duo/diplomas");
|
|
Assert.NotNull(dto);
|
|
|
|
var english = dto.Diplomas.Single(d => d.Id == "d2");
|
|
Assert.Equal("Arts", english.Beroep);
|
|
Assert.Contains(english.PolicyQuestions, q => q.Id == "nl-taalvaardigheid");
|
|
|
|
var dutch = dto.Diplomas.Single(d => d.Id == "d1");
|
|
Assert.Empty(dutch.PolicyQuestions);
|
|
|
|
// DUO "not found" fallback: an unlisted diploma → user uses the manual path,
|
|
// which the same lookup provides (maximal question set + declarable professions).
|
|
Assert.DoesNotContain(dto.Diplomas, d => d.Id == "unknown-id");
|
|
Assert.Equal(3, dto.Handmatig.PolicyQuestions.Count);
|
|
Assert.Equal(5, dto.Handmatig.Beroepen.Count);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task IntakePolicy_returns_scholing_threshold()
|
|
{
|
|
var dto = await _client.GetFromJsonAsync<IntakePolicyDto>("/api/v1/intake/policy");
|
|
Assert.NotNull(dto);
|
|
Assert.Equal(1000, dto.ScholingThreshold);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Registration_with_duo_diploma_succeeds()
|
|
{
|
|
var res = await _client.PostAsJsonAsync("/api/v1/registrations", new RegistratieRequest("duo"));
|
|
res.EnsureSuccessStatusCode();
|
|
var body = await res.Content.ReadFromJsonAsync<ReferentieResponse>();
|
|
Assert.NotNull(body);
|
|
Assert.StartsWith("BIG-2026-", body.Referentie);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Registration_with_manual_diploma_is_rejected_with_problem_details()
|
|
{
|
|
var res = await _client.PostAsJsonAsync("/api/v1/registrations", new RegistratieRequest("handmatig"));
|
|
Assert.Equal(HttpStatusCode.UnprocessableEntity, res.StatusCode);
|
|
var contentType = res.Content.Headers.ContentType;
|
|
Assert.NotNull(contentType);
|
|
Assert.Contains("application/problem+json", contentType.ToString());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Change_request_with_valid_phone_succeeds()
|
|
{
|
|
var res = await _client.PostAsJsonAsync("/api/v1/change-requests",
|
|
new { telefoon = "0612345678" });
|
|
res.EnsureSuccessStatusCode();
|
|
var body = await res.Content.ReadFromJsonAsync<ReferentieResponse>();
|
|
Assert.NotNull(body);
|
|
Assert.StartsWith("BIG-2026-", body.Referentie);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Change_request_with_bad_phone_is_rejected()
|
|
{
|
|
var res = await _client.PostAsJsonAsync("/api/v1/change-requests",
|
|
new { telefoon = "nope" });
|
|
Assert.Equal(HttpStatusCode.UnprocessableEntity, res.StatusCode);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Health_endpoint_is_ok()
|
|
{
|
|
var res = await _client.GetAsync("/health");
|
|
res.EnsureSuccessStatusCode();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Correlation_id_supplied_by_the_caller_is_echoed_back()
|
|
{
|
|
var req = new HttpRequestMessage(HttpMethod.Get, "/api/v1/notes");
|
|
req.Headers.Add("X-Correlation-Id", "test-cid-123");
|
|
var res = await _client.SendAsync(req);
|
|
Assert.Equal("test-cid-123", res.Headers.GetValues("X-Correlation-Id").Single());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Correlation_id_is_generated_when_the_caller_omits_it()
|
|
{
|
|
var res = await _client.GetAsync("/api/v1/notes");
|
|
Assert.NotEmpty(res.Headers.GetValues("X-Correlation-Id").Single());
|
|
}
|
|
|
|
// --- Document upload ---
|
|
|
|
private static MultipartFormDataContent UploadForm(string localId, string categoryId, string wizardId, string fileName, string contentType)
|
|
{
|
|
var content = new MultipartFormDataContent();
|
|
var file = new ByteArrayContent(new byte[] { 1, 2, 3 });
|
|
file.Headers.ContentType = new MediaTypeHeaderValue(contentType);
|
|
content.Add(file, "file", fileName);
|
|
content.Add(new StringContent(categoryId), "categoryId");
|
|
content.Add(new StringContent(localId), "localId");
|
|
content.Add(new StringContent(wizardId), "wizardId");
|
|
return content;
|
|
}
|
|
|
|
private async Task<UploadResponse> Upload(string localId, string categoryId = "diploma", string type = "application/pdf", string file = "d.pdf")
|
|
{
|
|
var res = await _client.PostAsync("/api/v1/uploads", UploadForm(localId, categoryId, "registratie", file, type));
|
|
Assert.Equal(HttpStatusCode.Created, res.StatusCode);
|
|
var uploaded = await res.Content.ReadFromJsonAsync<UploadResponse>();
|
|
Assert.NotNull(uploaded);
|
|
return uploaded;
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Categories_are_server_owned_config()
|
|
{
|
|
// A manual diploma requires a diploma upload; identiteit is always required.
|
|
var dto = await _client.GetFromJsonAsync<UploadCategoriesDto>("/api/v1/uploads/categories?wizardId=registratie&diplomaHerkomst=handmatig");
|
|
Assert.NotNull(dto);
|
|
Assert.Contains(dto.Categories, c => c.CategoryId == "diploma" && c.Required && !c.AllowPostDelivery);
|
|
Assert.Contains(dto.Categories, c => c.CategoryId == "identiteit" && c.AllowPostDelivery);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Upload_then_status_reports_complete_for_known_localId()
|
|
{
|
|
var localId = Guid.NewGuid().ToString();
|
|
var doc = await Upload(localId);
|
|
var status = await _client.GetFromJsonAsync<UploadStatusDto>($"/api/v1/uploads/status?localIds={localId},onbekend");
|
|
Assert.NotNull(status);
|
|
Assert.Contains(status.Results, r => r.LocalId == localId && r.Status == "complete" && r.DocumentId == doc.DocumentId);
|
|
Assert.Contains(status.Results, r => r.LocalId == "onbekend" && r.Status == "unknown");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Upload_content_is_served_back_with_its_type_inline_for_pdf()
|
|
{
|
|
var doc = await Upload(Guid.NewGuid().ToString());
|
|
var res = await _client.GetAsync($"/api/v1/uploads/{doc.DocumentId}/content");
|
|
res.EnsureSuccessStatusCode();
|
|
var contentType = res.Content.Headers.ContentType;
|
|
Assert.NotNull(contentType);
|
|
Assert.Equal("application/pdf", contentType.MediaType);
|
|
Assert.Equal(new byte[] { 1, 2, 3 }, await res.Content.ReadAsByteArrayAsync());
|
|
// pdf/image → inline (no attachment disposition) so the browser previews it
|
|
Assert.NotEqual("attachment", res.Content.Headers.ContentDisposition?.DispositionType);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Upload_content_404_for_unknown_document()
|
|
{
|
|
Assert.Equal(HttpStatusCode.NotFound, (await _client.GetAsync("/api/v1/uploads/demo-nope/content")).StatusCode);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Upload_rejects_wrong_type()
|
|
{
|
|
var res = await _client.PostAsync("/api/v1/uploads",
|
|
UploadForm(Guid.NewGuid().ToString(), "diploma", "registratie", "d.txt", "text/plain"));
|
|
Assert.Equal(HttpStatusCode.BadRequest, res.StatusCode);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task User_delete_succeeds_then_404()
|
|
{
|
|
var doc = await Upload(Guid.NewGuid().ToString());
|
|
Assert.Equal(HttpStatusCode.NoContent, (await _client.DeleteAsync($"/api/v1/uploads/{doc.DocumentId}")).StatusCode);
|
|
Assert.Equal(HttpStatusCode.NotFound, (await _client.DeleteAsync($"/api/v1/uploads/{doc.DocumentId}")).StatusCode);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task User_delete_blocked_with_409_once_linked_to_submission()
|
|
{
|
|
var doc = await Upload(Guid.NewGuid().ToString());
|
|
var submit = await _client.PostAsJsonAsync("/api/v1/registrations",
|
|
new RegistratieRequest("duo", new[] { new DocumentRefDto("diploma", "digital", doc.DocumentId) }));
|
|
submit.EnsureSuccessStatusCode();
|
|
Assert.Equal(HttpStatusCode.Conflict, (await _client.DeleteAsync($"/api/v1/uploads/{doc.DocumentId}")).StatusCode);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Admin_delete_requires_admin_role()
|
|
{
|
|
var doc = await Upload(Guid.NewGuid().ToString());
|
|
Assert.Equal(HttpStatusCode.Forbidden, (await _client.DeleteAsync($"/api/v1/admin/uploads/{doc.DocumentId}")).StatusCode);
|
|
|
|
var req = new HttpRequestMessage(HttpMethod.Delete, $"/api/v1/admin/uploads/{doc.DocumentId}");
|
|
req.Headers.Add("X-Admin", "true");
|
|
Assert.Equal(HttpStatusCode.NoContent, (await _client.SendAsync(req)).StatusCode);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Audit_log_records_upload_and_delete_metadata_only()
|
|
{
|
|
var doc = await Upload(Guid.NewGuid().ToString());
|
|
await _client.DeleteAsync($"/api/v1/uploads/{doc.DocumentId}");
|
|
Assert.Contains(DocumentStore.AuditLog, a => a.DocumentId == doc.DocumentId && a.Action == "upload");
|
|
Assert.Contains(DocumentStore.AuditLog, a => a.DocumentId == doc.DocumentId && a.Action == "delete-user");
|
|
}
|
|
}
|