feat(api): manage localized push template definitions
Add revision-protected reads, atomic upsert and deletion with structured actions. Preserve omitted translations and fix insertion of new push languages. Ref: IT-1627
This commit is contained in:
@@ -0,0 +1,125 @@
|
||||
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<TemplateDatabaseFixture>
|
||||
{
|
||||
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<JsonElement> Read(DefinitionApiTestContext c) =>
|
||||
(await c.Client.GetFromJsonAsync<JsonElement>(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<JsonElement>(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<JsonElement>()).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<string, Action>
|
||||
{
|
||||
["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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user