refactor(acl): resolve the objecttype's highest published version (refs #149)

Counting the `versions` URLs assumed a contiguous, all-published list. Read the
objecttype's versions collection instead and take the highest one whose status
is `published`, so a draft version — whose schema is still being shaped — is
never written against.
This commit is contained in:
not
2026-08-14 09:19:50 +02:00
parent d14f379358
commit c67ee7d3f5
2 changed files with 50 additions and 20 deletions
+34 -13
View File
@@ -46,24 +46,32 @@ public class ObjectenGatewayTests
},
new FixedClock(new DateOnly(2026, 6, 4)));
// The Objecttypen lookup (by name) and the Objecten search (by data attribute) that precede every
// write. `respondToWrite` decides what the POST/PATCH returns.
// A stack that answers the three reads every write is preceded by: the objecttype list (matched by
// name), that objecttype's versions, and the Objecten search for an existing record.
private static HttpResponseMessage Route(HttpRequestMessage req, object[] existingObjects) =>
req.RequestUri!.AbsolutePath.StartsWith("/api/v2/objecttypes", StringComparison.Ordinal)
? new HttpResponseMessage(HttpStatusCode.OK)
req.RequestUri!.AbsolutePath.EndsWith("/versions", StringComparison.Ordinal)
? Json(new object[]
{
Content = JsonContent.Create(new
new { version = 1, status = "published" },
new { version = 2, status = "published" },
// A draft must never be written against, even though it is the highest version.
new { version = 3, status = "draft" },
})
: req.RequestUri.AbsolutePath.StartsWith("/api/v2/objecttypes", StringComparison.Ordinal)
? Json(new
{
results = new[]
{
new { url = "http://objecttypen:8000/api/v2/objecttypes/other", name = "SomethingElse", versions = new[] { "…/versions/1" } },
new { url = ObjecttypeUrl, name = "RegisterRecord", versions = new[] { "…/versions/1", "…/versions/2" } },
new { url = "http://objecttypen:8000/api/v2/objecttypes/other", name = "SomethingElse" },
new { url = ObjecttypeUrl, name = "RegisterRecord" },
},
}),
}
: req.Method == HttpMethod.Get
? new HttpResponseMessage(HttpStatusCode.OK) { Content = JsonContent.Create(new { results = existingObjects }) }
: new HttpResponseMessage(HttpStatusCode.Created) { Content = JsonContent.Create(new { url = "http://objecten:8000/api/v2/objects/obj-1" }) };
})
: req.Method == HttpMethod.Get
? Json(new { results = existingObjects })
: new HttpResponseMessage(HttpStatusCode.Created) { Content = JsonContent.Create(new { url = "http://objecten:8000/api/v2/objects/obj-1" }) };
private static HttpResponseMessage Json(object body) =>
new(HttpStatusCode.OK) { Content = JsonContent.Create(body) };
private static RegisterRecord Record() => new("zaak-uuid-1", RegisterRecordStatus.Ingeschreven, "REG-2026-0001");
@@ -76,6 +84,7 @@ public class ObjectenGatewayTests
var write = sent.Single(s => s.Method == HttpMethod.Post && s.Uri.AbsolutePath == "/api/v2/objects");
Assert.Contains($"\"type\":\"{ObjecttypeUrl}\"", write.Body);
// The highest *published* version (2), not the highest version (a draft 3).
Assert.Contains("\"typeVersion\":2", write.Body);
Assert.Contains("\"id\":\"zaak-uuid-1\"", write.Body);
Assert.Contains("\"status\":\"INGESCHREVEN\"", write.Body);
@@ -145,7 +154,19 @@ public class ObjectenGatewayTests
await gateway.UpsertAsync(Record());
await gateway.UpsertAsync(Record() with { Id = "zaak-uuid-2" });
Assert.Single(sent, s => s.Uri.AbsolutePath.StartsWith("/api/v2/objecttypes", StringComparison.Ordinal));
Assert.Single(sent, s => s.Uri.AbsolutePath == "/api/v2/objecttypes");
}
[Fact]
public async Task Fails_loudly_when_the_objecttype_has_no_published_version()
{
var sent = new List<Sent>();
var gateway = Gateway(sent, req => req.RequestUri!.AbsolutePath.EndsWith("/versions", StringComparison.Ordinal)
? Json(new object[] { new { version = 1, status = "draft" } })
: Route(req, []));
var error = await Assert.ThrowsAsync<InvalidOperationException>(() => gateway.UpsertAsync(Record()));
Assert.Contains("published version", error.Message);
}
[Fact]