using Acl.Application; namespace Acl.Tests; 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"); public (Uri Zaak, Uri Zaaktype, DateOnly Datum)? Approved; public Task OpenZaakAsync(ZaakRequest request, CancellationToken ct = default) { Captured = request; return Task.FromResult(Result); } public Task SetZaakToEindstatusAsync(Uri zaakUrl, Uri zaaktypeUrl, DateOnly datumStatusGezet, CancellationToken ct = default) { Approved = (zaakUrl, zaaktypeUrl, datumStatusGezet); return Task.CompletedTask; } public (Uri Zaak, Uri Zaaktype, DateOnly Datum)? Cancelled; public Task SetZaakToCancellationStatusAsync(Uri zaakUrl, Uri zaaktypeUrl, DateOnly datumStatusGezet, CancellationToken ct = default) { Cancelled = (zaakUrl, zaaktypeUrl, datumStatusGezet); return Task.CompletedTask; } public Uri? ReadReferenceFor; public Task GetZaakIdentificatieAsync(Uri zaakUrl, CancellationToken ct = default) { ReadReferenceFor = zaakUrl; return Task.FromResult("REG-FROM-ZAAK"); } public DocumentRequest? StoredDocument; public Uri DocumentResult { get; } = new("http://openzaak/documenten/api/v1/enkelvoudiginformatieobjecten/doc-1"); public Task StoreDocumentAsync(DocumentRequest request, CancellationToken ct = default) { 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); } public IReadOnlyList Zaaktypen { get; } = [ new("BIG-REGISTRATIE", "BIG-registratie", new Uri("http://openzaak/catalogi/api/v1/zaaktypen/big")), ]; public Task> ListZaaktypenAsync(CancellationToken ct = default) => Task.FromResult(Zaaktypen); } private sealed class FakeRegisterRecordGateway : IRegisterRecordGateway { public readonly List Upserted = []; public RegisterRecord? Stored; public Uri? ReadFrom; public Task UpsertAsync(RegisterRecord record, CancellationToken ct = default) { Upserted.Add(record); return Task.CompletedTask; } public Task GetAsync(Uri objectUrl, CancellationToken ct = default) { ReadFrom = objectUrl; return Task.FromResult(Stored); } } private static AclDefaults Defaults() => new() { Bronorganisatie = "517439943", VerantwoordelijkeOrganisatie = "517439943", Vertrouwelijkheidaanduiding = "openbaar", ZaaktypeIdentificatie = "BIG-REGISTRATIE", InformatieobjecttypeOmschrijving = "Diploma", }; private static InMemoryDefaultFillStore FillFrom(AclDefaults d) => new(new DefaultFillSettings(d.Bronorganisatie, d.VerantwoordelijkeOrganisatie, d.Vertrouwelijkheidaanduiding)); private static AclService ServiceWith(FakeGateway gateway, AclDefaults defaults, DateOnly today) => ServiceWith(gateway, new FakeRegisterRecordGateway(), defaults, today); private static AclService ServiceWith(FakeGateway gateway, FakeRegisterRecordGateway register, AclDefaults defaults, DateOnly today) => new(gateway, register, FillFrom(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_uses_the_resolved_zaaktype() { var gateway = new FakeGateway(); var service = ServiceWith(gateway, Defaults(), new DateOnly(2026, 6, 4)); var url = await service.OpenZaakAsync(new DomainRegistration("123456782", "reg-77")); Assert.Equal(gateway.Result, url); var req = Assert.IsType(gateway.Captured); Assert.Equal("517439943", req.Bronorganisatie); Assert.Equal("517439943", req.VerantwoordelijkeOrganisatie); Assert.Equal("openbaar", req.Vertrouwelijkheidaanduiding); // 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); } [Fact] public async Task Opening_a_zaak_also_writes_an_ingediend_register_record(/* S-19b-2 */) { var gateway = new FakeGateway(); var register = new FakeRegisterRecordGateway(); var service = ServiceWith(gateway, register, Defaults(), new DateOnly(2026, 6, 4)); await service.OpenZaakAsync(new DomainRegistration("123456782", "reg-77")); // The register — not ZGW — is what the read projection is sourced from (ADR-0028), so a // submitted registration has to exist there the moment the zaak is opened, not only on // approval. Approval upserts this same record to INGESCHREVEN. var record = Assert.Single(register.Upserted); Assert.Equal("abc", record.Id); Assert.Equal("INGEDIEND", record.Status); // The reference comes from the registration itself — no ZGW read-back needed on this path. Assert.Equal("reg-77", record.Reference); } [Fact] public async Task Reading_a_register_record_goes_through_the_objecten_gateway(/* S-19b-2 */) { var gateway = new FakeGateway(); var register = new FakeRegisterRecordGateway { Stored = new RegisterRecord("abc", "INGESCHREVEN", "reg-77") }; var service = ServiceWith(gateway, register, Defaults(), new DateOnly(2026, 6, 4)); var objectUrl = new Uri("http://objecten.local:8000/api/v2/objects/9de4a2ca"); var record = await service.GetRegisterRecordAsync(objectUrl); Assert.Equal(objectUrl, register.ReadFrom); Assert.Equal("abc", record!.Id); Assert.Equal("INGESCHREVEN", record.Status); Assert.Equal("reg-77", record.Reference); } [Fact] public async Task Reading_a_register_record_from_a_null_url_is_rejected(/* S-19b-2 */) { var gateway = new FakeGateway(); var register = new FakeRegisterRecordGateway(); var service = ServiceWith(gateway, register, Defaults(), new DateOnly(2026, 6, 4)); await Assert.ThrowsAsync(() => service.GetRegisterRecordAsync(null!)); Assert.Null(register.ReadFrom); } [Fact] public async Task Opening_a_zaak_reflects_a_default_fill_update(/* S-15b */) { var gateway = new FakeGateway(); var service = ServiceWith(gateway, Defaults(), new DateOnly(2026, 6, 4)); // A beheerder edits the default-fill; the very next zaak must use the new values (read per zaak). service.UpdateDefaultFill(new DefaultFillSettings("999999999", "888888888", "vertrouwelijk")); await service.OpenZaakAsync(new DomainRegistration("123456782", "reg-1")); var req = gateway.Captured!; Assert.Equal("999999999", req.Bronorganisatie); Assert.Equal("888888888", req.VerantwoordelijkeOrganisatie); Assert.Equal("vertrouwelijk", req.Vertrouwelijkheidaanduiding); } [Fact] public async Task Rejects_a_null_registration_without_calling_the_gateway() { var gateway = new FakeGateway(); 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_resolved_zaaktypes_eindstatus_dated_today() { var gateway = new FakeGateway(); 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(gateway.ResolvedZaaktype, gateway.Approved.Value.Zaaktype); Assert.Equal(new DateOnly(2026, 6, 4), gateway.Approved.Value.Datum); } [Fact] public async Task Approving_a_null_zaak_is_rejected_without_touching_the_gateway() { var gateway = new FakeGateway(); var register = new FakeRegisterRecordGateway(); var service = ServiceWith(gateway, register, Defaults(), new DateOnly(2026, 6, 4)); await Assert.ThrowsAsync(() => service.ApproveZaakAsync(null!)); Assert.Null(gateway.Approved); Assert.Empty(register.Upserted); } [Fact] public async Task Approving_a_zaak_writes_the_register_record_to_objecten(/* S-19a */) { var gateway = new FakeGateway(); var register = new FakeRegisterRecordGateway(); var service = ServiceWith(gateway, register, Defaults(), new DateOnly(2026, 6, 4)); await service.ApproveZaakAsync(new Uri("http://openzaak/zaken/api/v1/zaken/abc")); var record = Assert.Single(register.Upserted); // The record is keyed on the zaak id — the same key the read projection rows carry (S-19b). Assert.Equal("abc", record.Id); Assert.Equal("INGESCHREVEN", record.Status); // The public-safe reference comes from the zaak's identificatie, never from the domain payload. Assert.Equal("REG-FROM-ZAAK", record.Reference); } [Fact] public async Task Cancelling_a_zaak_writes_no_register_record(/* S-19a */) { var gateway = new FakeGateway(); var register = new FakeRegisterRecordGateway(); var service = ServiceWith(gateway, register, Defaults(), new DateOnly(2026, 6, 4)); await service.CancelZaakAsync(new Uri("http://openzaak/zaken/api/v1/zaken/abc")); // Only an approval enters the register; a cancelled zaak never becomes a register record. Assert.Empty(register.Upserted); } [Fact] public async Task Cancelling_a_zaak_sets_it_to_the_cancellation_status_dated_today() { var gateway = new FakeGateway(); 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(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); } [Fact] public async Task Cancelling_a_null_zaak_is_rejected_without_touching_the_gateway() { var gateway = new FakeGateway(); 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_uses_the_resolved_informatieobjecttype() { var gateway = new FakeGateway(); 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"); Assert.Equal(gateway.DocumentResult, url); var req = gateway.StoredDocument!; Assert.Equal(zaak, req.Zaak); // 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); Assert.Equal("nld", req.Taal); Assert.Equal("diploma.pdf", req.Bestandsnaam); Assert.Equal("application/pdf", req.Formaat); Assert.Equal(new byte[] { 1, 2, 3 }, req.Inhoud); } [Fact] public async Task Storing_a_diploma_rejects_null_or_blank_arguments() { 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")); await Assert.ThrowsAsync(() => service.StoreDiplomaAsync(zaak, null!, "d.pdf", "application/pdf")); await Assert.ThrowsAnyAsync(() => service.StoreDiplomaAsync(zaak, [1], " ", "application/pdf")); await Assert.ThrowsAnyAsync(() => service.StoreDiplomaAsync(zaak, [1], "d.pdf", " ")); } [Fact] public async Task Reading_a_zaak_reference_returns_the_zaaks_identificatie() { var gateway = new FakeGateway(); 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); Assert.Equal("REG-FROM-ZAAK", reference); Assert.Equal(zaak, gateway.ReadReferenceFor); } [Fact] public async Task Reading_a_null_zaak_reference_is_rejected() { var gateway = new FakeGateway(); var service = ServiceWith(gateway, Defaults(), new DateOnly(2026, 6, 4)); await Assert.ThrowsAsync(() => service.GetZaakReferenceAsync(null!)); Assert.Null(gateway.ReadReferenceFor); } [Fact] public async Task Listing_zaaktypen_returns_the_gateways_published_zaaktypen(/* S-15a */) { var gateway = new FakeGateway(); var service = ServiceWith(gateway, Defaults(), new DateOnly(2026, 6, 4)); var zaaktypen = await service.ListZaaktypenAsync(); var only = Assert.Single(zaaktypen); Assert.Equal("BIG-REGISTRATIE", only.Identificatie); Assert.Equal("BIG-registratie", only.Omschrijving); Assert.Equal(new Uri("http://openzaak/catalogi/api/v1/zaaktypen/big"), only.Url); } }