test(acl): raise the Objecten gateway above the mutation ratchet (refs #149)
The new gateway landed at 77.6%, dragging the ACL score under its 90 break threshold. The gaps were all real behaviour nobody was asserting: a failed or empty read being mistaken for "nothing there yet" and followed by a blind write, a `results`-less response taking down the resolve with an ArgumentNullException, the CRS headers going to Objecttypen (which is not a geo API), and the write body being sent chunked. ACL score 86.63% → 92.08%.
This commit is contained in:
@@ -19,7 +19,8 @@ public class ObjectenGatewayTests
|
||||
public DateOnly Today { get; } = today;
|
||||
}
|
||||
|
||||
private sealed record Sent(HttpMethod Method, Uri Uri, string? Body, string? Auth, string? ContentCrs, string? AcceptCrs);
|
||||
private sealed record Sent(
|
||||
HttpMethod Method, Uri Uri, string? Body, string? Auth, string? ContentCrs, string? AcceptCrs, long? ContentLength);
|
||||
|
||||
private const string ObjecttypeUrl = "http://objecttypen:8000/api/v2/objecttypes/ot-1";
|
||||
|
||||
@@ -27,13 +28,17 @@ public class ObjectenGatewayTests
|
||||
new(
|
||||
new HttpClient(new StubHandler(async req =>
|
||||
{
|
||||
// Read the length BEFORE the body: ReadAsStringAsync buffers the content and would set
|
||||
// ContentLength as a side effect, masking whether the gateway buffered it itself (uwsgi
|
||||
// rejects a chunked body).
|
||||
sent.Add(new Sent(
|
||||
req.Method,
|
||||
req.RequestUri!,
|
||||
req.Content is null ? null : await req.Content.ReadAsStringAsync(),
|
||||
req.Headers.Authorization?.ToString(),
|
||||
req.Content?.Headers.TryGetValues("Content-Crs", out var c) == true ? string.Join(",", c!) : null,
|
||||
req.Headers.TryGetValues("Accept-Crs", out var a) ? string.Join(",", a) : null));
|
||||
ContentLength: req.Content?.Headers.ContentLength,
|
||||
Body: req.Content is null ? null : await req.Content.ReadAsStringAsync(),
|
||||
Auth: req.Headers.Authorization?.ToString(),
|
||||
ContentCrs: req.Content?.Headers.TryGetValues("Content-Crs", out var c) == true ? string.Join(",", c!) : null,
|
||||
AcceptCrs: req.Headers.TryGetValues("Accept-Crs", out var a) ? string.Join(",", a) : null));
|
||||
return respond(req);
|
||||
})),
|
||||
new ObjectenOptions
|
||||
@@ -192,6 +197,100 @@ public class ObjectenGatewayTests
|
||||
|
||||
var error = await Assert.ThrowsAsync<HttpRequestException>(() => gateway.UpsertAsync(Record()));
|
||||
Assert.Contains("schema mismatch", error.Message);
|
||||
Assert.Contains("Creating the register record", error.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Surfaces_the_objecten_error_body_when_an_update_is_rejected()
|
||||
{
|
||||
var sent = new List<Sent>();
|
||||
object[] existing = [new { url = "http://objecten:8000/api/v2/objects/obj-9" }];
|
||||
var gateway = Gateway(sent, req => req.Method == HttpMethod.Patch
|
||||
? new HttpResponseMessage(HttpStatusCode.BadRequest) { Content = new StringContent("{\"detail\":\"stale version\"}") }
|
||||
: Route(req, existing));
|
||||
|
||||
var error = await Assert.ThrowsAsync<HttpRequestException>(() => gateway.UpsertAsync(Record()));
|
||||
Assert.Contains("stale version", error.Message);
|
||||
Assert.Contains("Updating the register record", error.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Surfaces_a_failed_read_instead_of_writing_blind()
|
||||
{
|
||||
var sent = new List<Sent>();
|
||||
var gateway = Gateway(sent, _ => new HttpResponseMessage(HttpStatusCode.Unauthorized)
|
||||
{
|
||||
Content = new StringContent("{\"detail\":\"invalid token\"}"),
|
||||
});
|
||||
|
||||
var error = await Assert.ThrowsAsync<HttpRequestException>(() => gateway.UpsertAsync(Record()));
|
||||
Assert.Contains("Querying objecttypen", error.Message);
|
||||
Assert.Contains("invalid token", error.Message);
|
||||
// A read that failed must never be mistaken for "nothing there yet" and followed by a write.
|
||||
Assert.DoesNotContain(sent, s => s.Method == HttpMethod.Post || s.Method == HttpMethod.Patch);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Surfaces_an_empty_read_body_rather_than_dereferencing_it()
|
||||
{
|
||||
var sent = new List<Sent>();
|
||||
var gateway = Gateway(sent, _ => new HttpResponseMessage(HttpStatusCode.OK)
|
||||
{
|
||||
Content = new StringContent("null", System.Text.Encoding.UTF8, "application/json"),
|
||||
});
|
||||
|
||||
var error = await Assert.ThrowsAsync<InvalidOperationException>(() => gateway.UpsertAsync(Record()));
|
||||
Assert.Contains("objecttypen", error.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Treats_a_result_less_response_as_no_match_rather_than_crashing()
|
||||
{
|
||||
var sent = new List<Sent>();
|
||||
// Neither collection carries a `results` key — the objecttype is absent, which must surface as
|
||||
// the "not registered" error rather than a NullReferenceException.
|
||||
var gateway = Gateway(sent, req => req.RequestUri!.AbsolutePath.EndsWith("/versions", StringComparison.Ordinal)
|
||||
? Json(Array.Empty<object>())
|
||||
: Json(new { }));
|
||||
|
||||
await Assert.ThrowsAsync<InvalidOperationException>(() => gateway.UpsertAsync(Record()));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Creates_the_object_when_the_search_response_carries_no_results_key()
|
||||
{
|
||||
var sent = new List<Sent>();
|
||||
var gateway = Gateway(sent, req => req.Method == HttpMethod.Get && req.RequestUri!.AbsolutePath == "/api/v2/objects"
|
||||
? Json(new { })
|
||||
: Route(req, []));
|
||||
|
||||
await gateway.UpsertAsync(Record());
|
||||
|
||||
Assert.Contains(sent, s => s.Method == HttpMethod.Post && s.Uri.AbsolutePath == "/api/v2/objects");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Reads_objecttypen_without_the_crs_headers_it_does_not_accept()
|
||||
{
|
||||
var sent = new List<Sent>();
|
||||
|
||||
await Gateway(sent, req => Route(req, [])).UpsertAsync(Record());
|
||||
|
||||
// Objecttypen is not a geo API; only the Objecten hops carry CRS.
|
||||
Assert.All(
|
||||
sent.Where(s => s.Uri.AbsolutePath.StartsWith("/api/v2/objecttypes", StringComparison.Ordinal)),
|
||||
s => Assert.Null(s.AcceptCrs));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Buffers_the_write_body_so_uwsgi_gets_a_content_length()
|
||||
{
|
||||
var sent = new List<Sent>();
|
||||
|
||||
await Gateway(sent, req => Route(req, [])).UpsertAsync(Record());
|
||||
|
||||
var write = sent.Single(s => s.Method == HttpMethod.Post && s.Uri.AbsolutePath == "/api/v2/objects");
|
||||
Assert.NotNull(write.ContentLength);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
Reference in New Issue
Block a user