test(bff): /behandel/werkbak needs a medewerker token with the behandelaar role (refs #13)
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) <noreply@anthropic.com>
This commit is contained in:
@@ -13,10 +13,17 @@ public sealed record ProjectionEntry(string Id, string Status, string? Reference
|
|||||||
/// <summary>A public-safe openbaar register row — only non-sensitive fields leave the BFF.</summary>
|
/// <summary>A public-safe openbaar register row — only non-sensitive fields leave the BFF.</summary>
|
||||||
public sealed record OpenbaarEntry(string Id, string Status, string? Reference);
|
public sealed record OpenbaarEntry(string Id, string Status, string? Reference);
|
||||||
|
|
||||||
|
/// <summary>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).</summary>
|
||||||
|
public sealed record WerkbakItem(string RegistrationId, string Bsn, string Status);
|
||||||
|
|
||||||
/// <summary>Port to the Domain Service (§8.3: the BFF is the portals' only backend; it fans out).</summary>
|
/// <summary>Port to the Domain Service (§8.3: the BFF is the portals' only backend; it fans out).</summary>
|
||||||
public interface IDomainClient
|
public interface IDomainClient
|
||||||
{
|
{
|
||||||
Task<SubmitAccepted> SubmitRegistrationAsync(string bsn, CancellationToken ct = default);
|
Task<SubmitAccepted> SubmitRegistrationAsync(string bsn, CancellationToken ct = default);
|
||||||
|
|
||||||
|
/// <summary>The behandelaar's werkbak — registrations awaiting beoordeling.</summary>
|
||||||
|
Task<IReadOnlyList<WerkbakItem>> GetWerkbakAsync(CancellationToken ct = default);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Port to the read projection.</summary>
|
/// <summary>Port to the read projection.</summary>
|
||||||
@@ -37,6 +44,9 @@ public sealed class DomainClient(HttpClient http) : IDomainClient
|
|||||||
return new SubmitAccepted(dto.RegistrationId, dto.Status);
|
return new SubmitAccepted(dto.RegistrationId, dto.Status);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<IReadOnlyList<WerkbakItem>> GetWerkbakAsync(CancellationToken ct = default)
|
||||||
|
=> await http.GetFromJsonAsync<List<WerkbakItem>>("behandel/werkbak", ct) ?? [];
|
||||||
|
|
||||||
private sealed record DomainResponse(string RegistrationId, string Status, string? ZaakUrl);
|
private sealed record DomainResponse(string RegistrationId, string Status, string? ZaakUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
57
services/bff/Bff.Tests/BehandelEndpointTests.cs
Normal file
57
services/bff/Bff.Tests/BehandelEndpointTests.cs
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
using System.Net;
|
||||||
|
using System.Net.Http.Headers;
|
||||||
|
using System.Net.Http.Json;
|
||||||
|
using Bff.Api;
|
||||||
|
|
||||||
|
namespace Bff.Tests;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The behandel werkbak endpoint (S-12c): reached only with a medewerker-realm token that carries the
|
||||||
|
/// <c>behandelaar</c> role. A missing token is 401; an authenticated medewerker without the role is
|
||||||
|
/// 403; a behandelaar gets the werkbak (staff view, incl. bsn).
|
||||||
|
/// </summary>
|
||||||
|
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<List<WerkbakItem>>();
|
||||||
|
var item = Assert.Single(items!);
|
||||||
|
Assert.Equal("reg-1", item.RegistrationId);
|
||||||
|
Assert.Equal("123456782", item.Bsn);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -60,12 +60,16 @@ internal sealed class FakeDomainClient : IDomainClient
|
|||||||
{
|
{
|
||||||
public string? SubmittedBsn { get; private set; }
|
public string? SubmittedBsn { get; private set; }
|
||||||
public SubmitAccepted Result { get; set; } = new("reg-123", "Ingediend");
|
public SubmitAccepted Result { get; set; } = new("reg-123", "Ingediend");
|
||||||
|
public List<WerkbakItem> Werkbak { get; } = [];
|
||||||
|
|
||||||
public Task<SubmitAccepted> SubmitRegistrationAsync(string bsn, CancellationToken ct = default)
|
public Task<SubmitAccepted> SubmitRegistrationAsync(string bsn, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
SubmittedBsn = bsn;
|
SubmittedBsn = bsn;
|
||||||
return Task.FromResult(Result);
|
return Task.FromResult(Result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Task<IReadOnlyList<WerkbakItem>> GetWerkbakAsync(CancellationToken ct = default)
|
||||||
|
=> Task.FromResult<IReadOnlyList<WerkbakItem>>(Werkbak);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Serves a configurable set of projection rows.</summary>
|
/// <summary>Serves a configurable set of projection rows.</summary>
|
||||||
|
|||||||
@@ -17,6 +17,22 @@ internal static class TestTokens
|
|||||||
new SymmetricSecurityKey(Encoding.UTF8.GetBytes("a-different-signing-key-256-bits-long-indeed-yes!")),
|
new SymmetricSecurityKey(Encoding.UTF8.GetBytes("a-different-signing-key-256-bits-long-indeed-yes!")),
|
||||||
expired: false);
|
expired: false);
|
||||||
|
|
||||||
|
/// <summary>A medewerker-realm token carrying the given realm roles under <c>realm_access.roles</c>
|
||||||
|
/// (Keycloak's shape), signed with the valid test key. Used to exercise behandel authorization.</summary>
|
||||||
|
public static string Medewerker(params string[] roles)
|
||||||
|
{
|
||||||
|
var handler = new JsonWebTokenHandler();
|
||||||
|
return handler.CreateToken(new SecurityTokenDescriptor
|
||||||
|
{
|
||||||
|
Claims = new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
["realm_access"] = new Dictionary<string, object> { ["roles"] = roles },
|
||||||
|
},
|
||||||
|
Expires = DateTime.UtcNow.AddMinutes(30),
|
||||||
|
SigningCredentials = new SigningCredentials(BffFactory.TestSigningKey, SecurityAlgorithms.HmacSha256),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private static string Create(string bsn, SymmetricSecurityKey key, bool expired)
|
private static string Create(string bsn, SymmetricSecurityKey key, bool expired)
|
||||||
{
|
{
|
||||||
var handler = new JsonWebTokenHandler();
|
var handler = new JsonWebTokenHandler();
|
||||||
|
|||||||
Reference in New Issue
Block a user