diff --git a/Makefile b/Makefile
index 50a6049..21ee91b 100644
--- a/Makefile
+++ b/Makefile
@@ -43,7 +43,7 @@ export DOCKER_HOST := unix://$(PODMAN_SOCK)
endif
endif
-.PHONY: ci lint build unit mutation smoke up down local local-down changelog openzaak-up openzaak-smoke openzaak-seed openzaak-down stack-up stack-smoke stack-down keycloak-up keycloak-smoke keycloak-down flowable-up flowable-smoke flowable-down help
+.PHONY: ci lint build unit mutation integration smoke up down local local-down changelog openzaak-up openzaak-smoke openzaak-seed openzaak-down stack-up stack-smoke stack-down keycloak-up keycloak-smoke keycloak-down flowable-up flowable-smoke flowable-down help
## ci: run the full pipeline — lint, build, unit, mutation, smoke (mirrors Gitea Actions)
ci: lint build unit mutation smoke
@@ -56,9 +56,9 @@ lint:
build:
dotnet build $(SLN) -c Release
-## unit: run unit tests
+## unit: run unit tests (excludes the container-backed Integration lane)
unit:
- dotnet test $(SLN) -c Release
+ dotnet test $(SLN) -c Release --filter "Category!=Integration"
## mutation: run the Stryker.NET ratchet on the ACL (fails below the recorded baseline)
# Stryker is pinned as a local dotnet tool (.config/dotnet-tools.json); `tool restore`
@@ -106,6 +106,21 @@ local-down:
changelog:
git-cliff --output CHANGELOG.md
+## integration: ACL integration tests against a real OpenZaak (S-04a, #46). Brings
+## the stack up, seeds a PUBLISHED BIG zaaktype (OZ_PUBLISH=1, so OpenZaak accepts a
+## real zaak POST), runs the Integration-category tests, then always tears down.
+## Kept out of `unit`/`mutation` because it needs the live stack. See ADR-0006.
+integration: openzaak-up
+ @bash -c 'set -e; \
+ for i in $$(seq 1 60); do \
+ c=$$(curl -s -o /dev/null -w "%{http_code}" $(OZ_BASE)/catalogi/api/v1/ || true); \
+ [ "$$c" = "200" ] && break; sleep 3; done; echo "OpenZaak ready ($$c)"; \
+ OZ_PUBLISH=1 python3 infra/openzaak/seed_catalogus.py; \
+ rc=0; dotnet test $(SLN) -c Release --filter "Category=Integration" || rc=$$?; \
+ docker compose -f $(OZ_COMPOSE) down --volumes >/dev/null 2>&1 || true; \
+ docker volume rm -f rr-oz-config >/dev/null 2>&1 || true; \
+ exit $$rc'
+
## openzaak-up: start the OpenZaak stack (migrations run on first start)
openzaak-up:
$(SEED) oz
diff --git a/register-referentie.slnx b/register-referentie.slnx
index fe5cf56..f5b32ab 100644
--- a/register-referentie.slnx
+++ b/register-referentie.slnx
@@ -4,6 +4,7 @@
+
diff --git a/services/acl/Acl.IntegrationTests/Acl.IntegrationTests.csproj b/services/acl/Acl.IntegrationTests/Acl.IntegrationTests.csproj
new file mode 100644
index 0000000..fff0e46
--- /dev/null
+++ b/services/acl/Acl.IntegrationTests/Acl.IntegrationTests.csproj
@@ -0,0 +1,30 @@
+
+
+
+
+ net10.0
+ enable
+ enable
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/services/acl/Acl.IntegrationTests/OpenZaakFixture.cs b/services/acl/Acl.IntegrationTests/OpenZaakFixture.cs
new file mode 100644
index 0000000..bcdfdcc
--- /dev/null
+++ b/services/acl/Acl.IntegrationTests/OpenZaakFixture.cs
@@ -0,0 +1,97 @@
+using System.Net.Http.Headers;
+using System.Security.Cryptography;
+using System.Text;
+using System.Text.Json;
+using Acl.Infrastructure;
+
+namespace Acl.IntegrationTests;
+
+///
+/// Shared connection to the running OpenZaak compose stack (ADR-0006). Reads the
+/// same endpoint + JWT-client config the seed uses, and locates the published
+/// BIG-REGISTRATIE zaaktype the ACL opens zaken against. Defaults match
+/// `infra/openzaak/seed_catalogus.py`; override via OZ_BASE / OZ_CLIENT_ID / OZ_SECRET.
+///
+public sealed class OpenZaakFixture : IDisposable
+{
+ private static string Env(string key, string fallback) =>
+ Environment.GetEnvironmentVariable(key) is { Length: > 0 } v ? v : fallback;
+
+ public Uri BaseUrl { get; } = new(Env("OZ_BASE", "http://localhost:8000"));
+ public string ClientId { get; } = Env("OZ_CLIENT_ID", "big-reference-seed");
+ public string Secret { get; } = Env("OZ_SECRET", "insecure-dev-secret-change-me");
+
+ public HttpClient Http { get; } = new();
+
+ public OpenZaakOptions Options => new()
+ {
+ BaseUrl = BaseUrl,
+ ClientId = ClientId,
+ Secret = Secret,
+ };
+
+ ///
+ /// The URL of the published BIG-REGISTRATIE zaaktype, or null when none is
+ /// published yet (a concept-only stack). `status=definitief` returns published
+ /// zaaktypen only — a concept zaaktype is deliberately excluded.
+ ///
+ public async Task FindPublishedBigZaaktypeAsync(CancellationToken ct = default)
+ {
+ var query = new Uri(BaseUrl,
+ "/catalogi/api/v1/zaaktypen?identificatie=BIG-REGISTRATIE&status=definitief");
+ using var message = new HttpRequestMessage(HttpMethod.Get, query);
+ message.Headers.Authorization = new AuthenticationHeaderValue("Bearer", MintToken());
+
+ using var response = await Http.SendAsync(message, ct);
+ response.EnsureSuccessStatusCode();
+
+ using var document = JsonDocument.Parse(await response.Content.ReadAsStringAsync(ct));
+ var results = document.RootElement.GetProperty("results");
+ return results.GetArrayLength() == 0
+ ? null
+ : new Uri(results[0].GetProperty("url").GetString()!);
+ }
+
+ /// GETs a previously-created zaak to prove it was really persisted.
+ public async Task GetZaakAsync(Uri zaakUrl, CancellationToken ct = default)
+ {
+ using var message = new HttpRequestMessage(HttpMethod.Get, zaakUrl);
+ message.Headers.Authorization = new AuthenticationHeaderValue("Bearer", MintToken());
+ message.Headers.Add("Accept-Crs", "EPSG:4326");
+
+ using var response = await Http.SendAsync(message, ct);
+ response.EnsureSuccessStatusCode();
+ var json = await response.Content.ReadAsStringAsync(ct);
+ return JsonDocument.Parse(json).RootElement.Clone();
+ }
+
+ // A ZGW (vng-api-common) HS256 JWT, mirroring the seed's client. Minted here
+ // rather than reusing Acl.Infrastructure's internal minter to keep that internal.
+ private string MintToken()
+ {
+ static string B64(byte[] b) =>
+ Convert.ToBase64String(b).TrimEnd('=').Replace('+', '-').Replace('/', '_');
+
+ var header = B64(JsonSerializer.SerializeToUtf8Bytes(new { alg = "HS256", typ = "JWT" }));
+ var payload = B64(JsonSerializer.SerializeToUtf8Bytes(new
+ {
+ iss = ClientId,
+ iat = DateTimeOffset.UtcNow.ToUnixTimeSeconds(),
+ client_id = ClientId,
+ user_id = "acl-integration-test",
+ user_representation = "acl-integration-test",
+ }));
+ var signingInput = $"{header}.{payload}";
+ using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(Secret));
+ var signature = B64(hmac.ComputeHash(Encoding.UTF8.GetBytes(signingInput)));
+ return $"{signingInput}.{signature}";
+ }
+
+ public void Dispose() => Http.Dispose();
+}
+
+[CollectionDefinition(Name)]
+public sealed class OpenZaakCollection : ICollectionFixture
+{
+ public const string Name = "OpenZaak";
+}
diff --git a/services/acl/Acl.IntegrationTests/OpenZaakGatewayIntegrationTests.cs b/services/acl/Acl.IntegrationTests/OpenZaakGatewayIntegrationTests.cs
new file mode 100644
index 0000000..16f2b7e
--- /dev/null
+++ b/services/acl/Acl.IntegrationTests/OpenZaakGatewayIntegrationTests.cs
@@ -0,0 +1,45 @@
+using Acl.Application;
+using Acl.Infrastructure;
+
+namespace Acl.IntegrationTests;
+
+///
+/// S-04a (#46): the deferred S-04 acceptance criterion — the ACL's OpenZaakGateway
+/// opening a zaak against a *real* OpenZaak, exercising real ZGW JWT auth and the
+/// real POST /zaken/api/v1/zaken contract (CRS headers, default-fill, the created
+/// zaak URL) that the stubbed-HttpMessageHandler unit tests cannot. See ADR-0006.
+///
+[Trait("Category", "Integration")]
+[Collection(OpenZaakCollection.Name)]
+public sealed class OpenZaakGatewayIntegrationTests(OpenZaakFixture stack)
+{
+ [Fact]
+ public async Task Opens_a_real_zaak_against_the_published_big_zaaktype_and_returns_its_url()
+ {
+ var zaaktype = await stack.FindPublishedBigZaaktypeAsync();
+ Assert.True(zaaktype is not null,
+ "No published BIG-REGISTRATIE zaaktype found in OpenZaak — bring the stack up and " +
+ "seed it with OZ_PUBLISH=1 (`make integration` does this).");
+
+ var gateway = new OpenZaakGateway(stack.Http, stack.Options);
+ var request = new ZaakRequest(
+ Bronorganisatie: "517439943",
+ VerantwoordelijkeOrganisatie: "517439943",
+ Vertrouwelijkheidaanduiding: "openbaar",
+ Zaaktype: zaaktype!,
+ Startdatum: DateOnly.FromDateTime(DateTime.UtcNow));
+
+ var zaakUrl = await gateway.OpenZaakAsync(request);
+
+ // The gateway returns the canonical zaak URL on OpenZaak's Zaken API...
+ Assert.StartsWith(
+ new Uri(stack.BaseUrl, "/zaken/api/v1/zaken/").ToString(),
+ zaakUrl.ToString());
+
+ // ...and that zaak is really persisted with the default-filled fields.
+ var zaak = await stack.GetZaakAsync(zaakUrl);
+ Assert.Equal(zaaktype.ToString(), zaak.GetProperty("zaaktype").GetString());
+ Assert.Equal("517439943", zaak.GetProperty("bronorganisatie").GetString());
+ Assert.Equal("openbaar", zaak.GetProperty("vertrouwelijkheidaanduiding").GetString());
+ }
+}
diff --git a/services/acl/Acl.slnx b/services/acl/Acl.slnx
index d33e693..aa02b3b 100644
--- a/services/acl/Acl.slnx
+++ b/services/acl/Acl.slnx
@@ -2,5 +2,6 @@
+
diff --git a/services/acl/stryker-config.json b/services/acl/stryker-config.json
index 4321e06..0412cc4 100644
--- a/services/acl/stryker-config.json
+++ b/services/acl/stryker-config.json
@@ -1,6 +1,7 @@
{
"stryker-config": {
"solution": "Acl.slnx",
+ "test-projects": ["Acl.Tests/Acl.Tests.csproj"],
"reporters": ["progress", "html"],
"thresholds": {
"high": 95,