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();