Files
atomic-design-poc/backend/tests/BigRegister.Tests/ZgwStubHandler.cs
T
ehoandClaude Sonnet 5 3ff80c124f feat(openzaak): bounded retry + flagged write divergence (WP-60)
Local aanvraag/document writes and their paired ZGW writes aren't
transactional; a ZGW failure after the local write succeeds used to
diverge silently. ZgwHttpClient now retries transport-shaped failures
(not 500, which can follow a partial commit on the non-idempotent
statussen/rollen POSTs), and a ZGW failure that survives retry sets
Aanvraag.ZgwError plus a zgw:divergence audit row instead of failing
or diverging quietly. No outbox/reconcile job: three request-triggered
write paths don't justify a persisted queue that would also need to
carry citizen PII for the JWT audit claims.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-30 18:11:55 +02:00

49 lines
2.2 KiB
C#

using System.Net;
using System.Text;
namespace BigRegister.Tests;
/// <summary>
/// Stub HttpMessageHandler shared by the ZGW source tests (no live server, no mocking
/// library) — keyed purely by request URL (method-agnostic, since no test scenario reuses a
/// URL across GET/POST). Records every request's url/body/auth-scheme for assertion.
/// Factored out of OpenZaakZaakSourceTests once OpenZaakDocumentSourceTests needed the
/// identical stub.
///
/// WP-60: an optional <paramref name="status"/> callback lets a test inject a failing status
/// for a given url on a given (0-based) attempt — e.g. "503 on the first call to /zaken, then
/// let it through" — to exercise ZgwHttpClient's retry without a live server. When it returns
/// a non-2xx code, <paramref name="respond"/> is not called for that attempt (so a test that
/// models an "always fails" url never has to also teach `respond` a success body it never
/// reaches).
/// </summary>
internal sealed class ZgwStubHandler(Func<string, string> respond, Func<string, int, HttpStatusCode>? status = null) : HttpMessageHandler
{
public List<string> Requests { get; } = new();
public List<string?> AuthSchemes { get; } = new();
public List<string> Bodies { get; } = new();
public string BodyOf(string url) => Bodies[Requests.LastIndexOf(url)];
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
var url = request.RequestUri!.ToString();
var attempt = Requests.Count(r => r == url);
Requests.Add(url);
AuthSchemes.Add(request.Headers.Authorization?.Scheme);
Bodies.Add(request.Content?.ReadAsStringAsync(cancellationToken).GetAwaiter().GetResult() ?? "");
var code = status?.Invoke(url, attempt) ?? HttpStatusCode.OK;
if (!((int)code >= 200 && (int)code < 300))
return Task.FromResult(new HttpResponseMessage(code)
{
Content = new StringContent("{\"detail\":\"stub failure\"}", Encoding.UTF8, "application/json"),
});
return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)
{
Content = new StringContent(respond(url), Encoding.UTF8, "application/json"),
});
}
}