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(() => PushTemplateRenderer.Render(template, message)); } [Fact] public void MissingBindingOrConfiguredActionFailsInsteadOfSendingIncompleteNotification() { var message = Message(); Assert.Throws(() => PushTemplateRenderer.Render(Template(), message with { TemplateData = message.TemplateData! with { Bindings = new Dictionary() } })); Assert.Throws(() => PushTemplateRenderer.Render(Template(), message with { TemplateData = message.TemplateData! with { Actions = new Dictionary() } })); } [Fact] public void TruncationKeepsUnicodeTextElementsIntact() { var message = Message() with { TemplateData = Message().TemplateData! with { Bindings = new Dictionary { ["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(() => 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 { ["Name"] = "{{Injected}}" }, Actions = new Dictionary { ["open"] = new() { ObjectId = "need-42" } } } }; }