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 { private readonly HttpClient _client = factory.CreateClient(); [Fact] public async Task DashboardView_computes_eligibility_decision() { var dto = await _client.GetFromJsonAsync("/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>("/api/v1/notes"); Assert.NotNull(notes); Assert.Equal(3, notes.Count); } [Fact] public async Task Brp_returns_address() { var dto = await _client.GetFromJsonAsync("/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("/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("/api/v1/intake/policy"); Assert.NotNull(dto); Assert.Equal(1000, dto.ScholingThreshold); } [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(); Assert.NotNull(body); Assert.StartsWith("BIG-2026-", body.Referentie); } [Fact] public async Task Change_request_with_bad_phone_is_rejected_with_problem_details() { var res = await _client.PostAsJsonAsync("/api/v1/change-requests", new { telefoon = "nope" }); Assert.Equal(HttpStatusCode.UnprocessableEntity, res.StatusCode); // The Submit helper's rejection shape — was asserted through POST /registrations until // RB-06 deleted it; /change-requests is the other endpoint on the same helper. var contentType = res.Content.Headers.ContentType; Assert.NotNull(contentType); Assert.Contains("application/problem+json", contentType.ToString()); } [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 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(); 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("/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($"/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()); // Through the real submit path (RB-06 deleted POST /registrations, which was the only // other caller of DocumentStore.Link and had no ownership guard on it). var created = await _client.PostAsJsonAsync("/api/v1/applications", new { type = "registratie" }); var aanvraag = (await created.Content.ReadFromJsonAsync())!; var submit = await _client.PostAsJsonAsync($"/api/v1/applications/{aanvraag.Id}/submit", new { diplomaHerkomst = "duo", documents = 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() { // RB-08: routed through CasesAdmin (cases:manage), like the other admin-cases // endpoints, not the standalone X-Admin header this used to accept. 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-Role", "admin"); 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"); } }