diff --git a/services/bff/Bff.Api/DownstreamClients.cs b/services/bff/Bff.Api/DownstreamClients.cs index f9761d5..8696b66 100644 --- a/services/bff/Bff.Api/DownstreamClients.cs +++ b/services/bff/Bff.Api/DownstreamClients.cs @@ -59,12 +59,26 @@ public interface IProjectionClient /// internal reference, not shown in the portal. public sealed record BeheerZaaktype(string Identificatie, string Omschrijving); -/// Port to the ACL for read-only catalogus queries (beheer portal, S-15a). The BFF reaches the -/// ACL directly for this read: the catalogus isn't a domain concern, and the ACL is the only code -/// allowed to read the ZGW Catalogi API (§8.1, ADR-0025). +/// The ACL default-fill settings the beheer portal reads + edits (S-15b): the three ZGW-mandatory +/// fields the ACL stamps on every zaak (ADR-0003). +public sealed record BeheerDefaultFill( + string Bronorganisatie, + string VerantwoordelijkeOrganisatie, + string Vertrouwelijkheidaanduiding); + +/// Port to the ACL for beheer queries (beheer portal). The BFF reaches the ACL directly: these +/// aren't a domain concern, and the ACL is the only code allowed to read/own the ZGW-facing config +/// (§8.1, ADR-0025). public interface IAclClient { + /// The published catalogus zaaktypen, read-only (S-15a). Task> GetZaaktypenAsync(CancellationToken ct = default); + + /// The current default-fill settings (S-15b). + Task GetDefaultFillAsync(CancellationToken ct = default); + + /// Replace the default-fill settings (S-15b). + Task UpdateDefaultFillAsync(BeheerDefaultFill settings, CancellationToken ct = default); } /// Calls the Domain Service's POST /registrations. @@ -141,4 +155,14 @@ public sealed class AclClient(HttpClient http) : IAclClient { public async Task> GetZaaktypenAsync(CancellationToken ct = default) => await http.GetFromJsonAsync>("catalogi/zaaktypen", ct) ?? []; + + public async Task GetDefaultFillAsync(CancellationToken ct = default) + => await http.GetFromJsonAsync("default-fill", ct) + ?? throw new InvalidOperationException("The ACL returned an empty default-fill response."); + + public async Task UpdateDefaultFillAsync(BeheerDefaultFill settings, CancellationToken ct = default) + { + using var response = await http.PutAsJsonAsync("default-fill", settings, ct); + response.EnsureSuccessStatusCode(); + } } diff --git a/services/bff/Bff.Tests/BeheerDefaultFillEndpointTests.cs b/services/bff/Bff.Tests/BeheerDefaultFillEndpointTests.cs new file mode 100644 index 0000000..472c664 --- /dev/null +++ b/services/bff/Bff.Tests/BeheerDefaultFillEndpointTests.cs @@ -0,0 +1,84 @@ +using System.Net; +using System.Net.Http.Headers; +using System.Net.Http.Json; +using Bff.Api; + +namespace Bff.Tests; + +/// +/// The beheer default-fill config endpoints (S-15b): read (GET) and edit (PUT) the ACL's default-fill, +/// reached only with a medewerker-realm token carrying the beheerder role. Missing token → 401; +/// a medewerker without the role → 403; a beheerder reads and updates via the ACL client. +/// +public class BeheerDefaultFillEndpointTests +{ + private static HttpRequestMessage Get(string? bearer) + { + var r = new HttpRequestMessage(HttpMethod.Get, "/beheer/default-fill"); + if (bearer is not null) r.Headers.Authorization = new AuthenticationHeaderValue("Bearer", bearer); + return r; + } + + private static HttpRequestMessage Put(string? bearer, object body) + { + var r = new HttpRequestMessage(HttpMethod.Put, "/beheer/default-fill") { Content = JsonContent.Create(body) }; + if (bearer is not null) r.Headers.Authorization = new AuthenticationHeaderValue("Bearer", bearer); + return r; + } + + [Fact] + public async Task Rejects_read_without_a_token() + { + using var factory = new BffFactory(); + var response = await factory.CreateClient().SendAsync(Get(bearer: null)); + Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode); + } + + [Fact] + public async Task Rejects_a_medewerker_without_the_beheerder_role() + { + using var factory = new BffFactory(); + var response = await factory.CreateClient().SendAsync(Get(TestTokens.Medewerker("behandelaar"))); + Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode); + } + + [Fact] + public async Task Serves_the_current_default_fill_to_a_beheerder() + { + using var factory = new BffFactory(); + factory.Acl.DefaultFill = new BeheerDefaultFill("517439943", "517439943", "openbaar"); + + var response = await factory.CreateClient().SendAsync(Get(TestTokens.Medewerker("beheerder"))); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var body = await response.Content.ReadFromJsonAsync(); + Assert.Equal("517439943", body!.Bronorganisatie); + Assert.Equal("openbaar", body.Vertrouwelijkheidaanduiding); + } + + [Fact] + public async Task Updates_the_default_fill_via_the_acl_for_a_beheerder() + { + using var factory = new BffFactory(); + + var response = await factory.CreateClient().SendAsync( + Put(TestTokens.Medewerker("beheerder"), + new { bronorganisatie = "999999999", verantwoordelijkeOrganisatie = "888888888", vertrouwelijkheidaanduiding = "vertrouwelijk" })); + + Assert.Equal(HttpStatusCode.NoContent, response.StatusCode); + Assert.Equal("999999999", factory.Acl.Updated!.Bronorganisatie); + Assert.Equal("vertrouwelijk", factory.Acl.Updated.Vertrouwelijkheidaanduiding); + } + + [Fact] + public async Task Rejects_an_update_from_a_non_beheerder() + { + using var factory = new BffFactory(); + + var response = await factory.CreateClient().SendAsync( + Put(TestTokens.Medewerker("behandelaar"), new { bronorganisatie = "1", verantwoordelijkeOrganisatie = "2", vertrouwelijkheidaanduiding = "openbaar" })); + + Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode); + Assert.Null(factory.Acl.Updated); + } +} diff --git a/services/bff/Bff.Tests/BffFactory.cs b/services/bff/Bff.Tests/BffFactory.cs index 7f1141e..278a61c 100644 --- a/services/bff/Bff.Tests/BffFactory.cs +++ b/services/bff/Bff.Tests/BffFactory.cs @@ -142,11 +142,24 @@ internal sealed class FakeProjectionClient : IProjectionClient => Task.FromResult>(Entries); } -/// Serves a configurable set of catalogus zaaktypen (beheer viewer, S-15a). +/// Serves catalogus zaaktypen (S-15a) and holds the default-fill settings (S-15b). internal sealed class FakeAclClient : IAclClient { public List Zaaktypen { get; } = []; public Task> GetZaaktypenAsync(CancellationToken ct = default) => Task.FromResult>(Zaaktypen); + + public BeheerDefaultFill DefaultFill { get; set; } = new("517439943", "517439943", "openbaar"); + public BeheerDefaultFill? Updated { get; private set; } + + public Task GetDefaultFillAsync(CancellationToken ct = default) + => Task.FromResult(DefaultFill); + + public Task UpdateDefaultFillAsync(BeheerDefaultFill settings, CancellationToken ct = default) + { + Updated = settings; + DefaultFill = settings; + return Task.CompletedTask; + } }