using System.Net; using System.Net.Http.Json; using System.Text; using System.Text.Json; using Acl.Application; using Acl.Infrastructure; namespace Acl.Tests; public class OpenZaakGatewayTests { private sealed class StubHandler(Func> onSend) : HttpMessageHandler { protected override Task SendAsync(HttpRequestMessage request, CancellationToken ct) => onSend(request); } private static OpenZaakGateway Gateway(StubHandler handler) => new( new HttpClient(handler), new OpenZaakOptions { BaseUrl = new("http://openzaak"), ClientId = "cid", Secret = "sec" }); private static ZaakRequest SampleRequest() => new( "517439943", "517439943", "openbaar", new("http://openzaak/catalogi/api/v1/zaaktypen/big"), new DateOnly(2026, 6, 4)); private static StubHandler Created(out RequestCapture capture) { var c = new RequestCapture(); capture = c; return new StubHandler(async req => { c.Seen = req; c.Body = req.Content is null ? null : await req.Content.ReadAsStringAsync(); return new HttpResponseMessage(HttpStatusCode.Created) { Content = JsonContent.Create(new { url = "http://openzaak/zaken/api/v1/zaken/xyz" }), }; }); } private sealed class RequestCapture { public HttpRequestMessage? Seen; public string? Body; } [Fact] public async Task Posts_zaak_to_openzaak_with_bearer_and_default_fields_and_returns_url() { var handler = Created(out var capture); var url = await Gateway(handler).OpenZaakAsync(SampleRequest()); Assert.Equal("http://openzaak/zaken/api/v1/zaken/xyz", url.ToString()); Assert.Equal(HttpMethod.Post, capture.Seen!.Method); Assert.Equal("http://openzaak/zaken/api/v1/zaken", capture.Seen.RequestUri!.ToString()); Assert.Equal("Bearer", capture.Seen.Headers.Authorization!.Scheme); Assert.False(string.IsNullOrWhiteSpace(capture.Seen.Headers.Authorization.Parameter)); Assert.Contains("\"bronorganisatie\":\"517439943\"", capture.Body); Assert.Contains("\"verantwoordelijkeOrganisatie\":\"517439943\"", capture.Body); Assert.Contains("\"vertrouwelijkheidaanduiding\":\"openbaar\"", capture.Body); Assert.Contains("\"startdatum\":\"2026-06-04\"", capture.Body); Assert.Contains("\"zaaktype\":\"http://openzaak/catalogi/api/v1/zaaktypen/big\"", capture.Body); } [Fact] public async Task Sends_the_geo_crs_headers_required_by_the_zaken_api() { var handler = Created(out var capture); await Gateway(handler).OpenZaakAsync(SampleRequest()); Assert.Equal("EPSG:4326", Assert.Single(capture.Seen!.Headers.GetValues("Accept-Crs"))); Assert.Equal("EPSG:4326", Assert.Single(capture.Seen.Content!.Headers.GetValues("Content-Crs"))); } [Fact] public async Task Mints_a_hs256_jwt_carrying_the_acl_identity_claims() { var handler = Created(out var capture); await Gateway(handler).OpenZaakAsync(SampleRequest()); var parts = capture.Seen!.Headers.Authorization!.Parameter!.Split('.'); Assert.Equal(3, parts.Length); using var header = JsonDocument.Parse(DecodeSegment(parts[0])); Assert.Equal("HS256", header.RootElement.GetProperty("alg").GetString()); Assert.Equal("JWT", header.RootElement.GetProperty("typ").GetString()); using var payload = JsonDocument.Parse(DecodeSegment(parts[1])); Assert.Equal("cid", payload.RootElement.GetProperty("client_id").GetString()); Assert.Equal("acl", payload.RootElement.GetProperty("user_id").GetString()); Assert.Equal("acl", payload.RootElement.GetProperty("user_representation").GetString()); } [Fact] public async Task Throws_when_openzaak_rejects_the_request() { var handler = new StubHandler(_ => Task.FromResult(new HttpResponseMessage(HttpStatusCode.BadRequest))); await Assert.ThrowsAsync( () => Gateway(handler).OpenZaakAsync(SampleRequest())); } [Fact] public async Task Throws_when_openzaak_returns_an_empty_body() { var handler = new StubHandler(_ => Task.FromResult(new HttpResponseMessage(HttpStatusCode.Created) { Content = new StringContent("null", Encoding.UTF8, "application/json"), })); await Assert.ThrowsAsync( () => Gateway(handler).OpenZaakAsync(SampleRequest())); } [Fact] public async Task Rejects_a_null_request() { var handler = new StubHandler(_ => throw new InvalidOperationException("should not be sent")); await Assert.ThrowsAsync( () => Gateway(handler).OpenZaakAsync(null!)); } // ZGW tokens are base64url with padding stripped (ZgwToken.B64Url); restore it to decode. private static string DecodeSegment(string segment) { var b64 = segment.Replace('-', '+').Replace('_', '/'); b64 = (b64.Length % 4) switch { 2 => b64 + "==", 3 => b64 + "=", _ => b64 }; return Encoding.UTF8.GetString(Convert.FromBase64String(b64)); } }