517b38791b
Add revision-protected reads, atomic upsert and deletion with structured actions. Preserve omitted translations and fix insertion of new push languages. Ref: IT-1627
126 lines
7.4 KiB
C#
126 lines
7.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 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);
|
|
}
|
|
}
|
|
|