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>
100 lines
3.8 KiB
C#
100 lines
3.8 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;
|
|
|
|
/// Who may see what about an upload. BIO-004: GET /uploads/{id}/content and
|
|
/// /uploads/status used to take no HttpContext at all — a diploma or identity scan was
|
|
/// protected by GUID unguessability alone, while DELETE on the same resource was
|
|
/// owner-scoped. BIO-005: the document audit trail recorded the raw owner BSN as
|
|
/// its Actor, on a store whose own doc comment says it holds no PII.
|
|
public class UploadAccessTests(TestWebApplicationFactory factory) : IClassFixture<TestWebApplicationFactory>
|
|
{
|
|
private readonly HttpClient _client = factory.CreateClient();
|
|
|
|
private const string OtherCitizen = "999999990";
|
|
|
|
private async Task<string> UploadAsOwner()
|
|
{
|
|
var form = new MultipartFormDataContent();
|
|
var file = new ByteArrayContent(new byte[] { 1, 2, 3 });
|
|
file.Headers.ContentType = new MediaTypeHeaderValue("application/pdf");
|
|
form.Add(file, "file", "diploma.pdf");
|
|
form.Add(new StringContent("diploma"), "categoryId");
|
|
form.Add(new StringContent("local-rb01"), "localId");
|
|
form.Add(new StringContent("registratie"), "wizardId");
|
|
var res = await _client.PostAsync("/api/v1/uploads", form);
|
|
Assert.Equal(HttpStatusCode.Created, res.StatusCode);
|
|
return (await res.Content.ReadFromJsonAsync<UploadResponse>())!.DocumentId;
|
|
}
|
|
|
|
private Task<HttpResponseMessage> Get(string path, params (string Name, string Value)[] headers)
|
|
{
|
|
var req = new HttpRequestMessage(HttpMethod.Get, path);
|
|
foreach (var (name, value) in headers) req.Headers.Add(name, value);
|
|
return _client.SendAsync(req);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task The_owner_can_read_the_bytes()
|
|
{
|
|
var id = await UploadAsOwner();
|
|
Assert.Equal(HttpStatusCode.OK, (await Get($"/api/v1/uploads/{id}/content")).StatusCode);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Another_citizen_gets_404_not_403()
|
|
{
|
|
var id = await UploadAsOwner();
|
|
// 404, not 403: a foreign id must not be distinguishable from one that never existed.
|
|
Assert.Equal(HttpStatusCode.NotFound,
|
|
(await Get($"/api/v1/uploads/{id}/content", ("X-Subject", OtherCitizen))).StatusCode);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task A_behandelaar_can_read_a_linked_document()
|
|
{
|
|
var id = await UploadAsOwner();
|
|
Assert.Equal(HttpStatusCode.OK,
|
|
(await Get($"/api/v1/uploads/{id}/content", ("X-Medewerker", "medewerker-1"))).StatusCode);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task A_medewerker_without_the_behandelaar_rol_does_not()
|
|
{
|
|
var id = await UploadAsOwner();
|
|
Assert.Equal(HttpStatusCode.NotFound,
|
|
(await Get($"/api/v1/uploads/{id}/content",
|
|
("X-Medewerker", "medewerker-1"), ("X-Rollen", "geen"))).StatusCode);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task The_document_audit_trail_records_a_masked_actor()
|
|
{
|
|
var id = await UploadAsOwner();
|
|
(await _client.DeleteAsync($"/api/v1/uploads/{id}")).EnsureSuccessStatusCode();
|
|
|
|
var rows = DocumentStore.AuditLog.Where(e => e.DocumentId == id).ToList();
|
|
Assert.Equal(new[] { "upload", "delete-user" }, rows.Select(e => e.Action));
|
|
Assert.All(rows, e => Assert.Equal("******782", e.Actor));
|
|
// The unmasked BSN stays where it is load-bearing — the ownership key, not the trail.
|
|
Assert.All(rows, e => Assert.DoesNotContain(DocumentStore.DemoOwner, e.Actor));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Status_reports_another_citizens_localId_as_unknown()
|
|
{
|
|
await UploadAsOwner();
|
|
var res = await Get("/api/v1/uploads/status?localIds=local-rb01", ("X-Subject", OtherCitizen));
|
|
res.EnsureSuccessStatusCode();
|
|
var status = (await res.Content.ReadFromJsonAsync<UploadStatusDto>())!;
|
|
var item = Assert.Single(status.Results);
|
|
Assert.Equal("unknown", item.Status);
|
|
Assert.Null(item.DocumentId);
|
|
}
|
|
}
|