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();
}
/// GETs a non-geo ZGW resource (e.g. a status) by URL — no CRS headers.
public async Task GetJsonAsync(Uri url, CancellationToken ct = default)
{
using var message = new HttpRequestMessage(HttpMethod.Get, url);
message.Headers.Authorization = new AuthenticationHeaderValue("Bearer", MintToken());
using var response = await Http.SendAsync(message, ct);
response.EnsureSuccessStatusCode();
var json = await response.Content.ReadAsStringAsync(ct);
return JsonDocument.Parse(json).RootElement.Clone();
}
/// The zaaktype's eindstatus (terminal statustype) URL — the one an approval sets.
public async Task FindEindstatustypeAsync(Uri zaaktypeUrl, CancellationToken ct = default)
{
var query = new Uri(BaseUrl,
"/catalogi/api/v1/statustypen?status=alles&zaaktype=" + Uri.EscapeDataString(zaaktypeUrl.ToString()));
var page = await GetJsonAsync(query, ct);
var results = page.GetProperty("results");
Uri? fallback = null;
var highest = int.MinValue;
foreach (var st in results.EnumerateArray())
{
if (st.TryGetProperty("isEindstatus", out var eind) && eind.GetBoolean())
return new Uri(st.GetProperty("url").GetString()!);
var volgnummer = st.GetProperty("volgnummer").GetInt32();
if (volgnummer > highest)
{
highest = volgnummer;
fallback = new Uri(st.GetProperty("url").GetString()!);
}
}
return fallback ?? throw new InvalidOperationException($"No statustypen for zaaktype {zaaktypeUrl}");
}
// 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";
}