feat(notification): add admin rest management and deletion
Add revision-safe service and template APIs, channel mapping, and validated admin controls. Ref: IT-1601
This commit is contained in:
@@ -0,0 +1,143 @@
|
||||
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())
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user