using System.IO; using System.Threading; using System.Threading.Tasks; using Microsoft.Data.Sqlite; using Xunit; using ytLive.Models; using ytLive.Services; using ytLive.ViewModels; namespace ytLive.Tests; public class SocialBarTests { private static string TempDbPath() { var path = Path.Combine(Path.GetTempPath(), $"ytLlive-social-test-{System.Guid.NewGuid():N}.db"); SqliteConnection.ClearAllPools(); if (File.Exists(path)) File.Delete(path); return path; } /// Resolves every fediverse domain as Mastodon — the heal test only /// cares that a missing stored software name gets filled in and persisted. private sealed class MastodonValidator : ISocialValidator { public Task ResolveFediverseSoftwareAsync(string domain, CancellationToken ct) => Task.FromResult("mastodon"); public Task LookupAsync(SocialService service, string handleOrUrl, CancellationToken ct) => throw new System.NotSupportedException(); } [Fact] public void SocialEntry_FediverseSoftware_SettableUpdatesLogo() { var entry = new SocialEntry { Service = SocialService.Fediverse, Handle = "@gramps@llamachile.tube", ProfileUrl = "https://llamachile.tube/@gramps", }; var honeycomb = entry.LogoData; entry.FediverseSoftware = "mastodon"; Assert.Equal(SocialServiceIcons.LogoDataForFediverse("mastodon"), entry.LogoData); Assert.NotEqual(honeycomb, entry.LogoData); } /// The single integration test for this branch: a fediverse entry /// persisted with a NULL software name is loaded, healed via the real validator /// seam, and the resolved name is persisted back — surviving a second load. [Fact] public async Task Socials_HealMissingFediverseSoftware_RoundTripsThroughDb() { var path = TempDbPath(); try { var socials = new SocialsConfig(); socials.Entries.Add(new SocialEntry { Service = SocialService.Fediverse, Handle = "@gramps@llamachile.tube", ProfileUrl = "https://llamachile.tube/@gramps", }); using (var store = new LayoutStore(path)) store.Save(new[] { new Scene { Name = "Live" } }, null, socials); SocialsConfig? loaded; using (var store = new LayoutStore(path)) { store.Load(); loaded = store.Socials; Assert.NotNull(loaded); Assert.Null(loaded!.Entries[0].FediverseSoftware); } var healed = await MainViewModel.HealFediverseSoftwareAsync(loaded!, new MastodonValidator()); Assert.Single(healed); Assert.Equal("mastodon", healed["@gramps@llamachile.tube"]); loaded.Entries[0].FediverseSoftware = healed["@gramps@llamachile.tube"]; using (var store = new LayoutStore(path)) store.Save(new[] { new Scene { Name = "Live" } }, null, loaded); using (var store = new LayoutStore(path)) { store.Load(); var again = store.Socials; Assert.NotNull(again); Assert.Equal("mastodon", again!.Entries[0].FediverseSoftware); } } finally { SqliteConnection.ClearAllPools(); if (File.Exists(path)) File.Delete(path); } } [Fact] public void SocialsConfig_RoundTrip_PersistsEntriesAndBarSettings() { var path = TempDbPath(); try { var socials = new SocialsConfig { BarPosition = SocialBarPosition.Top, BarEnabled = false, }; socials.Entries.Add(new SocialEntry { Service = SocialService.YouTube, Handle = "mychannel", ProfileUrl = "https://www.youtube.com/@mychannel", }); socials.Entries.Add(new SocialEntry { Service = SocialService.Twitch, Handle = "streamer123", ProfileUrl = "https://www.twitch.tv/streamer123", }); socials.Entries.Add(new SocialEntry { Service = SocialService.Fediverse, Handle = "@gramps@llamachile.tube", ProfileUrl = "https://llamachile.tube/@gramps", FediverseSoftware = "peertube", }); using (var store = new LayoutStore(path)) { var scene = new Scene { Name = "Live" }; scene.HasSocialBar = true; store.Save(new[] { scene }, null, socials); } using (var store = new LayoutStore(path)) { var scenes = store.Load(); Assert.NotNull(store.Socials); Assert.Equal(3, store.Socials!.Entries.Count); Assert.Equal(SocialService.YouTube, store.Socials.Entries[0].Service); Assert.Equal("mychannel", store.Socials.Entries[0].Handle); Assert.Equal("https://www.youtube.com/@mychannel", store.Socials.Entries[0].ProfileUrl); Assert.Equal(SocialService.Twitch, store.Socials.Entries[1].Service); Assert.Equal(SocialService.Fediverse, store.Socials.Entries[2].Service); Assert.Equal("@gramps@llamachile.tube", store.Socials.Entries[2].Handle); Assert.Equal("peertube", store.Socials.Entries[2].FediverseSoftware); Assert.Equal(SocialBarPosition.Top, store.Socials.BarPosition); Assert.False(store.Socials.BarEnabled); Assert.True(scenes[0].HasSocialBar); } } finally { SqliteConnection.ClearAllPools(); if (File.Exists(path)) File.Delete(path); } } [Fact] public void SocialsConfig_EmptyEntries_NotPersisted() { var path = TempDbPath(); try { var socials = new SocialsConfig(); using (var store = new LayoutStore(path)) { store.Save(new[] { new Scene { Name = "Live" } }, null, socials); } using (var store = new LayoutStore(path)) { Assert.Null(store.Socials); } } finally { SqliteConnection.ClearAllPools(); if (File.Exists(path)) File.Delete(path); } } [Fact] public void SocialsConfig_DefaultBarEnabled_IsOn() { Assert.True(new SocialsConfig().BarEnabled); } [Fact] public void SocialServiceIcons_CanonicalUrlFor_AllServices() { Assert.Equal("https://www.youtube.com/@test", SocialServiceIcons.CanonicalUrlFor(SocialService.YouTube, "test")); Assert.Equal("https://www.youtube.com/@test", SocialServiceIcons.CanonicalUrlFor(SocialService.YouTube, "@test")); Assert.Equal("https://x.com/user", SocialServiceIcons.CanonicalUrlFor(SocialService.X, "user")); Assert.Equal("https://github.com/dev", SocialServiceIcons.CanonicalUrlFor(SocialService.GitHub, "dev")); Assert.Equal("https://example.com", SocialServiceIcons.CanonicalUrlFor(SocialService.Website, "example.com")); Assert.Equal("https://example.com", SocialServiceIcons.CanonicalUrlFor(SocialService.Website, "https://example.com")); Assert.Equal("https://site.tld/@user", SocialServiceIcons.CanonicalUrlFor(SocialService.Link, "@user@site.tld")); Assert.Equal("https://site.tld/@user", SocialServiceIcons.CanonicalUrlFor(SocialService.Fediverse, "@user@site.tld")); } [Fact] public void SocialServiceIcons_LogoData_AllServices() { foreach (var service in Enum.GetValues()) Assert.False(string.IsNullOrWhiteSpace(SocialServiceIcons.LogoDataFor(service)), service.ToString()); Assert.NotEmpty(SocialServiceIcons.LockedIconData); Assert.NotEmpty(SocialServiceIcons.DoNotIconData); Assert.NotEqual(SocialServiceIcons.LogoDataFor(SocialService.YouTube), SocialServiceIcons.LogoDataFor(SocialService.Twitch)); } [Fact] public void SocialServiceIcons_LogoDataForFediverse_KnownAndUnknownSoftware() { Assert.Equal(SocialServiceIcons.LogoDataForFediverse("peertube"), SocialServiceIcons.LogoDataForFediverse("PeerTube")); Assert.Equal(SocialServiceIcons.LogoDataForFediverse("mastodon"), SocialServiceIcons.LogoDataForFediverse("MASTODON")); Assert.NotEqual( SocialServiceIcons.LogoDataForFediverse("peertube"), SocialServiceIcons.LogoDataForFediverse("mastodon")); Assert.Equal(SocialServiceIcons.LogoDataForFediverse(null), SocialServiceIcons.LogoDataForFediverse("gotosocial")); Assert.NotEqual(SocialServiceIcons.LogoDataForFediverse("peertube"), SocialServiceIcons.LogoDataForFediverse("unknown")); } [Fact] public void SocialEntry_FediverseLogo_ComesFromSoftwareName() { var entry = new SocialEntry { Service = SocialService.Fediverse, Handle = "@gramps@llamachile.tube", ProfileUrl = "https://llamachile.tube/@gramps", FediverseSoftware = "peertube", }; Assert.Equal(SocialServiceIcons.LogoDataForFediverse("peertube"), entry.LogoData); Assert.NotEqual(SocialServiceIcons.LogoDataFor(SocialService.Link), entry.LogoData); } [Fact] public void SocialServiceIcons_DetectService_ByDomain() { Assert.Equal(SocialService.YouTube, SocialServiceIcons.DetectService("youtube.com/@handle").service); Assert.Equal(SocialService.X, SocialServiceIcons.DetectService("https://x.com/user").service); Assert.Equal(SocialService.Instagram, SocialServiceIcons.DetectService("instagram.com/user").service); var fediverse = SocialServiceIcons.DetectService("@user@instance.tube"); Assert.Equal(SocialService.Fediverse, fediverse.service); Assert.Equal("@user@instance.tube", fediverse.handle); Assert.Equal("https://instance.tube/@user", fediverse.url); Assert.Equal(SocialService.GitHub, SocialServiceIcons.DetectService("https://github.com/dev").service); Assert.Equal(SocialService.Website, SocialServiceIcons.DetectService("example.com/path").service); } [Fact] public void SocialServiceIcons_DetectService_BareHandle_FallsBackToWebsite() { Assert.Equal(SocialService.Website, SocialServiceIcons.DetectService("justaname").service); } }