From 0ba410ad78b5b93db3b153ae399a93625a660320 Mon Sep 17 00:00:00 2001 From: Niek Otten Date: Wed, 22 Jul 2026 15:39:01 +0200 Subject: [PATCH 1/5] feat(acl): resolve zaaktype + informatieobjecttype by business key, not a pinned URL (refs #113) The ACL now discovers its BIG zaaktype (by identificatie) and diploma informatieobjecttype (by omschrijving) from OpenZaak's Catalogi API, instead of being handed server-assigned URLs in config. A CachedZaaktypeCatalog resolves lazily on first use and caches (success only, so a pre-publish miss is retried); AclDefaults now carries ZaaktypeIdentificatie/InformatieobjecttypeOmschrijving. Clear errors replace the opaque placeholder-URL 400. Unit tests cover the resolver (resolve/cache/retry-on-failure) and the gateway lookups (match/miss). Co-Authored-By: Claude Opus 4.8 (1M context) --- services/acl/Acl.Api/Program.cs | 2 + services/acl/Acl.Application/AclDefaults.cs | 11 ++- services/acl/Acl.Application/AclService.cs | 30 +++--- .../Acl.Application/CachedZaaktypeCatalog.cs | 46 ++++++++++ services/acl/Acl.Application/IZaakGateway.cs | 8 ++ .../acl/Acl.Application/IZaaktypeCatalog.cs | 12 +++ .../acl/Acl.Infrastructure/OpenZaakGateway.cs | 56 +++++++++++ services/acl/Acl.Tests/AclServiceTests.cs | 82 +++++++++-------- .../acl/Acl.Tests/OpenZaakGatewayTests.cs | 88 ++++++++++++++++++ .../acl/Acl.Tests/ZaaktypeCatalogTests.cs | 92 +++++++++++++++++++ 10 files changed, 370 insertions(+), 57 deletions(-) create mode 100644 services/acl/Acl.Application/CachedZaaktypeCatalog.cs create mode 100644 services/acl/Acl.Application/IZaaktypeCatalog.cs create mode 100644 services/acl/Acl.Tests/ZaaktypeCatalogTests.cs diff --git a/services/acl/Acl.Api/Program.cs b/services/acl/Acl.Api/Program.cs index 1b1bf7f..22cceee 100644 --- a/services/acl/Acl.Api/Program.cs +++ b/services/acl/Acl.Api/Program.cs @@ -11,6 +11,8 @@ builder.Services.AddSingleton(sp => sp.GetRequiredService() .GetSection("Acl:OpenZaak").Get() ?? throw new InvalidOperationException("Missing configuration section 'Acl:OpenZaak'")); builder.Services.AddHttpClient(); +// Singleton so the resolved zaaktype/informatieobjecttype URLs are cached across requests (S-27). +builder.Services.AddSingleton(); builder.Services.AddScoped(); var app = builder.Build(); diff --git a/services/acl/Acl.Application/AclDefaults.cs b/services/acl/Acl.Application/AclDefaults.cs index d378c8c..7874d03 100644 --- a/services/acl/Acl.Application/AclDefaults.cs +++ b/services/acl/Acl.Application/AclDefaults.cs @@ -6,9 +6,12 @@ public sealed class AclDefaults public required string Bronorganisatie { get; init; } public required string VerantwoordelijkeOrganisatie { get; init; } public required string Vertrouwelijkheidaanduiding { get; init; } - public required Uri ZaaktypeUrl { get; init; } - /// The informatieobjecttype an uploaded diploma is filed under (S-10b). Seeded in the - /// catalogus and injected like . - public required Uri InformatieobjecttypeUrl { get; init; } + /// The BIG zaaktype's stable business key. The ACL resolves the (server-assigned) zaaktype + /// URL from this via the Catalogi API instead of being handed a pinned URL (S-27, ADR-0021). + public required string ZaaktypeIdentificatie { get; init; } + + /// The omschrijving of the informatieobjecttype an uploaded diploma is filed under (S-10b); + /// resolved to a URL by the Catalogi API, like . + public required string InformatieobjecttypeOmschrijving { get; init; } } diff --git a/services/acl/Acl.Application/AclService.cs b/services/acl/Acl.Application/AclService.cs index 6f00fef..4b23964 100644 --- a/services/acl/Acl.Application/AclService.cs +++ b/services/acl/Acl.Application/AclService.cs @@ -2,9 +2,9 @@ namespace Acl.Application; /// The ACL's single operation: open a zaak from a domain payload, /// default-filling the ZGW-mandatory fields (ADR-0003). -public sealed class AclService(IZaakGateway gateway, AclDefaults defaults, IClock clock) +public sealed class AclService(IZaakGateway gateway, AclDefaults defaults, IZaaktypeCatalog catalog, IClock clock) { - public Task OpenZaakAsync(DomainRegistration registration, CancellationToken ct = default) + public async Task OpenZaakAsync(DomainRegistration registration, CancellationToken ct = default) { ArgumentNullException.ThrowIfNull(registration); @@ -12,34 +12,34 @@ public sealed class AclService(IZaakGateway gateway, AclDefaults defaults, ICloc defaults.Bronorganisatie, defaults.VerantwoordelijkeOrganisatie, defaults.Vertrouwelijkheidaanduiding, - defaults.ZaaktypeUrl, + await catalog.GetZaaktypeUrlAsync(ct), clock.Today, registration.Reference); - return gateway.OpenZaakAsync(request, ct); + return await gateway.OpenZaakAsync(request, ct); } /// - /// Approve a zaak: set it to the eindstatus of the configured BIG zaaktype (ADR-0003 default). The - /// domain hands over only the zaak URL; the ACL owns which statustype means "approved" (§8.1). + /// Approve a zaak: set it to the eindstatus of the BIG zaaktype (resolved by identificatie, S-27). + /// The domain hands over only the zaak URL; the ACL owns which statustype means "approved" (§8.1). /// - public Task ApproveZaakAsync(Uri zaakUrl, CancellationToken ct = default) + public async Task ApproveZaakAsync(Uri zaakUrl, CancellationToken ct = default) { ArgumentNullException.ThrowIfNull(zaakUrl); - return gateway.SetZaakToEindstatusAsync(zaakUrl, defaults.ZaaktypeUrl, clock.Today, ct); + await gateway.SetZaakToEindstatusAsync(zaakUrl, await catalog.GetZaaktypeUrlAsync(ct), clock.Today, ct); } /// - /// Cancel a zaak on document-timeout expiry (S-10c): set it to the configured BIG zaaktype's - /// cancellation statustype + resultaat. The domain hands over only the zaak URL; the ACL owns which + /// Cancel a zaak on document-timeout expiry (S-10c): set it to the BIG zaaktype's cancellation + /// statustype + resultaat. The domain hands over only the zaak URL; the ACL owns which /// statustype/resultaat means "cancelled" (§8.1). /// - public Task CancelZaakAsync(Uri zaakUrl, CancellationToken ct = default) + public async Task CancelZaakAsync(Uri zaakUrl, CancellationToken ct = default) { ArgumentNullException.ThrowIfNull(zaakUrl); - return gateway.SetZaakToCancellationStatusAsync(zaakUrl, defaults.ZaaktypeUrl, clock.Today, ct); + await gateway.SetZaakToCancellationStatusAsync(zaakUrl, await catalog.GetZaaktypeUrlAsync(ct), clock.Today, ct); } /// The zaak's reference (its ZGW identificatie), for the read projection (#78). @@ -56,7 +56,7 @@ public sealed class AclService(IZaakGateway gateway, AclDefaults defaults, ICloc /// and hand the file to the gateway, which creates the informatieobject and relates it to the zaak. /// The domain supplies only the zaak, the bytes, and the file's name/type (§8.1). /// - public Task StoreDiplomaAsync(Uri zaakUrl, byte[] content, string fileName, string contentType, CancellationToken ct = default) + public async Task StoreDiplomaAsync(Uri zaakUrl, byte[] content, string fileName, string contentType, CancellationToken ct = default) { ArgumentNullException.ThrowIfNull(zaakUrl); ArgumentNullException.ThrowIfNull(content); @@ -65,7 +65,7 @@ public sealed class AclService(IZaakGateway gateway, AclDefaults defaults, ICloc var request = new DocumentRequest( defaults.Bronorganisatie, - defaults.InformatieobjecttypeUrl, + await catalog.GetInformatieobjecttypeUrlAsync(ct), defaults.Vertrouwelijkheidaanduiding, zaakUrl, clock.Today, @@ -76,6 +76,6 @@ public sealed class AclService(IZaakGateway gateway, AclDefaults defaults, ICloc Formaat: contentType, Inhoud: content); - return gateway.StoreDocumentAsync(request, ct); + return await gateway.StoreDocumentAsync(request, ct); } } diff --git a/services/acl/Acl.Application/CachedZaaktypeCatalog.cs b/services/acl/Acl.Application/CachedZaaktypeCatalog.cs new file mode 100644 index 0000000..bc9b293 --- /dev/null +++ b/services/acl/Acl.Application/CachedZaaktypeCatalog.cs @@ -0,0 +1,46 @@ +namespace Acl.Application; + +/// Resolves the zaaktype + diploma-informatieobjecttype URLs from the Catalogi API on first +/// use and caches them for the process lifetime (S-27, ADR-0021). Lazy (not at startup) so the ACL +/// never crash-loops when it boots before the catalogus is seeded/published; a failed +/// resolution is not cached, so it is retried on the next call (e.g. once the zaaktype is published). +/// A process restart re-resolves. +public sealed class CachedZaaktypeCatalog(IZaakGateway gateway, AclDefaults defaults) : IZaaktypeCatalog +{ + private readonly SemaphoreSlim gate = new(1, 1); + private Uri? zaaktype; + private Uri? informatieobjecttype; + + public Task GetZaaktypeUrlAsync(CancellationToken ct = default) => + ResolveOnceAsync( + () => zaaktype, value => zaaktype = value, + () => gateway.ResolveZaaktypeUrlAsync(defaults.ZaaktypeIdentificatie, ct), ct); + + public Task GetInformatieobjecttypeUrlAsync(CancellationToken ct = default) => + ResolveOnceAsync( + () => informatieobjecttype, value => informatieobjecttype = value, + () => gateway.ResolveInformatieobjecttypeUrlAsync(defaults.InformatieobjecttypeOmschrijving, ct), ct); + + // Double-checked, single-flight resolution: return the cache if set; otherwise resolve under the + // gate and cache only on success (a throw leaves the cache empty so the next call retries). + private async Task ResolveOnceAsync(Func read, Action store, Func> resolve, CancellationToken ct) + { + if (read() is { } cached) + return cached; + + await gate.WaitAsync(ct); + try + { + if (read() is { } existing) + return existing; + + var resolved = await resolve(); + store(resolved); + return resolved; + } + finally + { + gate.Release(); + } + } +} diff --git a/services/acl/Acl.Application/IZaakGateway.cs b/services/acl/Acl.Application/IZaakGateway.cs index 1e71337..3848d8b 100644 --- a/services/acl/Acl.Application/IZaakGateway.cs +++ b/services/acl/Acl.Application/IZaakGateway.cs @@ -32,4 +32,12 @@ public interface IZaakGateway /// the created informatieobject. /// Task StoreDocumentAsync(DocumentRequest request, CancellationToken ct = default); + + /// Resolve the URL of the published zaaktype with the given + /// from the Catalogi API (S-27). Throws if no published zaaktype matches. + Task ResolveZaaktypeUrlAsync(string identificatie, CancellationToken ct = default); + + /// Resolve the URL of the published informatieobjecttype with the given + /// from the Catalogi API (S-27). Throws if none matches. + Task ResolveInformatieobjecttypeUrlAsync(string omschrijving, CancellationToken ct = default); } diff --git a/services/acl/Acl.Application/IZaaktypeCatalog.cs b/services/acl/Acl.Application/IZaaktypeCatalog.cs new file mode 100644 index 0000000..2394c2b --- /dev/null +++ b/services/acl/Acl.Application/IZaaktypeCatalog.cs @@ -0,0 +1,12 @@ +namespace Acl.Application; + +/// Supplies the ACL's zaaktype + diploma-informatieobjecttype URLs, resolved from OpenZaak's +/// Catalogi API by their stable business keys ( / +/// ) rather than pinned in config (S-27, +/// ADR-0021). Implementations resolve lazily on first use and cache the result. +public interface IZaaktypeCatalog +{ + Task GetZaaktypeUrlAsync(CancellationToken ct = default); + + Task GetInformatieobjecttypeUrlAsync(CancellationToken ct = default); +} diff --git a/services/acl/Acl.Infrastructure/OpenZaakGateway.cs b/services/acl/Acl.Infrastructure/OpenZaakGateway.cs index 5f33ba8..156b810 100644 --- a/services/acl/Acl.Infrastructure/OpenZaakGateway.cs +++ b/services/acl/Acl.Infrastructure/OpenZaakGateway.cs @@ -142,6 +142,48 @@ public sealed class OpenZaakGateway(HttpClient http, OpenZaakOptions options) : return created; } + public async Task ResolveZaaktypeUrlAsync(string identificatie, CancellationToken ct = default) + { + ArgumentException.ThrowIfNullOrWhiteSpace(identificatie); + + // The published zaaktype with this identificatie; status=definitief excludes concepts. + var page = await GetAsync( + "/catalogi/api/v1/zaaktypen?status=definitief&identificatie=" + Uri.EscapeDataString(identificatie), + "zaaktypen", ct); + var match = (page.Results ?? []).FirstOrDefault() + ?? throw new InvalidOperationException( + $"No published zaaktype with identificatie '{identificatie}' found in OpenZaak — is the BIG catalogus seeded and published?"); + return new Uri(match.Url); + } + + public async Task ResolveInformatieobjecttypeUrlAsync(string omschrijving, CancellationToken ct = default) + { + ArgumentException.ThrowIfNullOrWhiteSpace(omschrijving); + + // The informatieobjecttypen collection has no omschrijving filter, so match client-side over the + // published ones. + var page = await GetAsync( + "/catalogi/api/v1/informatieobjecttypen?status=definitief", "informatieobjecttypen", ct); + var match = (page.Results ?? []).FirstOrDefault(i => i.Omschrijving == omschrijving) + ?? throw new InvalidOperationException( + $"No published informatieobjecttype '{omschrijving}' found in OpenZaak — is the BIG catalogus seeded and published?"); + return new Uri(match.Url); + } + + // GETs an absolute-by-path ZGW resource with auth (no CRS — catalogi is not a geo API). + private async Task GetAsync(string pathAndQuery, string label, CancellationToken ct) + { + using var message = new HttpRequestMessage(HttpMethod.Get, new Uri(options.BaseUrl, pathAndQuery)); + message.Headers.Authorization = + new AuthenticationHeaderValue("Bearer", ZgwToken.Mint(options.ClientId, options.Secret)); + + using var response = await http.SendAsync(message, ct); + await EnsureSuccessAsync(response, $"Querying {label}", ct); + + return await response.Content.ReadFromJsonAsync(ct) + ?? throw new InvalidOperationException($"OpenZaak returned an empty {label} response"); + } + // POSTs a non-geo ZGW resource (resultaat/status — no CRS headers). Buffers the body so uwsgi gets // a Content-Length instead of a chunked body (as with zaak-create). private async Task PostAsync(string path, object dto, string action, CancellationToken ct) @@ -298,4 +340,18 @@ public sealed class OpenZaakGateway(HttpClient http, OpenZaakOptions options) : private sealed record ZaakInformatieobjectDto( [property: JsonPropertyName("zaak")] string Zaak, [property: JsonPropertyName("informatieobject")] string Informatieobject); + + private sealed record ZaaktypePage( + [property: JsonPropertyName("results")] IReadOnlyList? Results); + + private sealed record ZaaktypeDto( + [property: JsonPropertyName("url")] string Url, + [property: JsonPropertyName("identificatie")] string? Identificatie); + + private sealed record InformatieobjecttypePage( + [property: JsonPropertyName("results")] IReadOnlyList? Results); + + private sealed record InformatieobjecttypeDto( + [property: JsonPropertyName("url")] string Url, + [property: JsonPropertyName("omschrijving")] string? Omschrijving); } diff --git a/services/acl/Acl.Tests/AclServiceTests.cs b/services/acl/Acl.Tests/AclServiceTests.cs index 01cbcef..930d380 100644 --- a/services/acl/Acl.Tests/AclServiceTests.cs +++ b/services/acl/Acl.Tests/AclServiceTests.cs @@ -6,6 +6,12 @@ public class AclServiceTests { private sealed class FakeGateway : IZaakGateway { + // The URLs the catalogus resolves the configured identificatie/omschrijving to (S-27). + public Uri ResolvedZaaktype { get; } = new("http://openzaak/catalogi/api/v1/zaaktypen/big"); + public Uri ResolvedInformatieobjecttype { get; } = new("http://openzaak/catalogi/api/v1/informatieobjecttypen/dip"); + public string? ResolvedByIdentificatie; + public string? ResolvedByOmschrijving; + public ZaakRequest? Captured; public Uri Result { get; } = new("http://openzaak/zaken/api/v1/zaken/abc"); @@ -47,6 +53,18 @@ public class AclServiceTests StoredDocument = request; return Task.FromResult(DocumentResult); } + + public Task ResolveZaaktypeUrlAsync(string identificatie, CancellationToken ct = default) + { + ResolvedByIdentificatie = identificatie; + return Task.FromResult(ResolvedZaaktype); + } + + public Task ResolveInformatieobjecttypeUrlAsync(string omschrijving, CancellationToken ct = default) + { + ResolvedByOmschrijving = omschrijving; + return Task.FromResult(ResolvedInformatieobjecttype); + } } private static AclDefaults Defaults() => new() @@ -54,28 +72,23 @@ public class AclServiceTests Bronorganisatie = "517439943", VerantwoordelijkeOrganisatie = "517439943", Vertrouwelijkheidaanduiding = "openbaar", - ZaaktypeUrl = new("http://openzaak/catalogi/api/v1/zaaktypen/big"), - InformatieobjecttypeUrl = new("http://openzaak/catalogi/api/v1/informatieobjecttypen/dip"), + ZaaktypeIdentificatie = "BIG-REGISTRATIE", + InformatieobjecttypeOmschrijving = "Diploma", }; + private static AclService ServiceWith(FakeGateway gateway, AclDefaults defaults, DateOnly today) => + new(gateway, defaults, new CachedZaaktypeCatalog(gateway, defaults), new FixedClock(today)); + private sealed class FixedClock(DateOnly today) : IClock { public DateOnly Today { get; } = today; } [Fact] - public async Task Opening_a_zaak_default_fills_zgw_fields_and_returns_the_zaak_url() + public async Task Opening_a_zaak_default_fills_zgw_fields_and_uses_the_resolved_zaaktype() { var gateway = new FakeGateway(); - var defaults = new AclDefaults - { - Bronorganisatie = "517439943", - VerantwoordelijkeOrganisatie = "517439943", - Vertrouwelijkheidaanduiding = "openbaar", - ZaaktypeUrl = new("http://openzaak/catalogi/api/v1/zaaktypen/big"), - InformatieobjecttypeUrl = new("http://openzaak/catalogi/api/v1/informatieobjecttypen/dip"), - }; - var service = new AclService(gateway, defaults, new FixedClock(new DateOnly(2026, 6, 4))); + var service = ServiceWith(gateway, Defaults(), new DateOnly(2026, 6, 4)); var url = await service.OpenZaakAsync(new DomainRegistration("123456782", "reg-77")); @@ -84,7 +97,9 @@ public class AclServiceTests Assert.Equal("517439943", req.Bronorganisatie); Assert.Equal("517439943", req.VerantwoordelijkeOrganisatie); Assert.Equal("openbaar", req.Vertrouwelijkheidaanduiding); - Assert.Equal(defaults.ZaaktypeUrl, req.Zaaktype); + // The zaaktype is resolved from the configured identificatie, not a pinned URL (S-27). + Assert.Equal("BIG-REGISTRATIE", gateway.ResolvedByIdentificatie); + Assert.Equal(gateway.ResolvedZaaktype, req.Zaaktype); Assert.Equal(new DateOnly(2026, 6, 4), req.Startdatum); // The registration reference becomes the zaak identificatie (#78). Assert.Equal("reg-77", req.Identificatie); @@ -94,33 +109,24 @@ public class AclServiceTests public async Task Rejects_a_null_registration_without_calling_the_gateway() { var gateway = new FakeGateway(); - var defaults = new AclDefaults - { - Bronorganisatie = "517439943", - VerantwoordelijkeOrganisatie = "517439943", - Vertrouwelijkheidaanduiding = "openbaar", - ZaaktypeUrl = new("http://openzaak/catalogi/api/v1/zaaktypen/big"), - InformatieobjecttypeUrl = new("http://openzaak/catalogi/api/v1/informatieobjecttypen/dip"), - }; - var service = new AclService(gateway, defaults, new FixedClock(new DateOnly(2026, 6, 4))); + var service = ServiceWith(gateway, Defaults(), new DateOnly(2026, 6, 4)); await Assert.ThrowsAsync(() => service.OpenZaakAsync(null!)); Assert.Null(gateway.Captured); } [Fact] - public async Task Approving_a_zaak_sets_it_to_its_zaaktypes_eindstatus_dated_today() + public async Task Approving_a_zaak_sets_it_to_its_resolved_zaaktypes_eindstatus_dated_today() { var gateway = new FakeGateway(); - var defaults = Defaults(); - var service = new AclService(gateway, defaults, new FixedClock(new DateOnly(2026, 6, 4))); + var service = ServiceWith(gateway, Defaults(), new DateOnly(2026, 6, 4)); var zaak = new Uri("http://openzaak/zaken/api/v1/zaken/abc"); await service.ApproveZaakAsync(zaak); Assert.NotNull(gateway.Approved); Assert.Equal(zaak, gateway.Approved!.Value.Zaak); - Assert.Equal(defaults.ZaaktypeUrl, gateway.Approved.Value.Zaaktype); + Assert.Equal(gateway.ResolvedZaaktype, gateway.Approved.Value.Zaaktype); Assert.Equal(new DateOnly(2026, 6, 4), gateway.Approved.Value.Datum); } @@ -128,7 +134,7 @@ public class AclServiceTests public async Task Approving_a_null_zaak_is_rejected_without_touching_the_gateway() { var gateway = new FakeGateway(); - var service = new AclService(gateway, Defaults(), new FixedClock(new DateOnly(2026, 6, 4))); + var service = ServiceWith(gateway, Defaults(), new DateOnly(2026, 6, 4)); await Assert.ThrowsAsync(() => service.ApproveZaakAsync(null!)); Assert.Null(gateway.Approved); @@ -138,15 +144,14 @@ public class AclServiceTests public async Task Cancelling_a_zaak_sets_it_to_the_cancellation_status_dated_today() { var gateway = new FakeGateway(); - var defaults = Defaults(); - var service = new AclService(gateway, defaults, new FixedClock(new DateOnly(2026, 6, 4))); + var service = ServiceWith(gateway, Defaults(), new DateOnly(2026, 6, 4)); var zaak = new Uri("http://openzaak/zaken/api/v1/zaken/abc"); await service.CancelZaakAsync(zaak); Assert.NotNull(gateway.Cancelled); Assert.Equal(zaak, gateway.Cancelled!.Value.Zaak); - Assert.Equal(defaults.ZaaktypeUrl, gateway.Cancelled.Value.Zaaktype); + Assert.Equal(gateway.ResolvedZaaktype, gateway.Cancelled.Value.Zaaktype); Assert.Equal(new DateOnly(2026, 6, 4), gateway.Cancelled.Value.Datum); // Cancellation must not touch the approval path. Assert.Null(gateway.Approved); @@ -156,18 +161,17 @@ public class AclServiceTests public async Task Cancelling_a_null_zaak_is_rejected_without_touching_the_gateway() { var gateway = new FakeGateway(); - var service = new AclService(gateway, Defaults(), new FixedClock(new DateOnly(2026, 6, 4))); + var service = ServiceWith(gateway, Defaults(), new DateOnly(2026, 6, 4)); await Assert.ThrowsAsync(() => service.CancelZaakAsync(null!)); Assert.Null(gateway.Cancelled); } [Fact] - public async Task Storing_a_diploma_default_fills_the_document_fields_and_returns_its_url() + public async Task Storing_a_diploma_default_fills_the_document_fields_and_uses_the_resolved_informatieobjecttype() { var gateway = new FakeGateway(); - var defaults = Defaults(); - var service = new AclService(gateway, defaults, new FixedClock(new DateOnly(2026, 6, 4))); + var service = ServiceWith(gateway, Defaults(), new DateOnly(2026, 6, 4)); var zaak = new Uri("http://openzaak/zaken/api/v1/zaken/abc"); var url = await service.StoreDiplomaAsync(zaak, [1, 2, 3], "diploma.pdf", "application/pdf"); @@ -175,7 +179,9 @@ public class AclServiceTests Assert.Equal(gateway.DocumentResult, url); var req = gateway.StoredDocument!; Assert.Equal(zaak, req.Zaak); - Assert.Equal(defaults.InformatieobjecttypeUrl, req.Informatieobjecttype); + // The informatieobjecttype is resolved from the configured omschrijving (S-27). + Assert.Equal("Diploma", gateway.ResolvedByOmschrijving); + Assert.Equal(gateway.ResolvedInformatieobjecttype, req.Informatieobjecttype); Assert.Equal("517439943", req.Bronorganisatie); Assert.Equal("openbaar", req.Vertrouwelijkheidaanduiding); Assert.Equal(new DateOnly(2026, 6, 4), req.Creatiedatum); @@ -188,7 +194,7 @@ public class AclServiceTests [Fact] public async Task Storing_a_diploma_rejects_null_or_blank_arguments() { - var service = new AclService(new FakeGateway(), Defaults(), new FixedClock(new DateOnly(2026, 6, 4))); + var service = ServiceWith(new FakeGateway(), Defaults(), new DateOnly(2026, 6, 4)); var zaak = new Uri("http://openzaak/zaken/api/v1/zaken/abc"); await Assert.ThrowsAsync(() => service.StoreDiplomaAsync(null!, [1], "d.pdf", "application/pdf")); @@ -201,7 +207,7 @@ public class AclServiceTests public async Task Reading_a_zaak_reference_returns_the_zaaks_identificatie() { var gateway = new FakeGateway(); - var service = new AclService(gateway, Defaults(), new FixedClock(new DateOnly(2026, 6, 4))); + var service = ServiceWith(gateway, Defaults(), new DateOnly(2026, 6, 4)); var zaak = new Uri("http://openzaak/zaken/api/v1/zaken/abc"); var reference = await service.GetZaakReferenceAsync(zaak); @@ -214,7 +220,7 @@ public class AclServiceTests public async Task Reading_a_null_zaak_reference_is_rejected() { var gateway = new FakeGateway(); - var service = new AclService(gateway, Defaults(), new FixedClock(new DateOnly(2026, 6, 4))); + var service = ServiceWith(gateway, Defaults(), new DateOnly(2026, 6, 4)); await Assert.ThrowsAsync(() => service.GetZaakReferenceAsync(null!)); Assert.Null(gateway.ReadReferenceFor); diff --git a/services/acl/Acl.Tests/OpenZaakGatewayTests.cs b/services/acl/Acl.Tests/OpenZaakGatewayTests.cs index f167f75..fb750aa 100644 --- a/services/acl/Acl.Tests/OpenZaakGatewayTests.cs +++ b/services/acl/Acl.Tests/OpenZaakGatewayTests.cs @@ -675,4 +675,92 @@ public class OpenZaakGatewayTests await Assert.ThrowsAsync(() => Gateway(handler).StoreDocumentAsync(null!)); } + + // ── Catalogi resolution by business key (S-27) ──────────────────────────────────────────────── + + [Fact] + public async Task Resolves_the_published_zaaktype_url_by_identificatie() + { + HttpRequestMessage? seen = null; + var handler = new StubHandler(req => + { + seen = req; + return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK) + { + Content = JsonContent.Create(new + { + results = new[] { new { url = "http://openzaak/catalogi/api/v1/zaaktypen/big", identificatie = "BIG-REGISTRATIE" } }, + }), + }); + }); + + var url = await Gateway(handler).ResolveZaaktypeUrlAsync("BIG-REGISTRATIE"); + + Assert.Equal("http://openzaak/catalogi/api/v1/zaaktypen/big", url.ToString()); + Assert.Equal(HttpMethod.Get, seen!.Method); + // Filters to the published zaaktype with that identificatie, and authenticates. + Assert.Contains("/catalogi/api/v1/zaaktypen", seen.RequestUri!.ToString()); + Assert.Contains("status=definitief", seen.RequestUri!.Query); + Assert.Contains("identificatie=BIG-REGISTRATIE", seen.RequestUri!.Query); + Assert.Equal("Bearer", seen.Headers.Authorization!.Scheme); + } + + [Fact] + public async Task Resolving_a_zaaktype_throws_a_clear_error_when_none_is_published() + { + var handler = new StubHandler(_ => Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK) + { + Content = JsonContent.Create(new { results = Array.Empty() }), + })); + + var ex = await Assert.ThrowsAsync( + () => Gateway(handler).ResolveZaaktypeUrlAsync("BIG-REGISTRATIE")); + Assert.Contains("BIG-REGISTRATIE", ex.Message); + } + + [Fact] + public async Task Resolves_the_informatieobjecttype_url_by_omschrijving() + { + var handler = new StubHandler(_ => Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK) + { + Content = JsonContent.Create(new + { + results = new[] + { + new { url = "http://openzaak/catalogi/api/v1/informatieobjecttypen/other", omschrijving = "Overig" }, + new { url = "http://openzaak/catalogi/api/v1/informatieobjecttypen/dip", omschrijving = "Diploma" }, + }, + }), + })); + + var url = await Gateway(handler).ResolveInformatieobjecttypeUrlAsync("Diploma"); + + // Matches on omschrijving, not position. + Assert.Equal("http://openzaak/catalogi/api/v1/informatieobjecttypen/dip", url.ToString()); + } + + [Fact] + public async Task Resolving_an_informatieobjecttype_throws_when_no_omschrijving_matches() + { + var handler = new StubHandler(_ => Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK) + { + Content = JsonContent.Create(new + { + results = new[] { new { url = "http://openzaak/catalogi/api/v1/informatieobjecttypen/other", omschrijving = "Overig" } }, + }), + })); + + var ex = await Assert.ThrowsAsync( + () => Gateway(handler).ResolveInformatieobjecttypeUrlAsync("Diploma")); + Assert.Contains("Diploma", ex.Message); + } + + [Fact] + public async Task Resolving_rejects_a_blank_business_key_without_calling_openzaak() + { + var handler = new StubHandler(_ => throw new InvalidOperationException("should not be sent")); + + await Assert.ThrowsAnyAsync(() => Gateway(handler).ResolveZaaktypeUrlAsync(" ")); + await Assert.ThrowsAnyAsync(() => Gateway(handler).ResolveInformatieobjecttypeUrlAsync(" ")); + } } diff --git a/services/acl/Acl.Tests/ZaaktypeCatalogTests.cs b/services/acl/Acl.Tests/ZaaktypeCatalogTests.cs new file mode 100644 index 0000000..8f3f7f6 --- /dev/null +++ b/services/acl/Acl.Tests/ZaaktypeCatalogTests.cs @@ -0,0 +1,92 @@ +using Acl.Application; + +namespace Acl.Tests; + +public class ZaaktypeCatalogTests +{ + // A gateway that only supports resolution; the other members are unused here. + private sealed class ResolvingGateway : IZaakGateway + { + public int ZaaktypeCalls; + public int InformatieobjecttypeCalls; + public string? LastIdentificatie; + public string? LastOmschrijving; + public int ThrowZaaktypeTimes; + public Uri ZaaktypeUrl { get; } = new("http://openzaak/catalogi/api/v1/zaaktypen/big"); + public Uri InformatieobjecttypeUrl { get; } = new("http://openzaak/catalogi/api/v1/informatieobjecttypen/dip"); + + public Task ResolveZaaktypeUrlAsync(string identificatie, CancellationToken ct = default) + { + ZaaktypeCalls++; + LastIdentificatie = identificatie; + if (ZaaktypeCalls <= ThrowZaaktypeTimes) + throw new InvalidOperationException("no published zaaktype yet"); + return Task.FromResult(ZaaktypeUrl); + } + + public Task ResolveInformatieobjecttypeUrlAsync(string omschrijving, CancellationToken ct = default) + { + InformatieobjecttypeCalls++; + LastOmschrijving = omschrijving; + return Task.FromResult(InformatieobjecttypeUrl); + } + + public Task OpenZaakAsync(ZaakRequest request, CancellationToken ct = default) => throw new NotSupportedException(); + public Task SetZaakToEindstatusAsync(Uri z, Uri zt, DateOnly d, CancellationToken ct = default) => throw new NotSupportedException(); + public Task SetZaakToCancellationStatusAsync(Uri z, Uri zt, DateOnly d, CancellationToken ct = default) => throw new NotSupportedException(); + public Task GetZaakIdentificatieAsync(Uri z, CancellationToken ct = default) => throw new NotSupportedException(); + public Task StoreDocumentAsync(DocumentRequest r, CancellationToken ct = default) => throw new NotSupportedException(); + } + + private static AclDefaults Defaults() => new() + { + Bronorganisatie = "517439943", + VerantwoordelijkeOrganisatie = "517439943", + Vertrouwelijkheidaanduiding = "openbaar", + ZaaktypeIdentificatie = "BIG-REGISTRATIE", + InformatieobjecttypeOmschrijving = "Diploma", + }; + + [Fact] + public async Task Resolves_the_zaaktype_and_informatieobjecttype_by_their_configured_business_keys() + { + var gateway = new ResolvingGateway(); + var catalog = new CachedZaaktypeCatalog(gateway, Defaults()); + + Assert.Equal(gateway.ZaaktypeUrl, await catalog.GetZaaktypeUrlAsync()); + Assert.Equal(gateway.InformatieobjecttypeUrl, await catalog.GetInformatieobjecttypeUrlAsync()); + Assert.Equal("BIG-REGISTRATIE", gateway.LastIdentificatie); + Assert.Equal("Diploma", gateway.LastOmschrijving); + } + + [Fact] + public async Task Caches_the_resolved_urls_so_the_gateway_is_hit_once() + { + var gateway = new ResolvingGateway(); + var catalog = new CachedZaaktypeCatalog(gateway, Defaults()); + + for (var i = 0; i < 3; i++) + { + await catalog.GetZaaktypeUrlAsync(); + await catalog.GetInformatieobjecttypeUrlAsync(); + } + + Assert.Equal(1, gateway.ZaaktypeCalls); + Assert.Equal(1, gateway.InformatieobjecttypeCalls); + } + + [Fact] + public async Task Does_not_cache_a_failed_resolution_so_it_is_retried() + { + // The zaaktype is not published yet on the first call; the catalog must retry (not cache the + // failure) so a later call succeeds once it is published. + var gateway = new ResolvingGateway { ThrowZaaktypeTimes = 1 }; + var catalog = new CachedZaaktypeCatalog(gateway, Defaults()); + + await Assert.ThrowsAsync(() => catalog.GetZaaktypeUrlAsync()); + var url = await catalog.GetZaaktypeUrlAsync(); + + Assert.Equal(gateway.ZaaktypeUrl, url); + Assert.Equal(2, gateway.ZaaktypeCalls); + } +} -- 2.54.0 From 24c7ebe8cb8e807a529aae5e7c17700e9de859c1 Mon Sep 17 00:00:00 2001 From: Niek Otten Date: Wed, 22 Jul 2026 15:57:10 +0200 Subject: [PATCH 2/5] test(acl): live integration tests for catalogus resolution by business key (refs #113) Assert the gateway resolves the published BIG-REGISTRATIE zaaktype + Diploma informatieobjecttype against a real seeded OpenZaak, and that an unknown identificatie throws a clear error. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../OpenZaakGatewayIntegrationTests.cs | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/services/acl/Acl.IntegrationTests/OpenZaakGatewayIntegrationTests.cs b/services/acl/Acl.IntegrationTests/OpenZaakGatewayIntegrationTests.cs index d59d0d9..70c84bf 100644 --- a/services/acl/Acl.IntegrationTests/OpenZaakGatewayIntegrationTests.cs +++ b/services/acl/Acl.IntegrationTests/OpenZaakGatewayIntegrationTests.cs @@ -161,4 +161,32 @@ public sealed class OpenZaakGatewayIntegrationTests(OpenZaakFixture stack) Assert.Contains(relations.EnumerateArray(), r => r.GetProperty("zaak").GetString() == zaakUrl.ToString()); } + + [Fact] + public async Task Resolves_the_published_zaaktype_and_diploma_informatieobjecttype_by_business_key() + { + var expectedZaaktype = await stack.FindPublishedBigZaaktypeAsync(); + Assert.True(expectedZaaktype is not null, + "No published BIG-REGISTRATIE zaaktype found — seed the stack with OZ_PUBLISH=1."); + var expectedInformatieobjecttype = await stack.FindPublishedDiplomaInformatieobjecttypeAsync(); + Assert.True(expectedInformatieobjecttype is not null, + "No published Diploma informatieobjecttype found — seed the stack with OZ_PUBLISH=1."); + + var gateway = new OpenZaakGateway(stack.Http, stack.Options); + + // The ACL discovers both URLs from the live Catalogi API by their stable business keys (S-27), + // matching what the fixture found independently — no pinned URL needed. + Assert.Equal(expectedZaaktype, await gateway.ResolveZaaktypeUrlAsync("BIG-REGISTRATIE")); + Assert.Equal(expectedInformatieobjecttype, await gateway.ResolveInformatieobjecttypeUrlAsync("Diploma")); + } + + [Fact] + public async Task Resolving_an_unknown_zaaktype_identificatie_throws_a_clear_error() + { + var gateway = new OpenZaakGateway(stack.Http, stack.Options); + + var ex = await Assert.ThrowsAsync( + () => gateway.ResolveZaaktypeUrlAsync("NO-SUCH-ZAAKTYPE")); + Assert.Contains("NO-SUCH-ZAAKTYPE", ex.Message); + } } -- 2.54.0 From 734dfb21dda895313243206486066d5722192a74 Mon Sep 17 00:00:00 2001 From: Niek Otten Date: Wed, 22 Jul 2026 15:57:10 +0200 Subject: [PATCH 3/5] refactor(infra): drop zaaktype/informatieobjecttype URL injection; add ADR-0021 (refs #113) The ACL now discovers those URLs itself (S-27), so no stack captures/injects them: docker-compose.yml/.local.yml carry ZaaktypeIdentificatie/InformatieobjecttypeOmschrijving instead of placeholder URLs, run-domain-check.sh + local-seed stop emitting the URLs, and the local acl.env shrinks to the OpenZaak base URL. That base URL injection stays: OpenZaak rejects a single-label host on zaak-create (confirmed), so the ACL is still pointed at the container IP. ADR-0021 + demo-script note. Co-Authored-By: Claude Opus 4.8 (1M context) --- ...-acl-resolves-zaaktype-by-identificatie.md | 67 ++++++++++++++++++ docs/demo-script.md | 28 +++++++- infra/docker-compose.local.yml | 17 ++--- infra/docker-compose.yml | 23 +++--- infra/local/seed-zaaktype.sh | 14 ++-- .../seed_catalogus.cpython-314.pyc | Bin 0 -> 19398 bytes infra/run-domain-check.sh | 17 ++--- 7 files changed, 126 insertions(+), 40 deletions(-) create mode 100644 docs/architecture/adr-0021-acl-resolves-zaaktype-by-identificatie.md create mode 100644 infra/openzaak/__pycache__/seed_catalogus.cpython-314.pyc diff --git a/docs/architecture/adr-0021-acl-resolves-zaaktype-by-identificatie.md b/docs/architecture/adr-0021-acl-resolves-zaaktype-by-identificatie.md new file mode 100644 index 0000000..15a296f --- /dev/null +++ b/docs/architecture/adr-0021-acl-resolves-zaaktype-by-identificatie.md @@ -0,0 +1,67 @@ +# ADR-0021: The ACL resolves its zaaktype by identificatie, not a pinned URL + +- **Status:** Accepted +- **Date:** 2026-07-22 +- **Deciders:** Respellion engineering +- **Relates to:** S-27 (#113), proposed in #117. The cleaner design deliberately split out of S-B04 + (#110, ADR-0020), which fixed the local stack with an infra-only bootstrap. + +## Context + +The ACL was handed a **pinned zaaktype URL** (`Acl__Defaults__ZaaktypeUrl`) and diploma +informatieobjecttype URL. OpenZaak assigns those UUIDs at creation, so the URL is not knowable when +the compose file is written — every stack had to seed the catalogus and then capture + inject the +resulting URLs out of band: `run-domain-check.sh` for CI, and the `local-seed` → `acl.env` bootstrap +(ADR-0020) for `make local`. Brittle, and a stale/placeholder URL failed opaquely (OpenZaak 400). + +## Decision + +**The ACL resolves its zaaktype (by `identificatie`) and diploma informatieobjecttype (by +`omschrijving`) from OpenZaak's Catalogi API, instead of being handed the URLs.** + +- **Config:** `AclDefaults.ZaaktypeUrl`/`InformatieobjecttypeUrl` → `ZaaktypeIdentificatie` + (`BIG-REGISTRATIE`) / `InformatieobjecttypeOmschrijving` (`Diploma`). +- **Lookup (gateway, §8.1):** `GET /catalogi/api/v1/zaaktypen?status=definitief&identificatie=…` → + the published zaaktype URL; `GET /catalogi/api/v1/informatieobjecttypen?status=definitief` matched + on `omschrijving`. Reuses the gateway's existing catalogus-query machinery. +- **Timing = lazy + cached (`CachedZaaktypeCatalog`).** Resolve on first use (first zaak open / + document store) and cache for the process lifetime. Lazy avoids a startup ordering coupling — the + ACL never crash-loops when it boots before the catalogus is published. A **failed** resolution is + not cached, so it is retried on the next call (e.g. once the zaaktype is published); a restart + re-resolves. +- **Failure mode:** no published match → a clear "No published zaaktype with identificatie '…' found + in OpenZaak — is the BIG catalogus seeded and published?" error, replacing the opaque placeholder + 400. + +## Consequences + +**Positive** + +- No stack captures or injects a server-assigned URL any more: `run-domain-check.sh` drops the + `ACL_ZAAKTYPE_URL`/`ACL_INFORMATIEOBJECTTYPE_URL` capture+inject, `docker-compose.yml`/`.local.yml` + drop the placeholder URL env, and `local-seed`/`acl.env` shrink to a single line. The ACL + self-configures from the catalogus it already talks to. +- The failure mode is legible (a named error instead of a 400 on a zeros-UUID). + +**Negative / costs** + +- The ACL still needs its OpenZaak **BaseUrl** pointed at a **URL-valid host (a container IP)**, so + the base-URL injection from ADR-0020 stays (the local `acl.env` now carries only that; CI keeps + `ACL_OPENZAAK_BASEURL`). This is **not** something S-27 can remove: OpenZaak validates the + `zaaktype` field on zaak-create with Django's URLValidator and **rejects a single-label host** + (`http://openzaak:8000/…` → `zaaktype: bad-url, "Voer een geldige URL in."`, confirmed empirically). + So ADR-0020's `seed-env` volume + ACL entrypoint shim are **simplified, not deleted**. +- New branching in the gateway/resolver → unit + integration test surface; the mutation ratchet + covers it (§5). +- A seed step still **creates + publishes** the zaaktype (this ADR changes only discovery). Reaching + OpenZaak's Catalogi API to *seed* likewise needs the IP host (its query params hit the same + URLValidator) — unchanged from before. + +## Alternatives considered + +- **Resolve at startup** (eager). Simpler cache, but reintroduces the ordering coupling (crash-loop + if the catalogus isn't published yet). Rejected in favour of lazy. +- **Per-request resolution** (no cache). No stale-cache risk, but a Catalogi lookup on every ACL + operation. Rejected; a process-lifetime cache with restart-to-refresh is enough here. +- **Keep the pinned URL** (status quo / ADR-0020 only). Rejected — the brittleness this ADR removes is + exactly what S-27 was carved out to fix. diff --git a/docs/demo-script.md b/docs/demo-script.md index 9191f05..c94a4e0 100644 --- a/docs/demo-script.md +++ b/docs/demo-script.md @@ -26,9 +26,31 @@ make verify-local # → "OK — a fresh local stack completed the flow with # test123); it shows as INGESCHREVEN in the openbaar register at http://localhost:8141. ``` -> The zaaktype UUID is server-assigned, so `local-seed` writes the real URL into a shared volume as -> `acl.env` and the ACL sources it on startup (ADR-0020). The cleaner long-term fix — the ACL -> resolving its zaaktype by `identificatie` — is tracked separately as S-27 (#113). +> The zaaktype is discovered by the ACL itself since S-27 (below); `local-seed`'s `acl.env` now +> carries only OpenZaak's IP base URL, which the ACL still needs because OpenZaak rejects a +> single-label host on zaak-create (ADR-0020 + ADR-0021). + +--- + +## S-27 — ACL resolves its zaaktype by identificatie, not a pinned URL (#113, ADR-0021) + +**Outcome:** the ACL discovers its BIG zaaktype (by `identificatie`) and diploma informatieobjecttype +(by `omschrijving`) from OpenZaak's Catalogi API, instead of being handed the server-assigned URLs. +No user-visible behaviour change — the flow runs exactly as before — but no stack captures/injects a +zaaktype URL any more, and a missing catalogus now fails with a clear message instead of an opaque 400. + +```bash +# The live ACL↔OpenZaak integration test proves resolution against a real seeded OpenZaak: +make verify-acl # → "resolves the published BIG-REGISTRATIE zaaktype + Diploma informatieobjecttype by business key" + +# End-to-end unchanged (the ACL self-discovers the zaaktype during the flow): +make verify-local # local stack — still green, now with no zaaktype-URL injection +make verify-domain # CI stack — recreates the ACL pointed only at OpenZaak's IP (no URL to inject) +``` + +> The ACL still needs its OpenZaak base URL at a URL-valid host (a container IP): OpenZaak's +> URLValidator rejects a single-label host like `openzaak:8000` on zaak-create. So ADR-0020's base-URL +> injection stays; only the zaaktype/informatieobjecttype **URL** injection is gone (ADR-0021). --- diff --git a/infra/docker-compose.local.yml b/infra/docker-compose.local.yml index beafbb8..0bb7aad 100644 --- a/infra/docker-compose.local.yml +++ b/infra/docker-compose.local.yml @@ -315,21 +315,22 @@ services: context: ../services/acl dockerfile: Dockerfile image: register-referentie/acl:dev - # The base/zaaktype/informatieobjecttype below are PLACEHOLDERS. The real, server-assigned - # values are written by the local-seed one-shot into seed-env:/seed/acl.env, which the entrypoint - # sources (set -a) so they override these before the app starts (S-B04, #110, ADR-0020). Sourcing - # a runtime-generated env file is why we override the entrypoint here rather than use `env_file:` - # (which compose reads at parse time, before the seed has run). + # The ACL discovers its zaaktype + informatieobjecttype URLs from the Catalogi API by the business + # keys below (S-27, ADR-0021), so no URL is injected. It still needs its OpenZaak BaseUrl pointed at + # a URL-valid host (OpenZaak rejects a single-label host like `openzaak` on zaak-create), so the + # local-seed one-shot writes that IP base into seed-env:/seed/acl.env, which the entrypoint sources + # (set -a) before the app starts. A runtime-generated env file is why we override the entrypoint here + # rather than use `env_file:` (which compose reads at parse time, before the seed has run). entrypoint: ["/bin/sh", "-c", "set -a; . /seed/acl.env; set +a; exec dotnet Acl.Api.dll"] environment: - Acl__OpenZaak__BaseUrl: http://openzaak:8000/ + Acl__OpenZaak__BaseUrl: http://openzaak:8000/ # placeholder; seed-env/acl.env supplies the IP base Acl__OpenZaak__ClientId: big-reference-seed Acl__OpenZaak__Secret: insecure-dev-secret-change-me Acl__Defaults__Bronorganisatie: "517439943" Acl__Defaults__VerantwoordelijkeOrganisatie: "517439943" Acl__Defaults__Vertrouwelijkheidaanduiding: openbaar - Acl__Defaults__ZaaktypeUrl: http://openzaak:8000/catalogi/api/v1/zaaktypen/00000000-0000-0000-0000-000000000000 - Acl__Defaults__InformatieobjecttypeUrl: http://openzaak:8000/catalogi/api/v1/informatieobjecttypen/00000000-0000-0000-0000-000000000000 + Acl__Defaults__ZaaktypeIdentificatie: BIG-REGISTRATIE + Acl__Defaults__InformatieobjecttypeOmschrijving: Diploma ports: - "8100:8080" volumes: diff --git a/infra/docker-compose.yml b/infra/docker-compose.yml index ce76cdf..99f18fe 100644 --- a/infra/docker-compose.yml +++ b/infra/docker-compose.yml @@ -15,12 +15,12 @@ # # docker compose -f infra/docker-compose.yml up -d --build --wait # -# After first boot, seed the BIG catalogus and note the zaaktype URL: -# python infra/openzaak/seed_catalogus.py -# Then set ACL_ZAAKTYPE_URL in a .env file or your shell and re-up the acl -# service: -# export ACL_ZAAKTYPE_URL=http://openzaak:8000/catalogi/api/v1/zaaktypen/ -# docker compose -f infra/docker-compose.yml up -d acl +# After first boot, seed + publish the BIG catalogus: +# OZ_PUBLISH=1 python infra/openzaak/seed_catalogus.py +# The ACL discovers the zaaktype by identificatie (S-27, ADR-0021), so there is no URL to inject — +# just point its BaseUrl at an OpenZaak host OpenZaak accepts on zaak-create (a container IP; a +# single-label host is rejected): +# ACL_OPENZAAK_BASEURL=http://:8000/ docker compose -f infra/docker-compose.yml up -d acl services: @@ -304,11 +304,12 @@ services: Acl__Defaults__Bronorganisatie: "517439943" Acl__Defaults__VerantwoordelijkeOrganisatie: "517439943" Acl__Defaults__Vertrouwelijkheidaanduiding: openbaar - # Override with the real zaaktype URL after running seed_catalogus.py. - Acl__Defaults__ZaaktypeUrl: ${ACL_ZAAKTYPE_URL:-http://openzaak:8000/catalogi/api/v1/zaaktypen/00000000-0000-0000-0000-000000000000} - # The informatieobjecttype a diploma is filed under (S-10b). Placeholder until seed_catalogus.py - # (OZ_PUBLISH=1) reports the real URL, which verify-domain injects like the zaaktype URL. - Acl__Defaults__InformatieobjecttypeUrl: ${ACL_INFORMATIEOBJECTTYPE_URL:-http://openzaak:8000/catalogi/api/v1/informatieobjecttypen/00000000-0000-0000-0000-000000000000} + # The ACL resolves the (server-assigned) zaaktype + diploma informatieobjecttype URLs from the + # Catalogi API by these stable business keys (S-27, ADR-0021) — no URL to capture and inject. + # BaseUrl above stays overridable because OpenZaak rejects a single-label host on zaak creation, + # so verify-domain still points the ACL at OpenZaak's container IP. + Acl__Defaults__ZaaktypeIdentificatie: BIG-REGISTRATIE + Acl__Defaults__InformatieobjecttypeOmschrijving: Diploma ports: - "8100:8080" healthcheck: diff --git a/infra/local/seed-zaaktype.sh b/infra/local/seed-zaaktype.sh index 5773ad4..80906ed 100755 --- a/infra/local/seed-zaaktype.sh +++ b/infra/local/seed-zaaktype.sh @@ -21,15 +21,13 @@ echo ">> seeding + publishing the BIG zaaktype at ${OZ_BASE} (idempotent)" out="$(python3 /work/seed_catalogus.py)" echo "$out" -zt="$(printf '%s\n' "$out" | sed -n 's/^ZAAKTYPE_URL //p' | head -1)" -iot="$(printf '%s\n' "$out" | sed -n 's/^INFORMATIEOBJECTTYPE_URL //p' | head -1)" -[ -n "$zt" ] || { echo "ERROR: seed did not report a ZAAKTYPE_URL" >&2; exit 1; } -[ -n "$iot" ] || { echo "ERROR: seed did not report an INFORMATIEOBJECTTYPE_URL" >&2; exit 1; } +# Sanity-check that the zaaktype was actually published (the ACL discovers it by identificatie, S-27). +printf '%s\n' "$out" | grep -q '^ZAAKTYPE_URL ' || { echo "ERROR: seed did not publish the zaaktype" >&2; exit 1; } -# The ACL entrypoint sources this; these keys override the placeholder defaults in the compose file. +# The ACL resolves the zaaktype/informatieobjecttype URLs itself (S-27, ADR-0021); the only value it +# still needs injected is the OpenZaak base URL at a URL-valid host (the container IP), because OpenZaak +# rejects a single-label host on zaak-create. The ACL entrypoint sources this. cat > /out/acl.env <> wrote /out/acl.env (base=${OZ_BASE}/ zaaktype=${zt})" +echo ">> wrote /out/acl.env (base=${OZ_BASE}/)" diff --git a/infra/openzaak/__pycache__/seed_catalogus.cpython-314.pyc b/infra/openzaak/__pycache__/seed_catalogus.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8f2c9eabf78ff36e1f70f19bba5b2ac5184ce8b0 GIT binary patch literal 19398 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 literal 0 HcmV?d00001 diff --git a/infra/run-domain-check.sh b/infra/run-domain-check.sh index 3c360d6..833032a 100755 --- a/infra/run-domain-check.sh +++ b/infra/run-domain-check.sh @@ -30,21 +30,18 @@ oz_ip="$(ip "$oz")"; dom_ip="$(ip "$dom")" oz_base="http://$oz_ip:8000" echo ">> openzaak=$oz_ip domain=$dom_ip network=$net" -echo ">> seeding a published BIG zaaktype (idempotent) and capturing its URL" +echo ">> seeding + publishing a BIG zaaktype (idempotent)" sid="$(docker create --network "$net" -e "OZ_BASE=$oz_base" -e OZ_PUBLISH=1 python:3-slim python /seed.py)" docker cp "$here/openzaak/seed_catalogus.py" "$sid:/seed.py" >/dev/null seed_out="$(docker start -a "$sid")" -zt_url="$(printf '%s\n' "$seed_out" | sed -n 's/^ZAAKTYPE_URL //p' | head -1)" -iot_url="$(printf '%s\n' "$seed_out" | sed -n 's/^INFORMATIEOBJECTTYPE_URL //p' | head -1)" docker rm -f "$sid" >/dev/null -[ -n "$zt_url" ] || { echo "ERROR: seed did not report a ZAAKTYPE_URL" >&2; exit 1; } -[ -n "$iot_url" ] || { echo "ERROR: seed did not report an INFORMATIEOBJECTTYPE_URL" >&2; exit 1; } -echo ">> zaaktype: $zt_url" -echo ">> informatieobjecttype: $iot_url" +printf '%s\n' "$seed_out" | grep -q '^ZAAKTYPE_URL ' || { echo "ERROR: seed did not publish the zaaktype" >&2; exit 1; } -echo ">> recreating the acl service pointed at the seeded zaaktype + informatieobjecttype (host-consistent)" -ACL_ZAAKTYPE_URL="$zt_url" ACL_INFORMATIEOBJECTTYPE_URL="$iot_url" ACL_OPENZAAK_BASEURL="$oz_base/" \ - docker compose -f "$compose" up -d acl +# The ACL resolves the zaaktype + informatieobjecttype by identificatie/omschrijving (S-27, ADR-0021), +# so there is no URL to inject — only the OpenZaak base URL, pointed at the same host's container IP +# (OpenZaak rejects a single-label host on zaak-create). +echo ">> recreating the acl service pointed at OpenZaak's IP (it resolves the zaaktype itself, S-27)" +ACL_OPENZAAK_BASEURL="$oz_base/" docker compose -f "$compose" up -d acl WAIT_TIMEOUT="${WAIT_TIMEOUT:-120}" bash "$here/wait-healthy.sh" acl echo ">> submitting a registration to the domain" -- 2.54.0 From 2fe187cbdd38f451eae6e4539c76d23a401f4815 Mon Sep 17 00:00:00 2001 From: Niek Otten Date: Wed, 22 Jul 2026 16:03:56 +0200 Subject: [PATCH 4/5] test(acl): cover the resolve null/error paths to hold the mutation ratchet (refs #113) Kills the survivors the new resolution code introduced: null Results (no "results" in the response) must throw "none found" not NRE; a non-success catalogi response must surface an error naming the resource. ACL mutation score 93.89% (break 90%). Co-Authored-By: Claude Opus 4.8 (1M context) --- .../acl/Acl.Tests/OpenZaakGatewayTests.cs | 79 ++++++++++++++++--- 1 file changed, 70 insertions(+), 9 deletions(-) diff --git a/services/acl/Acl.Tests/OpenZaakGatewayTests.cs b/services/acl/Acl.Tests/OpenZaakGatewayTests.cs index fb750aa..081701c 100644 --- a/services/acl/Acl.Tests/OpenZaakGatewayTests.cs +++ b/services/acl/Acl.Tests/OpenZaakGatewayTests.cs @@ -721,24 +721,85 @@ public class OpenZaakGatewayTests [Fact] public async Task Resolves_the_informatieobjecttype_url_by_omschrijving() { - var handler = new StubHandler(_ => Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK) + HttpRequestMessage? seen = null; + var handler = new StubHandler(req => { - Content = JsonContent.Create(new + seen = req; + return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK) { - results = new[] + Content = JsonContent.Create(new { - new { url = "http://openzaak/catalogi/api/v1/informatieobjecttypen/other", omschrijving = "Overig" }, - new { url = "http://openzaak/catalogi/api/v1/informatieobjecttypen/dip", omschrijving = "Diploma" }, - }, - }), - })); + results = new[] + { + new { url = "http://openzaak/catalogi/api/v1/informatieobjecttypen/other", omschrijving = "Overig" }, + new { url = "http://openzaak/catalogi/api/v1/informatieobjecttypen/dip", omschrijving = "Diploma" }, + }, + }), + }); + }); var url = await Gateway(handler).ResolveInformatieobjecttypeUrlAsync("Diploma"); - // Matches on omschrijving, not position. + // Queries the published informatieobjecttypen collection, and matches on omschrijving (not position). + Assert.Contains("/catalogi/api/v1/informatieobjecttypen", seen!.RequestUri!.ToString()); + Assert.Contains("status=definitief", seen.RequestUri!.Query); Assert.Equal("http://openzaak/catalogi/api/v1/informatieobjecttypen/dip", url.ToString()); } + [Fact] + public async Task Resolving_a_zaaktype_throws_when_the_response_carries_no_results() + { + // No "results" property → the page's Results is null; the gateway must treat that as "none + // found" (not dereference null). + var handler = new StubHandler(_ => Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK) + { + Content = JsonContent.Create(new { count = 0 }), + })); + + await Assert.ThrowsAsync( + () => Gateway(handler).ResolveZaaktypeUrlAsync("BIG-REGISTRATIE")); + } + + [Fact] + public async Task Resolving_an_informatieobjecttype_throws_when_the_response_carries_no_results() + { + var handler = new StubHandler(_ => Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK) + { + Content = JsonContent.Create(new { count = 0 }), + })); + + await Assert.ThrowsAsync( + () => Gateway(handler).ResolveInformatieobjecttypeUrlAsync("Diploma")); + } + + [Fact] + public async Task Resolving_a_zaaktype_surfaces_a_non_success_catalogi_response() + { + var handler = new StubHandler(_ => Task.FromResult(new HttpResponseMessage(HttpStatusCode.InternalServerError) + { + Content = new StringContent("boom"), + })); + + var ex = await Assert.ThrowsAsync( + () => Gateway(handler).ResolveZaaktypeUrlAsync("BIG-REGISTRATIE")); + // The error names the resource being queried and includes OpenZaak's body. + Assert.Contains("zaaktypen", ex.Message); + Assert.Contains("boom", ex.Message); + } + + [Fact] + public async Task Resolving_an_informatieobjecttype_surfaces_a_non_success_catalogi_response() + { + var handler = new StubHandler(_ => Task.FromResult(new HttpResponseMessage(HttpStatusCode.InternalServerError) + { + Content = new StringContent("boom"), + })); + + var ex = await Assert.ThrowsAsync( + () => Gateway(handler).ResolveInformatieobjecttypeUrlAsync("Diploma")); + Assert.Contains("informatieobjecttypen", ex.Message); + } + [Fact] public async Task Resolving_an_informatieobjecttype_throws_when_no_omschrijving_matches() { -- 2.54.0 From f363cb1663e2ae09d11e573889aa4b25b95f9e66 Mon Sep 17 00:00:00 2001 From: Niek Otten Date: Wed, 22 Jul 2026 16:10:30 +0200 Subject: [PATCH 5/5] fix(acceptance): implement the resolve API in the in-memory ZGW gateway (refs #113) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The acceptance InMemoryZaakGateway now implements IZaakGateway's new Resolve{Zaaktype,Informatieobjecttype}UrlAsync, and the "een zaak openen" step configures the ACL by identificatie + points the fake's resolved zaaktype at the scenario URL — the Release build (which compiles tests/acceptance) failed without this. All 17 acceptance + 54 ACL unit tests green. Co-Authored-By: Claude Opus 4.8 (1M context) --- tests/acceptance/Steps/EenZaakOpenenSteps.cs | 9 ++++++--- tests/acceptance/Support/InMemoryZaakGateway.cs | 11 +++++++++++ 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/tests/acceptance/Steps/EenZaakOpenenSteps.cs b/tests/acceptance/Steps/EenZaakOpenenSteps.cs index 6dd2fc4..493fb55 100644 --- a/tests/acceptance/Steps/EenZaakOpenenSteps.cs +++ b/tests/acceptance/Steps/EenZaakOpenenSteps.cs @@ -29,9 +29,12 @@ public sealed class EenZaakOpenenSteps Bronorganisatie = values["bronorganisatie"], VerantwoordelijkeOrganisatie = values["verantwoordelijkeOrganisatie"], Vertrouwelijkheidaanduiding = values["vertrouwelijkheidaanduiding"], - ZaaktypeUrl = new Uri(values["zaaktype"]), - InformatieobjecttypeUrl = new Uri("http://openzaak/catalogi/api/v1/informatieobjecttypen/dip"), + ZaaktypeIdentificatie = "BIG-REGISTRATIE", + InformatieobjecttypeOmschrijving = "Diploma", }; + // The ACL resolves the zaaktype by identificatie (S-27); the scenario's zaaktype URL is what + // the catalogus resolves it to, so the created zaak still carries that URL. + _gateway.ResolvedZaaktypeUrl = new Uri(values["zaaktype"]); } [Given("today is \"(.*)\"")] @@ -41,7 +44,7 @@ public sealed class EenZaakOpenenSteps [When("the domain asks the ACL to open a zaak")] public async Task WhenTheDomainAsksTheAclToOpenAZaak() { - var service = new AclService(_gateway, _defaults!, new FixedClock(_today)); + var service = new AclService(_gateway, _defaults!, new CachedZaaktypeCatalog(_gateway, _defaults!), new FixedClock(_today)); _returnedUrl = await service.OpenZaakAsync(_registration!); } diff --git a/tests/acceptance/Support/InMemoryZaakGateway.cs b/tests/acceptance/Support/InMemoryZaakGateway.cs index d16d353..f8fc104 100644 --- a/tests/acceptance/Support/InMemoryZaakGateway.cs +++ b/tests/acceptance/Support/InMemoryZaakGateway.cs @@ -14,6 +14,11 @@ public sealed class InMemoryZaakGateway : IZaakGateway public (Uri Zaak, Uri Zaaktype, DateOnly Datum)? Approved { get; private set; } public (Uri Zaak, Uri Zaaktype, DateOnly Datum)? Cancelled { get; private set; } + // The URLs the catalogus resolves the configured identificatie/omschrijving to (S-27); settable so + // a scenario can pin the zaaktype the ACL should default-fill. + public Uri ResolvedZaaktypeUrl { get; set; } = new("http://openzaak/catalogi/api/v1/zaaktypen/big"); + public Uri ResolvedInformatieobjecttypeUrl { get; set; } = new("http://openzaak/catalogi/api/v1/informatieobjecttypen/dip"); + public Task OpenZaakAsync(ZaakRequest request, CancellationToken ct = default) { Captured = request; @@ -37,4 +42,10 @@ public sealed class InMemoryZaakGateway : IZaakGateway public Task StoreDocumentAsync(DocumentRequest request, CancellationToken ct = default) => Task.FromResult(new Uri("http://openzaak/documenten/api/v1/enkelvoudiginformatieobjecten/acc-doc")); + + public Task ResolveZaaktypeUrlAsync(string identificatie, CancellationToken ct = default) + => Task.FromResult(ResolvedZaaktypeUrl); + + public Task ResolveInformatieobjecttypeUrlAsync(string omschrijving, CancellationToken ct = default) + => Task.FromResult(ResolvedInformatieobjecttypeUrl); } -- 2.54.0