using System.Collections.Generic; using System.Linq; using System.Text.Json; using Jellyfin.Plugin.Floatplane.Api.Models; using Xunit; namespace Jellyfin.Plugin.Floatplane.Tests; public class ModelTests { [Fact] public void ContentCreator_list_posts_have_channel_object_but_no_attachments() { var posts = Fixture.Json>("content-creator.json"); Assert.Equal(5, posts.Count); var first = posts[0]; Assert.Equal("SSvhppI28t", first.Id); Assert.Equal("How Board Games Come ALIVE...- Board", first.Title); Assert.NotNull(first.Channel); Assert.False(first.Channel!.IsReferenceOnly); Assert.Equal("ShortCircuit", first.Channel.Title); Assert.Equal("64135da7ce81077a8480c679", first.Channel.Id); Assert.Null(first.VideoAttachments); Assert.True(first.HasVideo); Assert.Equal(1104, first.DurationSeconds); Assert.NotNull(first.ReleaseDate); Assert.Equal(2026, first.ReleaseDate!.Value.Year); Assert.True(first.IsAccessible); } [Fact] public void ContentPost_expands_video_attachments() { var post = Fixture.Json("content-post.json"); Assert.Equal("SSvhppI28t", post.Id); Assert.NotNull(post.VideoAttachments); var video = Assert.Single(post.VideoAttachments!); Assert.False(video.IsReferenceOnly); Assert.Equal("dcm2jUIg1J", video.Id); Assert.Equal(1104, video.Duration); Assert.Equal(5, video.Levels.Count); Assert.Contains(video.Levels, l => l.Name == "1080" && l.Height == 1080 && l.Width == 1920); Assert.Single(video.TextTracks); Assert.Equal(new[] { "dcm2jUIg1J" }, post.AttachmentOrder); Assert.Equal("ShortCircuit", post.Channel!.Title); Assert.Equal("LinusTechTips", post.Creator!.Title); } [Fact] public void ContentVideo_parses_levels_and_sprite() { var video = Fixture.Json("content-video.json"); Assert.Equal("dcm2jUIg1J", video.Id); Assert.Equal("SSvhppI28t", video.PrimaryBlogPost); Assert.Equal(new[] { "360", "480", "720", "1080", "2160" }, video.Levels.Select(l => l.Name)); Assert.NotNull(video.TimelineSprite); Assert.StartsWith("https://pbs.floatplane.com/timeline_sprite/", video.TimelineSprite!.Path); } [Fact] public void IdOrObjectConverter_accepts_string_ids() { const string json = """ {"id":"p1","title":"T","channel":"chan123","creator":"cre456","videoAttachments":["v1","v2"],"metadata":{"hasVideo":true,"videoCount":2}} """; var post = JsonSerializer.Deserialize(json, FloatplaneJson.Options)!; Assert.Equal("chan123", post.Channel!.Id); Assert.True(post.Channel.IsReferenceOnly); Assert.Equal("cre456", post.Creator!.Id); Assert.True(post.Creator.IsReferenceOnly); Assert.Equal(2, post.VideoAttachments!.Count); Assert.All(post.VideoAttachments, v => Assert.True(v.IsReferenceOnly)); Assert.Equal(new[] { "v1", "v2" }, post.VideoAttachments.Select(v => v.Id)); } [Fact] public void IdOrObjectConverter_still_reads_objects_and_null() { const string json = """ {"id":"p1","channel":{"id":"c1","title":"Main","urlname":"main"},"creator":null} """; var post = JsonSerializer.Deserialize(json, FloatplaneJson.Options)!; Assert.Equal("Main", post.Channel!.Title); Assert.False(post.Channel.IsReferenceOnly); Assert.Null(post.Creator); } [Fact] public void Error_envelope_parses() { var error = Fixture.Json("error-not-logged-in.json"); Assert.True(error.Has("notLoggedInError")); Assert.False(error.Has("notSubscribedError")); Assert.Equal("You must be logged-in to access this resource.", error.Message); } [Fact] public void DeliveryInfo_resolves_usable_variants_and_reports_denied_4k() { var info = Fixture.Json("delivery-info.json"); var resolved = info.Resolve().ToList(); Assert.Equal(4, resolved.Count); Assert.All(resolved, v => Assert.True(v.IsUsable)); Assert.All(resolved, v => Assert.StartsWith("https://cdn-vod-drm2.floatplane.com/Videos/dcm2jUIg1J/", v.Url)); Assert.Equal(new[] { 360, 480, 720, 1080 }, resolved.Select(v => v.Height)); Assert.Equal("avc1.640028", resolved.Single(v => v.Height == 1080).Variant.Meta!.Video!.Codec); var fourK = info.Groups.Single().Variants.Single(v => v.Name == "2160-avc1"); Assert.False(fourK.IsUsable); Assert.False(fourK.Enabled); Assert.Equal("isMissingPermission", fourK.Meta!.Common!.Access!.DeniedReason); Assert.Equal(2160, fourK.Height); } [Fact] public void ImageModel_BestPath_picks_largest_child_within_limit() { var post = Fixture.Json>("content-creator.json")[0]; var path = post.Thumbnail!.BestPath(1200); Assert.EndsWith("_1200x675.jpeg", path); Assert.EndsWith("_400x225.jpeg", post.Thumbnail.BestPath(500)); Assert.Equal(post.Thumbnail.Path, post.Thumbnail.BestPath(100)); } [Fact] public void Subscriptions_and_self_and_creator_parse() { var subs = Fixture.Json>("user-subscriptions.json"); var self = Fixture.Json("user-self.json"); var creator = Fixture.Json("creator-info.json"); var sub = Assert.Single(subs); Assert.Equal("59f94c0bdd241b70349eb72b", sub.Creator); Assert.Equal("LTT Supporter", sub.Plan!.Title); Assert.False(string.IsNullOrEmpty(self.Username)); Assert.Equal("LinusTechTips", creator.Title); Assert.Equal("linustechtips", creator.Urlname); Assert.Equal(11, creator.Channels.Count); Assert.Contains(creator.Channels, c => c.Urlname == "techlinked"); Assert.NotNull(creator.LiveStream); } }