using System.Linq; using Xunit; using ytLive.Services; namespace ytLive.Tests; /// /// 2026-11-05 ruling (retires TASK 49's local wordlist): YouTube owns chat moderation, so the /// behaviour we owe it is not a second list — it is not showing what YouTube has removed. /// The feed sends deleted messages as a tombstone placeholder carrying no text, and marks /// non-displayable events with hasDisplayContent: false; both used to become blank rows on /// the overlay (the parser had no filter at all). /// public class ChatModerationParseTests { [Fact] public void Tombstone_NeverBecomesAChatRow() { const string json = """ { "items": [ {"id":"d1","snippet":{"type":"tombstone","liveChatId":"c","publishedAt":"2026-11-05T10:00:00.000Z"}}, {"id":"m1","snippet":{"type":"textMessageEvent","hasDisplayContent":true, "textMessageDetails":{"messageText":"hello"},"publishedAt":"2026-11-05T10:00:01.000Z"}, "authorDetails":{"displayName":"Viewer","channelId":"UC1"}} ], "nextPageToken":"n" } """; var page = YouTubeChatService.ParsePage(json); Assert.Single(page.Messages); Assert.Equal("hello", page.Messages[0].Message); } [Fact] public void MessagesWithoutDisplayContent_AreSkipped() { const string json = """ { "items": [ {"id":"m1","snippet":{"type":"textMessageEvent","hasDisplayContent":false, "publishedAt":"2026-11-05T10:00:00.000Z"}, "authorDetails":{"displayName":"Muted","channelId":"UC1"}}, {"id":"m2","snippet":{"type":"textMessageEvent","hasDisplayContent":true, "textMessageDetails":{"messageText":"visible"},"publishedAt":"2026-11-05T10:00:01.000Z"}, "authorDetails":{"displayName":"Viewer","channelId":"UC2"}} ] } """; var page = YouTubeChatService.ParsePage(json); Assert.Single(page.Messages); Assert.Equal("visible", page.Messages[0].Message); } [Fact] public void ChatModeEvents_AreNotChat() { const string json = """ { "items": [ {"id":"e1","snippet":{"type":"sponsorOnlyModeStartedEvent","hasDisplayContent":false, "publishedAt":"2026-11-05T10:00:00.000Z"}}, {"id":"e2","snippet":{"type":"chatEndedEvent","publishedAt":"2026-11-05T10:00:01.000Z"}} ] } """; Assert.Empty(YouTubeChatService.ParsePage(json).Messages); } [Fact] public void RealChatStillParses_WithAuthorAndSuperChat() { const string json = """ { "items": [ {"id":"m1","snippet":{"type":"superChatEvent","hasDisplayContent":true, "superChatDetails":{"amountMicros":12340000,"currency":"USD","amountDisplayString":"$12.34", "userComment":"nice stream"},"publishedAt":"2026-11-05T10:00:00.000Z"}, "authorDetails":{"displayName":"Funder","channelId":"UC9","isChatSponsor":true}} ] } """; var message = Assert.Single(YouTubeChatService.ParsePage(json).Messages); Assert.Equal("Funder", message.AuthorName); Assert.True(message.IsSuperChat); // Constraint that survives the TASK 49 retirement: reward fields are never reinterpreted. Assert.Equal("$12.34", message.SuperChatDisplayString); Assert.Equal("nice stream", message.Message); } }