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 { [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(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()).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() })).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(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 ReadAsync(DefinitionApiTestContext context) => (await context.Client.GetFromJsonAsync(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()) }; }