using System.Net; using System.Text.Json.Nodes; using Cleanuparr.Infrastructure.Features.Notifications.Apprise; using Cleanuparr.Infrastructure.Features.Notifications.Discord; using Cleanuparr.Infrastructure.Features.Notifications.Gotify; using Cleanuparr.Infrastructure.Features.Notifications.Notifiarr; using Cleanuparr.Infrastructure.Features.Notifications.Ntfy; using Cleanuparr.Infrastructure.Features.Notifications.Telegram; using Cleanuparr.Infrastructure.Tests.TestHelpers; using Cleanuparr.Persistence.Models.Configuration.Notification; using Cleanuparr.Shared.Helpers; using Microsoft.Extensions.Logging; using NSubstitute; using Shouldly; using Xunit; namespace Cleanuparr.Infrastructure.Tests.Features.Notifications; public class NotificationPayloadSerializationTests { private const string SpecialChars = "Ubuntu & bold it's"; private static (T proxy, FakeHttpMessageHandler handler) CreateProxy(Func factory) { FakeHttpMessageHandler handler = new(); handler.SetupResponse(HttpStatusCode.OK); IHttpClientFactory httpClientFactory = Substitute.For(); httpClientFactory.CreateClient(Constants.HttpClientWithRetryName).Returns(new HttpClient(handler)); return (factory(httpClientFactory), handler); } private static JsonObject ParseBody(FakeHttpMessageHandler handler) { string? body = handler.CapturedRequestBodies[0]; body.ShouldNotBeNull(); return JsonNode.Parse(body)!.AsObject(); } private static IEnumerable Keys(JsonObject obj) { return obj.Select(kv => kv.Key).OrderBy(k => k, StringComparer.Ordinal); } [Fact] public async Task Discord_OmitsNulls_UsesExplicitNames_PreservesSpecialChars() { (DiscordProxy proxy, FakeHttpMessageHandler handler) = CreateProxy(f => new DiscordProxy(Substitute.For>(), f)); DiscordPayload payload = new() { Username = null, AvatarUrl = "https://example.com/a.png?x=1&y=2", Embeds = [new DiscordEmbed { Title = SpecialChars, Description = "d", Color = 123 }] }; await proxy.SendNotification(payload, new DiscordConfig { WebhookUrl = "https://discord.com/api/webhooks/1/a" }); JsonObject body = ParseBody(handler); Keys(body).ShouldBe(["avatar_url", "embeds"]); body["avatar_url"]!.GetValue().ShouldBe("https://example.com/a.png?x=1&y=2"); JsonObject embed = body["embeds"]!.AsArray()[0]!.AsObject(); Keys(embed).ShouldBe(["color", "description", "fields", "title"]); embed["title"]!.GetValue().ShouldBe(SpecialChars); embed["color"]!.GetValue().ShouldBe(123); embed["fields"]!.AsArray().Count.ShouldBe(0); handler.CapturedRequestBodies[0]!.ShouldContain(SpecialChars); handler.CapturedRequestBodies[0]!.ShouldContain("x=1&y=2"); } [Fact] public async Task Ntfy_OmitsNulls_PreservesSpecialChars() { (NtfyProxy proxy, FakeHttpMessageHandler handler) = CreateProxy(f => new NtfyProxy(f)); NtfyPayload payload = new() { Topic = "my-topic", Message = SpecialChars, Title = null, Priority = null, Tags = ["a", "b"], Click = null }; await proxy.SendNotification(payload, new NtfyConfig { ServerUrl = "https://ntfy.example.com" }); JsonObject body = ParseBody(handler); Keys(body).ShouldBe(["message", "tags", "topic"]); body["topic"]!.GetValue().ShouldBe("my-topic"); body["message"]!.GetValue().ShouldBe(SpecialChars); body["tags"]!.AsArray().Select(n => n!.GetValue()).ShouldBe(["a", "b"]); handler.CapturedRequestBodies[0]!.ShouldContain(SpecialChars); } [Fact] public async Task Apprise_OmitsNulls_UsesDefaults_PreservesSpecialChars() { (AppriseProxy proxy, FakeHttpMessageHandler handler) = CreateProxy(f => new AppriseProxy(f)); ApprisePayload payload = new() { Title = "T", Body = SpecialChars }; await proxy.SendNotification(payload, new AppriseConfig { Url = "https://apprise.example.com", Key = "mykey" }); JsonObject body = ParseBody(handler); Keys(body).ShouldBe(["body", "format", "title", "type"]); body["title"]!.GetValue().ShouldBe("T"); body["body"]!.GetValue().ShouldBe(SpecialChars); body["type"]!.GetValue().ShouldBe("info"); body["format"]!.GetValue().ShouldBe("text"); handler.CapturedRequestBodies[0]!.ShouldContain(SpecialChars); } [Fact] public async Task Telegram_SendMessage_OmitsNulls_PreservesSpecialChars() { (TelegramProxy proxy, FakeHttpMessageHandler handler) = CreateProxy(f => new TelegramProxy(f)); TelegramPayload payload = new() { ChatId = "123", Text = SpecialChars, MessageThreadId = null, DisableNotification = false }; await proxy.SendNotification(payload, "TOKEN"); JsonObject body = ParseBody(handler); Keys(body).ShouldBe(["chat_id", "disable_notification", "disable_web_page_preview", "parse_mode", "text"]); body["chat_id"]!.GetValue().ShouldBe("123"); body["disable_notification"]!.GetValue().ShouldBeFalse(); body["disable_web_page_preview"]!.GetValue().ShouldBeTrue(); body["parse_mode"]!.GetValue().ShouldBe("HTML"); body["text"]!.GetValue().ShouldBe(SpecialChars); handler.CapturedRequestBodies[0]!.ShouldContain(SpecialChars); } [Fact] public async Task Gotify_OmitsNulls_PreservesSpecialChars() { (GotifyProxy proxy, FakeHttpMessageHandler handler) = CreateProxy(f => new GotifyProxy(Substitute.For>(), f)); GotifyPayload payload = new() { Title = SpecialChars, Message = "m", Priority = 5, Extras = null }; await proxy.SendNotification(payload, new GotifyConfig { ServerUrl = "https://gotify.example.com", ApplicationToken = "tok" }); JsonObject body = ParseBody(handler); Keys(body).ShouldBe(["message", "priority", "title"]); body["title"]!.GetValue().ShouldBe(SpecialChars); body["message"]!.GetValue().ShouldBe("m"); body["priority"]!.GetValue().ShouldBe(5); handler.CapturedRequestBodies[0]!.ShouldContain(SpecialChars); } [Fact] public async Task Notifiarr_IncludesNulls() { (NotifiarrProxy proxy, FakeHttpMessageHandler handler) = CreateProxy(f => new NotifiarrProxy(Substitute.For>(), f)); NotifiarrPayload payload = new() { Notification = new NotifiarrNotification { Update = false, Event = null }, Discord = null! }; await proxy.SendNotification(payload, new NotifiarrConfig { ApiKey = "0123456789" }); JsonObject body = ParseBody(handler); body.ContainsKey("discord").ShouldBeTrue(); body["discord"].ShouldBeNull(); JsonObject notification = body["notification"]!.AsObject(); notification["name"]!.GetValue().ShouldBe("Cleanuparr"); notification.ContainsKey("event").ShouldBeTrue(); notification["event"].ShouldBeNull(); notification["update"]!.GetValue().ShouldBeFalse(); } }