8e81572386
Add revision-safe service and template APIs, channel mapping, and validated admin controls. Ref: IT-1601
144 lines
9.4 KiB
C#
144 lines
9.4 KiB
C#
using System.Net;
|
|
using System.Net.Http.Json;
|
|
using System.Text.Json;
|
|
using System.Text.Json.Nodes;
|
|
|
|
namespace HrynCo.NotificationService.Web.IntegrationTests;
|
|
|
|
public sealed class ServiceCatalogApiTests(TemplateDatabaseFixture fixture) : IClassFixture<TemplateDatabaseFixture>
|
|
{
|
|
[Fact]
|
|
public async Task CreateUpdateAndExactReadRoundTripLanguageIdentitiesAndRevision()
|
|
{
|
|
await using var context = await DefinitionApiTestContext.CreateAsync(fixture);
|
|
var body = new { name = "Api Created", languages = new[] { new { code = "en" }, new { code = "uk" } } };
|
|
var response = await context.Client.PostAsJsonAsync("/api/v1/services", body);
|
|
Assert.Equal(HttpStatusCode.Created, response.StatusCode);
|
|
Assert.NotNull(response.Headers.Location);
|
|
var service = (await context.Client.GetFromJsonAsync<JsonElement>(response.Headers.Location)).GetProperty("data");
|
|
Assert.Equal(2, service.GetProperty("languages").GetArrayLength());
|
|
Assert.All(service.GetProperty("languages").EnumerateArray(), l => Assert.NotEqual(Guid.Empty, l.GetProperty("id").GetGuid()));
|
|
var url = "/api/v1/services/" + service.GetProperty("id").GetGuid();
|
|
var update = UpdateBody(service);
|
|
update["name"] = "Renamed API";
|
|
update["languages"]![0]!["code"] = "en-GB";
|
|
((JsonArray)update["languages"]!).Add(new JsonObject { ["code"] = "de" });
|
|
var updated = await context.Client.PutAsJsonAsync(url, update);
|
|
Assert.Equal(HttpStatusCode.OK, updated.StatusCode);
|
|
var saved = (await updated.Content.ReadFromJsonAsync<JsonElement>()).GetProperty("data");
|
|
Assert.NotEqual(service.GetProperty("revision").GetGuid(), saved.GetProperty("revision").GetGuid());
|
|
Assert.Contains(saved.GetProperty("languages").EnumerateArray(), l => l.GetProperty("id").GetGuid() == service.GetProperty("languages")[0].GetProperty("id").GetGuid() && l.GetProperty("code").GetString() == "en-GB");
|
|
Assert.Equal(3, saved.GetProperty("languages").GetArrayLength());
|
|
Assert.Equal(HttpStatusCode.Conflict, (await context.Client.PutAsJsonAsync(url, update)).StatusCode);
|
|
Assert.Equal(HttpStatusCode.Conflict, (await context.Client.PostAsJsonAsync("/api/v1/services", new { name = "Renamed API", languages = Array.Empty<object>() })).StatusCode);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task SnapshotOmissionOfUsedLanguageFailsWithoutChangingServiceOrDefinition()
|
|
{
|
|
await using var context = await DefinitionApiTestContext.CreateAsync(fixture);
|
|
await context.UpsertAsync(DefinitionApiTestContext.Body(null, "en"));
|
|
var before = await ReadAsync(context);
|
|
var definition = await context.ReadAsync();
|
|
var update = UpdateBody(before);
|
|
update["name"] = "Unsaved API rename";
|
|
update["languages"] = new JsonArray(new JsonObject { ["code"] = "fr" });
|
|
Assert.Equal(HttpStatusCode.Conflict, (await context.Client.PutAsJsonAsync(context.ServiceRoot, update)).StatusCode);
|
|
Assert.Equal(before.GetRawText(), (await ReadAsync(context)).GetRawText());
|
|
Assert.Equal(definition.GetRawText(), (await context.ReadAsync()).GetRawText());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task OrdinaryRemovalOnlyDeletesUnusedCatalogRows_AndReturnsNewRevisionOnRead()
|
|
{
|
|
await using var context = await DefinitionApiTestContext.CreateAsync(fixture);
|
|
await context.UpsertAsync(DefinitionApiTestContext.Body(null, "en"));
|
|
var before = await ReadAsync(context);
|
|
var used = before.GetProperty("languages").EnumerateArray().Single(l => l.GetProperty("code").GetString() == "en");
|
|
var unused = before.GetProperty("languages").EnumerateArray().Single(l => l.GetProperty("code").GetString() == "uk");
|
|
var root = context.ServiceRoot + "/languages/";
|
|
Assert.Equal(1, used.GetProperty("versionCount").GetInt32());
|
|
Assert.Equal(HttpStatusCode.Conflict, (await context.Client.DeleteAsync(root + used.GetProperty("id").GetGuid() + "?revision=" + before.GetProperty("revision").GetGuid())).StatusCode);
|
|
Assert.Equal(HttpStatusCode.OK, (await context.Client.DeleteAsync(root + unused.GetProperty("id").GetGuid() + "?revision=" + before.GetProperty("revision").GetGuid())).StatusCode);
|
|
var after = await ReadAsync(context);
|
|
Assert.NotEqual(before.GetProperty("revision").GetGuid(), after.GetProperty("revision").GetGuid());
|
|
Assert.DoesNotContain(after.GetProperty("languages").EnumerateArray(), l => l.GetProperty("code").GetString() == "uk");
|
|
Assert.Single((await context.ReadAsync()).GetProperty("translations").EnumerateArray());
|
|
Assert.Equal(HttpStatusCode.NotFound, (await context.Client.DeleteAsync(root + Guid.NewGuid() + "?revision=" + after.GetProperty("revision").GetGuid())).StatusCode);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ExplicitDestructiveRemovalRequiresConfirmationRevisionAndCurrentCount_AndRetainsDefinitions()
|
|
{
|
|
await using var context = await DefinitionApiTestContext.CreateAsync(fixture);
|
|
await context.UpsertAsync(DefinitionApiTestContext.Body(null, "en"), "First");
|
|
var service = await ReadAsync(context);
|
|
var language = service.GetProperty("languages").EnumerateArray().Single(l => l.GetProperty("code").GetString() == "en");
|
|
var url = context.ServiceRoot + "/languages/" + language.GetProperty("id").GetGuid() + "/delete-with-versions";
|
|
var body = new JsonObject { ["revision"] = service.GetProperty("revision").GetString(), ["expectedVersionCount"] = 1, ["confirmDelete"] = false };
|
|
Assert.Equal(HttpStatusCode.BadRequest, (await context.Client.PostAsJsonAsync(url, body)).StatusCode);
|
|
body.Remove("confirmDelete");
|
|
Assert.Equal(HttpStatusCode.BadRequest, (await context.Client.PostAsJsonAsync(url, body)).StatusCode);
|
|
body["confirmDelete"] = true;
|
|
await context.UpsertAsync(DefinitionApiTestContext.Body(null, "en"), "Second");
|
|
Assert.Equal(HttpStatusCode.Conflict, (await context.Client.PostAsJsonAsync(url, body)).StatusCode);
|
|
body["expectedVersionCount"] = 2;
|
|
body["revision"] = Guid.NewGuid().ToString();
|
|
Assert.Equal(HttpStatusCode.Conflict, (await context.Client.PostAsJsonAsync(url, body)).StatusCode);
|
|
body["revision"] = service.GetProperty("revision").GetString();
|
|
Assert.Equal(HttpStatusCode.OK, (await context.Client.PostAsJsonAsync(url, body)).StatusCode);
|
|
var definitions = (await context.Client.GetFromJsonAsync<JsonElement>(context.ServiceRoot + "/email-template-definitions/all")).GetProperty("data");
|
|
Assert.Equal(2, definitions.GetArrayLength());
|
|
Assert.All(definitions.EnumerateArray(), d => Assert.Empty(d.GetProperty("translations").EnumerateArray()));
|
|
Assert.DoesNotContain((await ReadAsync(context)).GetProperty("languages").EnumerateArray(), l => l.GetProperty("code").GetString() == "en");
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("nullLanguages")]
|
|
[InlineData("duplicateCodes")]
|
|
[InlineData("foreignId")]
|
|
public async Task InvalidCreatePersistsNoService(string scenario)
|
|
{
|
|
await using var context = await DefinitionApiTestContext.CreateAsync(fixture);
|
|
var body = new JsonObject { ["name"] = "InvalidCreate", ["languages"] = new JsonArray(new JsonObject { ["code"] = "en" }) };
|
|
switch (scenario)
|
|
{
|
|
case "nullLanguages": body["languages"] = null; break;
|
|
case "duplicateCodes": ((JsonArray)body["languages"]!).Add(new JsonObject { ["code"] = "en" }); break;
|
|
case "foreignId": body["languages"]![0]!["id"] = Guid.NewGuid().ToString(); break;
|
|
}
|
|
Assert.Equal(HttpStatusCode.BadRequest, (await context.Client.PostAsJsonAsync("/api/v1/services", body)).StatusCode);
|
|
Assert.Equal(HttpStatusCode.NotFound, (await context.Client.GetAsync("/api/v1/services?name=InvalidCreate")).StatusCode);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("emptyRevision", HttpStatusCode.BadRequest)]
|
|
[InlineData("missingRevision", HttpStatusCode.BadRequest)]
|
|
[InlineData("foreignLanguageId", HttpStatusCode.Conflict)]
|
|
public async Task InvalidUpdatePreservesCurrentServiceSnapshot(string scenario, HttpStatusCode expectedStatus)
|
|
{
|
|
await using var context = await DefinitionApiTestContext.CreateAsync(fixture);
|
|
var before = await ReadAsync(context);
|
|
var body = UpdateBody(before);
|
|
body["name"] = "Unsaved name";
|
|
switch (scenario)
|
|
{
|
|
case "emptyRevision": body["revision"] = Guid.Empty.ToString(); break;
|
|
case "missingRevision": body.Remove("revision"); break;
|
|
case "foreignLanguageId": body["languages"]![0]!["id"] = Guid.NewGuid().ToString(); break;
|
|
}
|
|
Assert.Equal(expectedStatus, (await context.Client.PutAsJsonAsync(context.ServiceRoot, body)).StatusCode);
|
|
Assert.Equal(before.GetRawText(), (await ReadAsync(context)).GetRawText());
|
|
}
|
|
|
|
private static async Task<JsonElement> ReadAsync(DefinitionApiTestContext context) =>
|
|
(await context.Client.GetFromJsonAsync<JsonElement>(context.ServiceRoot)).GetProperty("data");
|
|
|
|
private static JsonObject UpdateBody(JsonElement service) => new()
|
|
{
|
|
["name"] = service.GetProperty("name").GetString(), ["revision"] = service.GetProperty("revision").GetString(),
|
|
["languages"] = new JsonArray(service.GetProperty("languages").EnumerateArray().Select(l => (JsonNode)new JsonObject
|
|
{ ["id"] = l.GetProperty("id").GetString(), ["code"] = l.GetProperty("code").GetString() }).ToArray())
|
|
};
|
|
}
|