The gateway resolves the zaaktype's eindstatus from the catalogus (isEindstatus, falling back to the highest volgnummer) and POSTs a status against the zaak. Exposed as POST /statussen for the domain's approve use case. Adds an integration test that sets the eindstatus against a real OpenZaak and verifies the zaak's current status. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
135 lines
5.7 KiB
C#
135 lines
5.7 KiB
C#
using System.Net.Http.Headers;
|
|
using System.Security.Cryptography;
|
|
using System.Text;
|
|
using System.Text.Json;
|
|
using Acl.Infrastructure;
|
|
|
|
namespace Acl.IntegrationTests;
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
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,
|
|
};
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
public async Task<Uri?> 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()!);
|
|
}
|
|
|
|
/// <summary>GETs a previously-created zaak to prove it was really persisted.</summary>
|
|
public async Task<JsonElement> 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();
|
|
}
|
|
|
|
/// <summary>GETs a non-geo ZGW resource (e.g. a status) by URL — no CRS headers.</summary>
|
|
public async Task<JsonElement> 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();
|
|
}
|
|
|
|
/// <summary>The zaaktype's eindstatus (terminal statustype) URL — the one an approval sets.</summary>
|
|
public async Task<Uri> 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<OpenZaakFixture>
|
|
{
|
|
public const string Name = "OpenZaak";
|
|
}
|