From 08515b9bbad43b83c5201af0f1498bfd05c8edcb Mon Sep 17 00:00:00 2001 From: Niek Otten Date: Wed, 15 Jul 2026 11:03:36 +0200 Subject: [PATCH] test(bff): /behandel/werkbak needs a medewerker token with the behandelaar role (refs #13) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Red — the medewerker JWT scheme, the behandelaar policy, and the werkbak endpoint do not exist yet (endpoint 404s). Co-Authored-By: Claude Opus 4.8 (1M context) --- services/bff/Bff.Api/DownstreamClients.cs | 10 ++++ .../bff/Bff.Tests/BehandelEndpointTests.cs | 57 +++++++++++++++++++ services/bff/Bff.Tests/BffFactory.cs | 4 ++ services/bff/Bff.Tests/TestTokens.cs | 16 ++++++ 4 files changed, 87 insertions(+) create mode 100644 services/bff/Bff.Tests/BehandelEndpointTests.cs diff --git a/services/bff/Bff.Api/DownstreamClients.cs b/services/bff/Bff.Api/DownstreamClients.cs index c03f166..8571641 100644 --- a/services/bff/Bff.Api/DownstreamClients.cs +++ b/services/bff/Bff.Api/DownstreamClients.cs @@ -13,10 +13,17 @@ public sealed record ProjectionEntry(string Id, string Status, string? Reference /// A public-safe openbaar register row — only non-sensitive fields leave the BFF. public sealed record OpenbaarEntry(string Id, string Status, string? Reference); +/// A behandelaar's werkbak row: a registration awaiting beoordeling, with the bsn + status a +/// behandelaar sees (staff view — reached only behind medewerker/behandelaar authorization, S-12c). +public sealed record WerkbakItem(string RegistrationId, string Bsn, string Status); + /// Port to the Domain Service (§8.3: the BFF is the portals' only backend; it fans out). public interface IDomainClient { Task SubmitRegistrationAsync(string bsn, CancellationToken ct = default); + + /// The behandelaar's werkbak — registrations awaiting beoordeling. + Task> GetWerkbakAsync(CancellationToken ct = default); } /// Port to the read projection. @@ -37,6 +44,9 @@ public sealed class DomainClient(HttpClient http) : IDomainClient return new SubmitAccepted(dto.RegistrationId, dto.Status); } + public async Task> GetWerkbakAsync(CancellationToken ct = default) + => await http.GetFromJsonAsync>("behandel/werkbak", ct) ?? []; + private sealed record DomainResponse(string RegistrationId, string Status, string? ZaakUrl); } diff --git a/services/bff/Bff.Tests/BehandelEndpointTests.cs b/services/bff/Bff.Tests/BehandelEndpointTests.cs new file mode 100644 index 0000000..3d39147 --- /dev/null +++ b/services/bff/Bff.Tests/BehandelEndpointTests.cs @@ -0,0 +1,57 @@ +using System.Net; +using System.Net.Http.Headers; +using System.Net.Http.Json; +using Bff.Api; + +namespace Bff.Tests; + +/// +/// The behandel werkbak endpoint (S-12c): reached only with a medewerker-realm token that carries the +/// behandelaar role. A missing token is 401; an authenticated medewerker without the role is +/// 403; a behandelaar gets the werkbak (staff view, incl. bsn). +/// +public class BehandelEndpointTests +{ + private static HttpRequestMessage Werkbak(string? bearer) + { + var request = new HttpRequestMessage(HttpMethod.Get, "/behandel/werkbak"); + if (bearer is not null) + request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", bearer); + return request; + } + + [Fact] + public async Task Rejects_the_werkbak_without_a_token() + { + using var factory = new BffFactory(); + + var response = await factory.CreateClient().SendAsync(Werkbak(bearer: null)); + + Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode); + } + + [Fact] + public async Task Rejects_a_medewerker_without_the_behandelaar_role() + { + using var factory = new BffFactory(); + + var response = await factory.CreateClient().SendAsync(Werkbak(TestTokens.Medewerker("teamlead"))); + + Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode); + } + + [Fact] + public async Task Serves_the_werkbak_to_a_behandelaar() + { + using var factory = new BffFactory(); + factory.Domain.Werkbak.Add(new WerkbakItem("reg-1", "123456782", "InBehandeling")); + + var response = await factory.CreateClient().SendAsync(Werkbak(TestTokens.Medewerker("behandelaar"))); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var items = await response.Content.ReadFromJsonAsync>(); + var item = Assert.Single(items!); + Assert.Equal("reg-1", item.RegistrationId); + Assert.Equal("123456782", item.Bsn); + } +} diff --git a/services/bff/Bff.Tests/BffFactory.cs b/services/bff/Bff.Tests/BffFactory.cs index 510357b..e6a487a 100644 --- a/services/bff/Bff.Tests/BffFactory.cs +++ b/services/bff/Bff.Tests/BffFactory.cs @@ -60,12 +60,16 @@ internal sealed class FakeDomainClient : IDomainClient { public string? SubmittedBsn { get; private set; } public SubmitAccepted Result { get; set; } = new("reg-123", "Ingediend"); + public List Werkbak { get; } = []; public Task SubmitRegistrationAsync(string bsn, CancellationToken ct = default) { SubmittedBsn = bsn; return Task.FromResult(Result); } + + public Task> GetWerkbakAsync(CancellationToken ct = default) + => Task.FromResult>(Werkbak); } /// Serves a configurable set of projection rows. diff --git a/services/bff/Bff.Tests/TestTokens.cs b/services/bff/Bff.Tests/TestTokens.cs index b5788a5..12465f5 100644 --- a/services/bff/Bff.Tests/TestTokens.cs +++ b/services/bff/Bff.Tests/TestTokens.cs @@ -17,6 +17,22 @@ internal static class TestTokens new SymmetricSecurityKey(Encoding.UTF8.GetBytes("a-different-signing-key-256-bits-long-indeed-yes!")), expired: false); + /// A medewerker-realm token carrying the given realm roles under realm_access.roles + /// (Keycloak's shape), signed with the valid test key. Used to exercise behandel authorization. + public static string Medewerker(params string[] roles) + { + var handler = new JsonWebTokenHandler(); + return handler.CreateToken(new SecurityTokenDescriptor + { + Claims = new Dictionary + { + ["realm_access"] = new Dictionary { ["roles"] = roles }, + }, + Expires = DateTime.UtcNow.AddMinutes(30), + SigningCredentials = new SigningCredentials(BffFactory.TestSigningKey, SecurityAlgorithms.HmacSha256), + }); + } + private static string Create(string bsn, SymmetricSecurityKey key, bool expired) { var handler = new JsonWebTokenHandler();