test(acl,domain): cover the approval-flow mutants to hold the ratchet (refs #75)
Some checks failed
CI / lint (pull_request) Successful in 1m11s
CI / build (pull_request) Successful in 1m6s
CI / unit (pull_request) Successful in 1m23s
CI / frontend (pull_request) Successful in 2m45s
CI / mutation (pull_request) Successful in 6m3s
CI / verify-stack (pull_request) Failing after 13m34s
Some checks failed
CI / lint (pull_request) Successful in 1m11s
CI / build (pull_request) Successful in 1m6s
CI / unit (pull_request) Successful in 1m23s
CI / frontend (pull_request) Successful in 2m45s
CI / mutation (pull_request) Successful in 6m3s
CI / verify-stack (pull_request) Failing after 13m34s
The new gateway/use-case code left surviving mutants (empty NoCoverage on the ACL gateway status-set, plus unasserted error messages, null-guards, and the persist call). Add stub-handler tests for SetZaakToEindstatusAsync (eindstatus resolution, POST framing, failure paths) and assert the approve use case's messages, null handling and SaveAsync. Local Stryker: acl 100%, domain 98.4%, event-subscriber 100%. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -131,8 +131,9 @@ public class OpenZaakGatewayTests
|
|||||||
Content = new StringContent("null", Encoding.UTF8, "application/json"),
|
Content = new StringContent("null", Encoding.UTF8, "application/json"),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
await Assert.ThrowsAsync<InvalidOperationException>(
|
var ex = await Assert.ThrowsAsync<InvalidOperationException>(
|
||||||
() => Gateway(handler).OpenZaakAsync(SampleRequest()));
|
() => Gateway(handler).OpenZaakAsync(SampleRequest()));
|
||||||
|
Assert.Contains("empty zaak response", ex.Message);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -144,6 +145,153 @@ public class OpenZaakGatewayTests
|
|||||||
() => Gateway(handler).OpenZaakAsync(null!));
|
() => Gateway(handler).OpenZaakAsync(null!));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- SetZaakToEindstatusAsync (approval / S-09b) ---
|
||||||
|
|
||||||
|
private const string ZaakUrl = "http://openzaak/zaken/api/v1/zaken/xyz";
|
||||||
|
private static readonly Uri Zaaktype = new("http://openzaak/catalogi/api/v1/zaaktypen/big");
|
||||||
|
|
||||||
|
private sealed class Recorder
|
||||||
|
{
|
||||||
|
public List<HttpRequestMessage> Requests { get; } = [];
|
||||||
|
public List<string?> Bodies { get; } = [];
|
||||||
|
public List<long?> ContentLengths { get; } = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Routes the two calls the approval makes: GET statustypen (returns the given page) then POST statussen.
|
||||||
|
private static StubHandler ApprovalStub(Recorder rec, string statustypenJson,
|
||||||
|
HttpStatusCode getStatus = HttpStatusCode.OK, HttpStatusCode postStatus = HttpStatusCode.Created)
|
||||||
|
=> new(async req =>
|
||||||
|
{
|
||||||
|
rec.Requests.Add(req);
|
||||||
|
// Capture the length BEFORE reading the body (ReadAsStringAsync buffers as a side effect,
|
||||||
|
// which would mask whether the gateway buffered it itself — mirrors the zaak-create tests).
|
||||||
|
rec.ContentLengths.Add(req.Content?.Headers.ContentLength);
|
||||||
|
rec.Bodies.Add(req.Content is null ? null : await req.Content.ReadAsStringAsync());
|
||||||
|
if (req.Method == HttpMethod.Get)
|
||||||
|
return new HttpResponseMessage(getStatus)
|
||||||
|
{
|
||||||
|
Content = new StringContent(statustypenJson, Encoding.UTF8, "application/json"),
|
||||||
|
};
|
||||||
|
return new HttpResponseMessage(postStatus)
|
||||||
|
{
|
||||||
|
Content = JsonContent.Create(new { url = "http://openzaak/zaken/api/v1/statussen/new" }),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// Two statustypen; the eindstatus is flagged on the *lower* volgnummer so the tests prove the
|
||||||
|
// isEindstatus flag is preferred over "highest volgnummer", not coincidentally equal to it.
|
||||||
|
private static string StatustypenPage(bool withEindstatusFlag) => JsonSerializer.Serialize(new
|
||||||
|
{
|
||||||
|
results = new object[]
|
||||||
|
{
|
||||||
|
new { url = "http://openzaak/catalogi/api/v1/statustypen/1", volgnummer = 1, isEindstatus = withEindstatusFlag },
|
||||||
|
new { url = "http://openzaak/catalogi/api/v1/statustypen/2", volgnummer = 2, isEindstatus = false },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Approving_queries_statustypen_then_posts_the_flagged_eindstatus_against_the_zaak()
|
||||||
|
{
|
||||||
|
var rec = new Recorder();
|
||||||
|
|
||||||
|
await Gateway(ApprovalStub(rec, StatustypenPage(withEindstatusFlag: true)))
|
||||||
|
.SetZaakToEindstatusAsync(new Uri(ZaakUrl), Zaaktype, new DateOnly(2026, 6, 4));
|
||||||
|
|
||||||
|
Assert.Equal(2, rec.Requests.Count);
|
||||||
|
|
||||||
|
var get = rec.Requests[0];
|
||||||
|
Assert.Equal(HttpMethod.Get, get.Method);
|
||||||
|
Assert.StartsWith("http://openzaak/catalogi/api/v1/statustypen", get.RequestUri!.ToString());
|
||||||
|
Assert.Contains(Uri.EscapeDataString(Zaaktype.ToString()), get.RequestUri.ToString());
|
||||||
|
Assert.Equal("Bearer", get.Headers.Authorization!.Scheme);
|
||||||
|
|
||||||
|
var post = rec.Requests[1];
|
||||||
|
Assert.Equal(HttpMethod.Post, post.Method);
|
||||||
|
Assert.Equal("http://openzaak/zaken/api/v1/statussen", post.RequestUri!.ToString());
|
||||||
|
Assert.Equal("Bearer", post.Headers.Authorization!.Scheme);
|
||||||
|
// The isEindstatus-flagged statustype (/1) is chosen — even though /2 has a higher volgnummer.
|
||||||
|
Assert.Contains("\"zaak\":\"" + ZaakUrl + "\"", rec.Bodies[1]);
|
||||||
|
Assert.Contains("\"statustype\":\"http://openzaak/catalogi/api/v1/statustypen/1\"", rec.Bodies[1]);
|
||||||
|
Assert.Contains("\"datumStatusGezet\":\"2026-06-04T00:00:00Z\"", rec.Bodies[1]);
|
||||||
|
// The POST body is buffered (Content-Length set), so uwsgi doesn't get a chunked body.
|
||||||
|
Assert.NotNull(rec.ContentLengths[1]);
|
||||||
|
Assert.True(rec.ContentLengths[1] > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Approving_falls_back_to_the_highest_volgnummer_when_no_eindstatus_is_flagged()
|
||||||
|
{
|
||||||
|
var rec = new Recorder();
|
||||||
|
|
||||||
|
await Gateway(ApprovalStub(rec, StatustypenPage(withEindstatusFlag: false)))
|
||||||
|
.SetZaakToEindstatusAsync(new Uri(ZaakUrl), Zaaktype, new DateOnly(2026, 6, 4));
|
||||||
|
|
||||||
|
// No isEindstatus flag → the highest volgnummer (/2) is chosen.
|
||||||
|
Assert.Contains("\"statustype\":\"http://openzaak/catalogi/api/v1/statustypen/2\"", rec.Bodies[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Approving_throws_when_the_zaaktype_has_no_statustypen()
|
||||||
|
{
|
||||||
|
var rec = new Recorder();
|
||||||
|
|
||||||
|
// A page with no `results` property (Results is null) — the eindstatus cannot be resolved.
|
||||||
|
var ex = await Assert.ThrowsAsync<InvalidOperationException>(() =>
|
||||||
|
Gateway(ApprovalStub(rec, "{}"))
|
||||||
|
.SetZaakToEindstatusAsync(new Uri(ZaakUrl), Zaaktype, new DateOnly(2026, 6, 4)));
|
||||||
|
|
||||||
|
Assert.Contains("No statustypen found", ex.Message);
|
||||||
|
// It never posts a status when it cannot resolve the eindstatus.
|
||||||
|
Assert.Single(rec.Requests);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Approving_throws_when_the_statustypen_response_is_empty()
|
||||||
|
{
|
||||||
|
var rec = new Recorder();
|
||||||
|
|
||||||
|
var ex = await Assert.ThrowsAsync<InvalidOperationException>(() =>
|
||||||
|
Gateway(ApprovalStub(rec, "null"))
|
||||||
|
.SetZaakToEindstatusAsync(new Uri(ZaakUrl), Zaaktype, new DateOnly(2026, 6, 4)));
|
||||||
|
|
||||||
|
Assert.Contains("empty statustypen", ex.Message);
|
||||||
|
Assert.Single(rec.Requests);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Approving_throws_when_the_statustypen_query_fails()
|
||||||
|
{
|
||||||
|
var rec = new Recorder();
|
||||||
|
|
||||||
|
await Assert.ThrowsAsync<HttpRequestException>(() =>
|
||||||
|
Gateway(ApprovalStub(rec, "{}", getStatus: HttpStatusCode.InternalServerError))
|
||||||
|
.SetZaakToEindstatusAsync(new Uri(ZaakUrl), Zaaktype, new DateOnly(2026, 6, 4)));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Approving_throws_when_posting_the_status_fails()
|
||||||
|
{
|
||||||
|
var rec = new Recorder();
|
||||||
|
|
||||||
|
await Assert.ThrowsAsync<HttpRequestException>(() =>
|
||||||
|
Gateway(ApprovalStub(rec, StatustypenPage(withEindstatusFlag: true), postStatus: HttpStatusCode.BadRequest))
|
||||||
|
.SetZaakToEindstatusAsync(new Uri(ZaakUrl), Zaaktype, new DateOnly(2026, 6, 4)));
|
||||||
|
|
||||||
|
// It resolved the eindstatus and attempted the POST before failing.
|
||||||
|
Assert.Equal(2, rec.Requests.Count);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Approving_rejects_a_null_zaak_or_zaaktype()
|
||||||
|
{
|
||||||
|
var handler = new StubHandler(_ => throw new InvalidOperationException("should not be sent"));
|
||||||
|
|
||||||
|
await Assert.ThrowsAsync<ArgumentNullException>(() =>
|
||||||
|
Gateway(handler).SetZaakToEindstatusAsync(null!, Zaaktype, new DateOnly(2026, 6, 4)));
|
||||||
|
await Assert.ThrowsAsync<ArgumentNullException>(() =>
|
||||||
|
Gateway(handler).SetZaakToEindstatusAsync(new Uri(ZaakUrl), null!, new DateOnly(2026, 6, 4)));
|
||||||
|
}
|
||||||
|
|
||||||
// ZGW tokens are base64url with padding stripped (ZgwToken.B64Url); restore it to decode.
|
// ZGW tokens are base64url with padding stripped (ZgwToken.B64Url); restore it to decode.
|
||||||
private static string DecodeSegment(string segment)
|
private static string DecodeSegment(string segment)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -64,4 +64,14 @@ public class AclHttpClientTests
|
|||||||
await Assert.ThrowsAsync<HttpRequestException>(
|
await Assert.ThrowsAsync<HttpRequestException>(
|
||||||
() => client.ApproveZaakAsync(new Uri("http://openzaak/zaken/api/v1/zaken/abc")));
|
() => client.ApproveZaakAsync(new Uri("http://openzaak/zaken/api/v1/zaken/abc")));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Approve_rejects_a_null_zaak_url_without_sending_a_request()
|
||||||
|
{
|
||||||
|
var capture = new RequestCapture();
|
||||||
|
var client = Client(capture.Responds(HttpStatusCode.NoContent));
|
||||||
|
|
||||||
|
await Assert.ThrowsAsync<ArgumentNullException>(() => client.ApproveZaakAsync(null!));
|
||||||
|
Assert.Null(capture.Seen);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,19 @@ public class ApproveRegistrationTests
|
|||||||
Assert.Equal(RegistrationStatus.Ingeschreven, saved!.Status);
|
Assert.Equal(RegistrationStatus.Ingeschreven, saved!.Status);
|
||||||
Assert.Equal(FakeAclClient.DefaultZaakUrl, acl.ApprovedZaakUrl);
|
Assert.Equal(FakeAclClient.DefaultZaakUrl, acl.ApprovedZaakUrl);
|
||||||
Assert.Equal(1, acl.ApproveCallCount);
|
Assert.Equal(1, acl.ApproveCallCount);
|
||||||
|
// The approved aggregate is persisted (not just mutated in memory).
|
||||||
|
Assert.Equal(1, store.SaveCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Rejects_a_null_command_without_touching_the_store_or_acl()
|
||||||
|
{
|
||||||
|
var store = new FakeRegistrationStore();
|
||||||
|
var acl = new FakeAclClient();
|
||||||
|
var handler = new ApproveRegistration(store, acl);
|
||||||
|
|
||||||
|
await Assert.ThrowsAsync<ArgumentNullException>(() => handler.HandleAsync(null!));
|
||||||
|
Assert.Equal(0, acl.ApproveCallCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -36,8 +49,9 @@ public class ApproveRegistrationTests
|
|||||||
var acl = new FakeAclClient();
|
var acl = new FakeAclClient();
|
||||||
var handler = new ApproveRegistration(store, acl);
|
var handler = new ApproveRegistration(store, acl);
|
||||||
|
|
||||||
await Assert.ThrowsAsync<InvalidOperationException>(
|
var ex = await Assert.ThrowsAsync<InvalidOperationException>(
|
||||||
() => handler.HandleAsync(new ApproveRegistrationCommand(RegistrationId.New())));
|
() => handler.HandleAsync(new ApproveRegistrationCommand(RegistrationId.New())));
|
||||||
|
Assert.Contains("No registration", ex.Message);
|
||||||
Assert.Equal(0, acl.ApproveCallCount);
|
Assert.Equal(0, acl.ApproveCallCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,8 +64,9 @@ public class ApproveRegistrationTests
|
|||||||
store.Seed(registration);
|
store.Seed(registration);
|
||||||
var handler = new ApproveRegistration(store, acl);
|
var handler = new ApproveRegistration(store, acl);
|
||||||
|
|
||||||
await Assert.ThrowsAsync<InvalidOperationException>(
|
var ex = await Assert.ThrowsAsync<InvalidOperationException>(
|
||||||
() => handler.HandleAsync(new ApproveRegistrationCommand(registration.Id)));
|
() => handler.HandleAsync(new ApproveRegistrationCommand(registration.Id)));
|
||||||
|
Assert.Contains("no zaak", ex.Message);
|
||||||
Assert.Equal(0, acl.ApproveCallCount);
|
Assert.Equal(0, acl.ApproveCallCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -117,7 +117,8 @@ public class RegistrationTests
|
|||||||
registration.AttachZaak(new Uri("http://openzaak/zaken/api/v1/zaken/abc"));
|
registration.AttachZaak(new Uri("http://openzaak/zaken/api/v1/zaken/abc"));
|
||||||
registration.Approve();
|
registration.Approve();
|
||||||
|
|
||||||
Assert.Throws<InvalidOperationException>(() => registration.Approve());
|
var ex = Assert.Throws<InvalidOperationException>(() => registration.Approve());
|
||||||
|
Assert.Contains("only an INGEDIEND", ex.Message);
|
||||||
Assert.Equal(RegistrationStatus.Ingeschreven, registration.Status);
|
Assert.Equal(RegistrationStatus.Ingeschreven, registration.Status);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user