feat(templates): add localized push templates and actions
Share template keys across email and push with independent editors and revisions. Render localized actions and preserve version-one delivery. Ref: IT-1627
This commit is contained in:
@@ -0,0 +1,90 @@
|
||||
using System.Text.Json;
|
||||
using HrynCo.NotificationService.Contracts.Messages;
|
||||
using HrynCo.NotificationService.DAL.Abstract.Templates;
|
||||
using HrynCo.NotificationService.Worker.Services.Push;
|
||||
|
||||
namespace HrynCo.NotificationService.Services.Tests.Push;
|
||||
|
||||
public sealed class PushTemplateRendererTests
|
||||
{
|
||||
[Fact]
|
||||
public void RendersLocalizedLabelsAndOpaqueObjectIdsWithoutReinterpretingValues()
|
||||
{
|
||||
var result = PushTemplateRenderer.Render(Template(), Message());
|
||||
Assert.Equal("Hello {{Injected}}", result.Title);
|
||||
var button = Assert.Single(result.Buttons);
|
||||
Assert.Equal("View {{Injected}}", button.Label);
|
||||
Assert.Equal("need-42", button.ObjectId);
|
||||
Assert.Equal("open", result.DefaultAction!.Action);
|
||||
using var payload = JsonDocument.Parse(FirebasePushClient.BuildPayload(Guid.NewGuid(), result)["actions"]);
|
||||
Assert.Equal("need-42", payload.RootElement.GetProperty("buttons")[0].GetProperty("objectId").GetString());
|
||||
Assert.Equal("open", payload.RootElement.GetProperty("defaultAction").GetProperty("action").GetString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UsesEnglishFallbackAndRejectsMissingFallback()
|
||||
{
|
||||
var message = Message() with { LanguageCode = "uk" };
|
||||
Assert.Equal("Hello {{Injected}}", PushTemplateRenderer.Render(Template(), message).Title);
|
||||
var template = Template();
|
||||
template.Languages = [];
|
||||
Assert.Throws<EmailTemplateWriteException>(() => PushTemplateRenderer.Render(template, message));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MissingBindingOrConfiguredActionFailsInsteadOfSendingIncompleteNotification()
|
||||
{
|
||||
var message = Message();
|
||||
Assert.Throws<EmailTemplateWriteException>(() => PushTemplateRenderer.Render(Template(), message with
|
||||
{
|
||||
TemplateData = message.TemplateData! with { Bindings = new Dictionary<string, string>() }
|
||||
}));
|
||||
Assert.Throws<EmailTemplateWriteException>(() => PushTemplateRenderer.Render(Template(), message with
|
||||
{
|
||||
TemplateData = message.TemplateData! with { Actions = new Dictionary<string, PushActionMetadata>() }
|
||||
}));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TruncationKeepsUnicodeTextElementsIntact()
|
||||
{
|
||||
var message = Message() with
|
||||
{
|
||||
TemplateData = Message().TemplateData! with { Bindings = new Dictionary<string, string> { ["Name"] = string.Concat(Enumerable.Repeat("😀", 100)) } }
|
||||
};
|
||||
var result = PushTemplateRenderer.Render(Template(), message);
|
||||
Assert.True(result.Title.Length <= 100);
|
||||
Assert.EndsWith("…", result.Title);
|
||||
Assert.False(char.IsHighSurrogate(result.Title[^2]));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("{\"buttons\":null}")]
|
||||
[InlineData("{\"buttons\":[{\"action\":\"open\",\"label\":\"\"}]}")]
|
||||
[InlineData("{\"buttons\":[{\"action\":\"open\",\"label\":\"Open\"},{\"action\":\"open\",\"label\":\"Again\"}]}")]
|
||||
[InlineData("{\"defaultAction\":\"https://example.invalid\"}")]
|
||||
[InlineData("{\"unknown\":true}")]
|
||||
public void RejectsInvalidActionDefinitions(string json) =>
|
||||
Assert.Throws<EmailTemplateWriteException>(() => PushTemplateActionValidation.Parse(json));
|
||||
|
||||
private static PushTemplateDefinition Template() => new()
|
||||
{
|
||||
Key = "Shared", Variables = [new EmailTemplateVariable { Name = "Name", Required = true }],
|
||||
Languages = [new PushTemplateTranslation
|
||||
{
|
||||
LanguageCode = "en", Title = "Hello {{Name}}", Body = "Shared item",
|
||||
ActionsJson = "{\"defaultAction\":\"open\",\"buttons\":[{\"action\":\"open\",\"label\":\"View {{Name}}\"}]}"
|
||||
}]
|
||||
};
|
||||
|
||||
private static SendPushMessageData Message() => new()
|
||||
{
|
||||
RegistrationId = Guid.NewGuid(), BindingId = Guid.NewGuid(), UserId = Guid.NewGuid(),
|
||||
ServiceName = "Example", Version = 2, TemplateKey = "Shared", LanguageCode = "en", ExpiresAt = DateTimeOffset.UtcNow.AddMinutes(5),
|
||||
TemplateData = new PushTemplateData
|
||||
{
|
||||
Bindings = new Dictionary<string, string> { ["Name"] = "{{Injected}}" },
|
||||
Actions = new Dictionary<string, PushActionMetadata> { ["open"] = new() { ObjectId = "need-42" } }
|
||||
}
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user