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,