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 PushTemplateDefinitionApiTests(TemplateDatabaseFixture fixture) : IClassFixture { private static string Url(DefinitionApiTestContext c, string suffix = "", string key = "Welcome") => c.ServiceRoot + "/push-template-definitions" + suffix + "?key=" + Uri.EscapeDataString(key); private static JsonObject Body(Guid? revision, params string[] languages) => new() { ["revision"] = revision?.ToString(), ["variables"] = new JsonArray(new JsonObject { ["name"] = "Name", ["required"] = true }), ["translations"] = new JsonArray(languages.Select(code => (JsonNode)new JsonObject { ["languageCode"] = code, ["title"] = "Title " + code, ["body"] = "Hello {{Name}}", ["actions"] = new JsonObject { ["defaultAction"] = "contacts", ["buttons"] = new JsonArray( new JsonObject { ["action"] = "contacts", ["label"] = "Go " + code }) } }).ToArray()) }; private static async Task Read(DefinitionApiTestContext c) => (await c.Client.GetFromJsonAsync(Url(c))).GetProperty("data"); [Fact] public async Task CreatePatchReadAndDeletePreserveEmailAndOmittedLanguages() { await using var c = await DefinitionApiTestContext.CreateAsync(fixture); await c.UpsertAsync(DefinitionApiTestContext.Body(null, "en")); var email = await c.ReadAsync(); var create = await c.Client.PostAsJsonAsync(Url(c, "/upsert"), Body(null, "en", "uk")); Assert.Equal(HttpStatusCode.Created, create.StatusCode); Assert.NotNull(create.Headers.Location); var original = await Read(c); var revision = original.GetProperty("revision").GetGuid(); var update = await c.Client.PostAsJsonAsync(Url(c, "/upsert"), Body(revision, "de")); Assert.Equal(HttpStatusCode.OK, update.StatusCode); var saved = await Read(c); Assert.Equal(3, saved.GetProperty("translations").GetArrayLength()); Assert.Equal(original.GetProperty("translations")[1].GetRawText(), saved.GetProperty("translations")[2].GetRawText()); Assert.Equal("contacts", saved.GetProperty("translations")[0].GetProperty("actions").GetProperty("defaultAction").GetString()); var all = await c.Client.GetFromJsonAsync(c.ServiceRoot + "/push-template-definitions/all"); Assert.Single(all.GetProperty("data").EnumerateArray()); Assert.Equal(HttpStatusCode.Conflict, (await c.Client.DeleteAsync(Url(c) + "&revision=" + revision)).StatusCode); var remove = Body(saved.GetProperty("revision").GetGuid()); remove["removeLanguageCodes"] = new JsonArray("de", "en", "uk"); Assert.Equal(HttpStatusCode.OK, (await c.Client.PostAsJsonAsync(Url(c, "/upsert"), remove)).StatusCode); saved = await Read(c); Assert.Empty(saved.GetProperty("translations").EnumerateArray()); Assert.Equal(HttpStatusCode.OK, (await c.Client.DeleteAsync(Url(c) + "&revision=" + saved.GetProperty("revision").GetGuid())).StatusCode); Assert.Equal(HttpStatusCode.NotFound, (await c.Client.GetAsync(Url(c))).StatusCode); Assert.Equal(email.GetRawText(), (await c.ReadAsync()).GetRawText()); } [Theory] [InlineData("nullVariables")] [InlineData("missingTranslations")] [InlineData("nullTranslation")] [InlineData("duplicateLanguage")] [InlineData("unknownLanguage")] [InlineData("nullActions")] [InlineData("unknownActionField")] [InlineData("blankLabel")] [InlineData("unknownPlaceholder")] [InlineData("invalidRetainedTranslation")] [InlineData("overlap")] [InlineData("unknownRemoval")] [InlineData("emptyRevision")] public async Task InvalidPatchPreservesEntireDefinition(string scenario) { await using var c = await DefinitionApiTestContext.CreateAsync(fixture); await c.Client.PostAsJsonAsync(Url(c, "/upsert"), Body(null, "en", "uk")); var before = await Read(c); var body = Body(before.GetProperty("revision").GetGuid(), "en"); ApplyInvalidCase(body, scenario); var result = await c.Client.PostAsJsonAsync(Url(c, "/upsert"), body); Assert.Equal(HttpStatusCode.BadRequest, result.StatusCode); Assert.False((await result.Content.ReadFromJsonAsync()).GetProperty("success").GetBoolean()); Assert.Equal(before.GetRawText(), (await Read(c)).GetRawText()); } private static void ApplyInvalidCase(JsonObject body, string scenario) { var translation = body["translations"]![0]!; var cases = new Dictionary { ["nullVariables"] = () => { body["variables"] = null; }, ["missingTranslations"] = () => { body.Remove("translations"); }, ["nullTranslation"] = () => { body["translations"] = new JsonArray((JsonNode?)null); }, ["duplicateLanguage"] = () => { body["translations"]!.AsArray().Add(translation.DeepClone()); }, ["unknownLanguage"] = () => { translation["languageCode"] = "fr"; }, ["nullActions"] = () => { translation["actions"] = null; }, ["unknownActionField"] = () => { translation["actions"]!["url"] = "ignored"; }, ["blankLabel"] = () => { translation["actions"]!["buttons"]![0]!["label"] = ""; }, ["unknownPlaceholder"] = () => { translation["body"] = "{{Unknown}}"; }, ["invalidRetainedTranslation"] = () => { body["variables"] = new JsonArray(); body["translations"] = new JsonArray(); }, ["overlap"] = () => { body["removeLanguageCodes"] = new JsonArray("en"); }, ["unknownRemoval"] = () => { body["removeLanguageCodes"] = new JsonArray("fr"); }, ["emptyRevision"] = () => { body["revision"] = Guid.Empty.ToString(); }, }; cases[scenario](); } [Fact] public async Task ConcurrentUpdatesAndCreatesRejectStaleRevisions() { await using var c = await DefinitionApiTestContext.CreateAsync(fixture); var creates = await Task.WhenAll(Enumerable.Range(0, 2).Select(_ => c.Client.PostAsJsonAsync(Url(c, "/upsert"), Body(null, "en")))); Assert.Single(creates, r => r.StatusCode == HttpStatusCode.Created); Assert.Single(creates, r => r.StatusCode == HttpStatusCode.Conflict); var revision = (await Read(c)).GetProperty("revision").GetGuid(); var updates = await Task.WhenAll(new[] { "de", "uk" }.Select(l => c.Client.PostAsJsonAsync(Url(c, "/upsert"), Body(revision, l)))); Assert.Single(updates, r => r.StatusCode == HttpStatusCode.OK); Assert.Single(updates, r => r.StatusCode == HttpStatusCode.Conflict); Assert.Equal(2, (await Read(c)).GetProperty("translations").GetArrayLength()); Assert.Equal(HttpStatusCode.Conflict, (await c.Client.PostAsJsonAsync(Url(c, "/upsert", "Missing"), Body(Guid.NewGuid(), "en"))).StatusCode); Assert.Equal(HttpStatusCode.BadRequest, (await c.Client.DeleteAsync(Url(c))).StatusCode); Assert.Equal(HttpStatusCode.NotFound, (await c.Client.GetAsync($"/api/v1/services/{Guid.NewGuid()}/push-template-definitions/all")).StatusCode); Assert.Equal(HttpStatusCode.NotFound, (await c.Client.PostAsJsonAsync($"/api/v1/services/{Guid.NewGuid()}/push-template-definitions/upsert?key=X", Body(null, "en"))).StatusCode); } }