using System.Net; using System.Net.Http.Json; using BigRegister.Api.Contracts; using BigRegister.Api.Data; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc.Testing; using Microsoft.Extensions.DependencyInjection; namespace BigRegister.Tests; /// /// WP-60's required verification: a ZGW failure mid-submit must not leave the two write sides /// silently diverged — it's flagged (, an audit row) instead. /// Not an off : that /// fixture hardcodes Zgw:Enabled=false, so this builds its own factory the same way /// does, but with a stub primary handler /// () instead of a live OpenZaak. /// public class ZgwDivergenceTests { private const string ZrcBase = "https://oz.example/zaken/api/v1"; private const string ZtBase = "https://oz.example/catalogi/api/v1"; private const string ZaaktypeUrl = $"{ZtBase}/zaaktypen/zt-1"; private static WebApplicationFactory Factory(ZgwStubHandler stub) { var dbPath = Path.Combine(Path.GetTempPath(), $"bigregister-zgw-divergence-{Guid.NewGuid():N}.db"); return new WebApplicationFactory().WithWebHostBuilder(builder => builder .UseSetting("ConnectionStrings:AppDb", $"Data Source={dbPath}") .UseSetting("Zgw:Enabled", "true") .UseSetting("Zgw:ZrcBaseUrl", ZrcBase) .UseSetting("Zgw:ZtcBaseUrl", ZtBase) .UseSetting("Zgw:ClientId", "c") .UseSetting("Zgw:Secret", "s") .UseSetting("Zgw:Bronorganisatie", "123443210") .UseSetting("Zgw:VerantwoordelijkeOrganisatie", "123443210") .UseSetting("Zgw:ZaaktypeUrls:registratie", ZaaktypeUrl) .ConfigureServices(services => services.ConfigureHttpClientDefaults(b => b.ConfigurePrimaryHttpMessageHandler(() => stub)))); } /// Doesn't call GET /applications first (unlike ApplicationTests.Create) — under /// Zgw:Enabled=true that route goes through IZaakSource too, which this test's stub doesn't /// need to answer since every test here uses a fresh db and creates exactly one aanvraag. private static async Task CreateConcept(HttpClient client, string type = "registratie") { var res = await client.PostAsJsonAsync("/api/v1/applications", new { type }); res.EnsureSuccessStatusCode(); var body = (await res.Content.ReadFromJsonAsync())!; return body.Id; } private static string SuccessBody(string url) => url switch { _ when url == $"{ZrcBase}/zaken" => $$""" { "url": "{{ZrcBase}}/zaken/uuid-new", "identificatie": "BIG-2026-000123", "zaaktype": "{{ZaaktypeUrl}}", "startdatum": "2026-07-30", "einddatum": null, "registratiedatum": "2026-07-30" } """, _ when url.StartsWith($"{ZtBase}/statustypen") => """ { "count": 1, "next": null, "results": [ { "url": "https://oz.example/catalogi/api/v1/statustypen/st-1", "volgnummer": 1 } ] } """, _ when url.StartsWith($"{ZtBase}/roltypen") => """ { "count": 1, "next": null, "results": [ { "url": "https://oz.example/catalogi/api/v1/roltypen/rt-initiator" } ] } """, _ when url == $"{ZrcBase}/statussen" => "{}", _ when url == $"{ZrcBase}/rollen" => "{}", _ => throw new InvalidOperationException($"unexpected ZGW call {url}"), }; [Fact] public async Task Submit_with_a_failing_zgw_flags_the_divergence_instead_of_diverging_silently() { var stub = new ZgwStubHandler(SuccessBody, (url, _) => url == $"{ZrcBase}/zaken" ? HttpStatusCode.ServiceUnavailable : HttpStatusCode.OK); using var factory = Factory(stub); using var client = factory.CreateClient(); var id = await CreateConcept(client); var res = await client.PostAsJsonAsync($"/api/v1/applications/{id}/submit", new { diplomaHerkomst = "duo" }); // The local write is still authoritative: 200 with a real reference, not a 500. res.EnsureSuccessStatusCode(); var body = (await res.Content.ReadFromJsonAsync())!; Assert.NotEmpty(body.Referentie); var stored = ApplicationStore.ListAll().Single(a => a.Id == id); Assert.Null(stored.ZaakUrl); Assert.NotNull(stored.ZgwError); var audit = await client.SendAsync(AdminRequest(HttpMethod.Get, "/api/v1/admin/audit")); audit.EnsureSuccessStatusCode(); var entries = (await audit.Content.ReadFromJsonAsync>())!; Assert.Contains(entries, e => e.Action == "zgw:divergence" && e.Decision == "deny" && e.Resource == body.Referentie); } [Fact] public async Task Submit_with_a_healthy_zgw_leaves_no_divergence_flag() { var stub = new ZgwStubHandler(SuccessBody); using var factory = Factory(stub); using var client = factory.CreateClient(); var id = await CreateConcept(client); var res = await client.PostAsJsonAsync($"/api/v1/applications/{id}/submit", new { diplomaHerkomst = "duo" }); res.EnsureSuccessStatusCode(); var stored = ApplicationStore.ListAll().Single(a => a.Id == id); Assert.Equal($"{ZrcBase}/zaken/uuid-new", stored.ZaakUrl); Assert.Null(stored.ZgwError); } private static HttpRequestMessage AdminRequest(HttpMethod method, string path) { var req = new HttpRequestMessage(method, path); req.Headers.Add("X-Role", "admin"); return req; } }