From 771450d46df5288da3252fbce96968049ee4cbe6 Mon Sep 17 00:00:00 2001 From: Niek Otten Date: Mon, 20 Jul 2026 10:39:31 +0200 Subject: [PATCH] test(bff): self-service documents endpoint forwards id + bsn to the domain (refs #102) RED: POST /self-service/registrations/{id}/documents requires a digid token, takes the bsn from the token, forwards to the domain, and relays the domain's 404 for an unknown/not-owned registration. Adds the IDomainClient.ProvideDocumentsAsync port + client + fake; the endpoint itself follows. Co-Authored-By: Claude Opus 4.8 (1M context) --- services/bff/Bff.Api/DownstreamClients.cs | 16 ++++++++ services/bff/Bff.Tests/BffFactory.cs | 12 ++++++ .../bff/Bff.Tests/SelfServiceEndpointTests.cs | 41 +++++++++++++++++++ 3 files changed, 69 insertions(+) diff --git a/services/bff/Bff.Api/DownstreamClients.cs b/services/bff/Bff.Api/DownstreamClients.cs index 30b2429..f935247 100644 --- a/services/bff/Bff.Api/DownstreamClients.cs +++ b/services/bff/Bff.Api/DownstreamClients.cs @@ -27,6 +27,11 @@ public interface IDomainClient /// unknown or not the caller's (404), so the BFF can relay a 404 rather than a 500. Task WithdrawRegistrationAsync(string registrationId, string bsn, CancellationToken ct = default); + /// Provide the documents the caller's own registration is waiting for ("documenten + /// aanleveren"). Owner-scoped by . Returns false when the domain + /// reports the registration is unknown or not the caller's (404), so the BFF can relay a 404. + Task ProvideDocumentsAsync(string registrationId, string bsn, CancellationToken ct = default); + /// The behandelaar's werkbak — registrations awaiting beoordeling. Task> GetWerkbakAsync(CancellationToken ct = default); @@ -63,6 +68,17 @@ public sealed class DomainClient(HttpClient http) : IDomainClient return true; } + public async Task ProvideDocumentsAsync(string registrationId, string bsn, CancellationToken ct = default) + { + using var response = await http.PostAsJsonAsync( + $"registrations/{registrationId}/documents", new { bsn }, ct); + // The domain 404s an unknown or not-owned registration; relay that rather than fail hard. + if (response.StatusCode == System.Net.HttpStatusCode.NotFound) + return false; + response.EnsureSuccessStatusCode(); + return true; + } + public async Task> GetWerkbakAsync(CancellationToken ct = default) => await http.GetFromJsonAsync>("behandel/werkbak", ct) ?? []; diff --git a/services/bff/Bff.Tests/BffFactory.cs b/services/bff/Bff.Tests/BffFactory.cs index f108da8..492ab05 100644 --- a/services/bff/Bff.Tests/BffFactory.cs +++ b/services/bff/Bff.Tests/BffFactory.cs @@ -94,6 +94,18 @@ internal sealed class FakeDomainClient : IDomainClient return Task.FromResult(WithdrawSucceeds); } + public (string RegistrationId, string Bsn)? DocumentsProvidedFor { get; private set; } + + /// Whether the fake domain reports the provide-documents as done (true → 204) or + /// not-found/not-owned (false → 404). Tests set this to exercise the relay. + public bool ProvideDocumentsSucceeds { get; set; } = true; + + public Task ProvideDocumentsAsync(string registrationId, string bsn, CancellationToken ct = default) + { + DocumentsProvidedFor = (registrationId, bsn); + return Task.FromResult(ProvideDocumentsSucceeds); + } + public (string RegistrationId, string Besluit)? Decided { get; private set; } public Task> GetWerkbakAsync(CancellationToken ct = default) diff --git a/services/bff/Bff.Tests/SelfServiceEndpointTests.cs b/services/bff/Bff.Tests/SelfServiceEndpointTests.cs index f7adaa9..c8e0ccd 100644 --- a/services/bff/Bff.Tests/SelfServiceEndpointTests.cs +++ b/services/bff/Bff.Tests/SelfServiceEndpointTests.cs @@ -112,5 +112,46 @@ public class SelfServiceEndpointTests Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); } + private static HttpRequestMessage ProvideDocuments(string? bearer, string id = "reg-123") + { + var request = new HttpRequestMessage(HttpMethod.Post, $"/self-service/registrations/{id}/documents"); + if (bearer is not null) + request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", bearer); + return request; + } + + [Fact] + public async Task Rejects_providing_documents_without_a_token() + { + using var factory = new BffFactory(); + + var response = await factory.CreateClient().SendAsync(ProvideDocuments(bearer: null)); + + Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode); + Assert.Null(factory.Domain.DocumentsProvidedFor); + } + + [Fact] + public async Task Provides_documents_for_the_callers_registration_forwarding_the_id_and_bsn() + { + using var factory = new BffFactory(); + + var response = await factory.CreateClient().SendAsync(ProvideDocuments(TestTokens.Valid("123456782"), "reg-9")); + + Assert.Equal(HttpStatusCode.NoContent, response.StatusCode); + Assert.Equal(("reg-9", "123456782"), factory.Domain.DocumentsProvidedFor); + } + + [Fact] + public async Task Relays_not_found_providing_documents_for_an_unknown_or_not_owned_registration() + { + using var factory = new BffFactory(); + factory.Domain.ProvideDocumentsSucceeds = false; + + var response = await factory.CreateClient().SendAsync(ProvideDocuments(TestTokens.Valid("123456782"))); + + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + } + private sealed record SubmitAcceptedDto(string RegistrationId, string Status); }