Files
hrynco-notification-service/HrynCo.NotificationService.Web.IntegrationTests/ServiceCatalogApiTests.cs
T
agrynco 8e81572386 feat(notification): add admin rest management and deletion
Add revision-safe service and template APIs, channel mapping, and validated admin controls.

Ref: IT-1601
2026-10-17 22:51:01 +03:00

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())
};
}