Tests: xUnit project with HAR fixtures (50 tests); fix relative delivery/HLS URLs resolving to file:// on Linux
All checks were successful
release / build-release (push) Successful in 1m19s
All checks were successful
release / build-release (push) Successful in 1m19s
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
206
Jellyfin.Plugin.Floatplane.Tests/CatalogServiceTests.cs
Normal file
206
Jellyfin.Plugin.Floatplane.Tests/CatalogServiceTests.cs
Normal file
@@ -0,0 +1,206 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Plugin.Floatplane.Api;
|
||||
using Jellyfin.Plugin.Floatplane.Api.Models;
|
||||
using Jellyfin.Plugin.Floatplane.Channel;
|
||||
using Jellyfin.Plugin.Floatplane.Configuration;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Xunit;
|
||||
|
||||
namespace Jellyfin.Plugin.Floatplane.Tests;
|
||||
|
||||
public class CatalogServiceTests
|
||||
{
|
||||
/// <summary>Builds <paramref name="count"/> distinct posts from the 5 fixture posts.</summary>
|
||||
private static List<PostModel> MakePosts(int count, int startIndex = 0, Func<int, bool>? locked = null)
|
||||
{
|
||||
var template = Fixture.Json<List<PostModel>>("content-creator.json");
|
||||
var result = new List<PostModel>();
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
var n = startIndex + i;
|
||||
var p = Fixture.Json<List<PostModel>>("content-creator.json")[n % template.Count];
|
||||
p.Id = $"post{n:D3}";
|
||||
p.Guid = p.Id;
|
||||
p.IsAccessible = locked == null || !locked(n);
|
||||
result.Add(p);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static string Json(List<PostModel> posts) => JsonSerializer.Serialize(posts, FloatplaneJson.Options);
|
||||
|
||||
private static (CatalogService Catalog, FakeHttpHandler Handler) Build(PluginConfiguration config, Func<int, List<PostModel>> pageForOffset)
|
||||
{
|
||||
var handler = new FakeHttpHandler().On("/api/v3/content/creator", r =>
|
||||
{
|
||||
var query = r.RequestUri!.Query.TrimStart('?').Split('&').Select(p => p.Split('=', 2)).ToDictionary(p => p[0], p => Uri.UnescapeDataString(p[1]));
|
||||
var offset = int.Parse(query["fetchAfter"], System.Globalization.CultureInfo.InvariantCulture);
|
||||
return FakeHttpHandler.Json(Json(pageForOffset(offset)));
|
||||
});
|
||||
var api = new FloatplaneApiClient(new FakeHttpClientFactory(handler), NullLogger<FloatplaneApiClient>.Instance, () => config);
|
||||
var catalog = new CatalogService(api, NullLogger<CatalogService>.Instance, () => config);
|
||||
return (catalog, handler);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FetchPosts_pages_until_short_page()
|
||||
{
|
||||
var (catalog, handler) = Build(
|
||||
new PluginConfiguration(),
|
||||
offset => offset switch
|
||||
{
|
||||
0 => MakePosts(20, 0),
|
||||
20 => MakePosts(5, 20),
|
||||
_ => new List<PostModel>()
|
||||
});
|
||||
|
||||
var posts = await catalog.FetchPostsAsync("cre1", string.Empty, 100, CancellationToken.None);
|
||||
|
||||
Assert.Equal(25, posts.Count);
|
||||
Assert.Equal(25, posts.Select(p => p.Id).Distinct().Count());
|
||||
Assert.Equal(2, handler.Requests.Count);
|
||||
Assert.Contains("fetchAfter=0", handler.Requests[0].RequestUri!.Query);
|
||||
Assert.Contains("fetchAfter=20", handler.Requests[1].RequestUri!.Query);
|
||||
Assert.All(handler.Requests, r => Assert.Contains("hasVideo=true", r.RequestUri!.Query));
|
||||
Assert.All(handler.Requests, r => Assert.DoesNotContain("channel=", r.RequestUri!.Query));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FetchPosts_stops_on_empty_page()
|
||||
{
|
||||
var (catalog, handler) = Build(
|
||||
new PluginConfiguration(),
|
||||
offset => offset == 0 ? MakePosts(20, 0) : new List<PostModel>());
|
||||
|
||||
var posts = await catalog.FetchPostsAsync("cre1", "chan1", 100, CancellationToken.None);
|
||||
|
||||
Assert.Equal(20, posts.Count);
|
||||
Assert.Equal(2, handler.Requests.Count);
|
||||
Assert.All(handler.Requests, r => Assert.Contains("channel=chan1", r.RequestUri!.Query));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FetchPosts_respects_max()
|
||||
{
|
||||
var (catalog, handler) = Build(
|
||||
new PluginConfiguration(),
|
||||
offset => MakePosts(20, offset));
|
||||
|
||||
var posts = await catalog.FetchPostsAsync("cre1", string.Empty, 30, CancellationToken.None);
|
||||
|
||||
Assert.Equal(30, posts.Count);
|
||||
Assert.Equal(2, handler.Requests.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FetchPosts_filters_locked_posts_unless_configured()
|
||||
{
|
||||
// Without a session Floatplane marks everything inaccessible, so the filter only applies when logged in.
|
||||
var config = new PluginConfiguration { ShowLockedPosts = false, SessionCookie = "sid" };
|
||||
var (catalog, _) = Build(config, offset => offset == 0 ? MakePosts(10, 0, n => n % 2 == 1) : new List<PostModel>());
|
||||
|
||||
var visible = await catalog.FetchPostsAsync("cre1", string.Empty, 100, CancellationToken.None);
|
||||
Assert.Equal(5, visible.Count);
|
||||
Assert.All(visible, p => Assert.True(p.IsAccessible));
|
||||
|
||||
config.ShowLockedPosts = true;
|
||||
var all = await catalog.FetchPostsAsync("cre1", string.Empty, 100, CancellationToken.None);
|
||||
Assert.Equal(10, all.Count);
|
||||
Assert.Equal(5, all.Count(p => !p.IsAccessible));
|
||||
|
||||
// No session: locked posts are always listed (browsing is public; Floatplane reports everything locked).
|
||||
config.ShowLockedPosts = false;
|
||||
config.SessionCookie = string.Empty;
|
||||
var anonymous = await catalog.FetchPostsAsync("cre1", string.Empty, 100, CancellationToken.None);
|
||||
Assert.Equal(10, anonymous.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FetchPosts_passes_search_and_skips_posts_without_video()
|
||||
{
|
||||
var pages = MakePosts(3, 0);
|
||||
pages[1].Metadata!.HasVideo = false;
|
||||
var (catalog, handler) = Build(new PluginConfiguration(), offset => offset == 0 ? pages : new List<PostModel>());
|
||||
|
||||
var posts = await catalog.FetchPostsAsync("cre1", string.Empty, 100, CancellationToken.None, "framework");
|
||||
|
||||
Assert.Equal(2, posts.Count);
|
||||
Assert.Contains("search=framework", handler.Requests[0].RequestUri!.Query);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetPosts_caches_and_Invalidate_bumps_generation()
|
||||
{
|
||||
var (catalog, handler) = Build(new PluginConfiguration { CatalogCacheMinutes = 30, MaxPostsPerChannel = 20 }, offset => offset == 0 ? MakePosts(5, 0) : new List<PostModel>());
|
||||
|
||||
var first = await catalog.GetPostsAsync("cre1", "chan1", CancellationToken.None);
|
||||
var second = await catalog.GetPostsAsync("cre1", "chan1", CancellationToken.None);
|
||||
|
||||
Assert.Same(first, second);
|
||||
Assert.Single(handler.Requests);
|
||||
Assert.Equal(1, catalog.CachedPostListCount);
|
||||
Assert.NotNull(catalog.FindCachedPost("post002"));
|
||||
Assert.Null(catalog.FindCachedPost("nope"));
|
||||
|
||||
var generation = catalog.Generation;
|
||||
catalog.Invalidate();
|
||||
|
||||
Assert.Equal(generation + 1, catalog.Generation);
|
||||
Assert.Equal(0, catalog.CachedPostListCount);
|
||||
await catalog.GetPostsAsync("cre1", "chan1", CancellationToken.None);
|
||||
Assert.Equal(2, handler.Requests.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetCreators_without_session_resolves_extra_creators_publicly()
|
||||
{
|
||||
var config = new PluginConfiguration { ExtraCreators = "linustechtips, , bitwit" };
|
||||
var handler = new FakeHttpHandler()
|
||||
.On("/api/v3/creator/named", _ => FakeHttpHandler.Json("[" + Fixture.Text("creator-info.json") + "]"));
|
||||
var api = new FloatplaneApiClient(new FakeHttpClientFactory(handler), NullLogger<FloatplaneApiClient>.Instance, () => config);
|
||||
var catalog = new CatalogService(api, NullLogger<CatalogService>.Instance, () => config);
|
||||
|
||||
var creators = await catalog.GetCreatorsAsync(false, CancellationToken.None);
|
||||
|
||||
var creator = Assert.Single(creators);
|
||||
Assert.Equal("LinusTechTips", creator.Title);
|
||||
Assert.Equal(11, creator.Channels.Count);
|
||||
Assert.Equal("none", catalog.SessionState);
|
||||
Assert.Contains("creatorURL%5B0%5D=linustechtips&creatorURL%5B1%5D=bitwit", Assert.Single(handler.Requests).RequestUri!.Query);
|
||||
Assert.Same(creator, catalog.FindCreator(creator.Id));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetCreators_with_session_uses_subscriptions_and_marks_invalid_cookie()
|
||||
{
|
||||
var config = new PluginConfiguration { SessionCookie = "sid" };
|
||||
var handler = new FakeHttpHandler()
|
||||
.On("/api/v3/user/subscriptions", _ => FakeHttpHandler.Json(Fixture.Text("user-subscriptions.json")))
|
||||
.On("/api/v3/creator/info", _ => FakeHttpHandler.Json(Fixture.Text("creator-info.json")));
|
||||
var api = new FloatplaneApiClient(new FakeHttpClientFactory(handler), NullLogger<FloatplaneApiClient>.Instance, () => config);
|
||||
var catalog = new CatalogService(api, NullLogger<CatalogService>.Instance, () => config);
|
||||
|
||||
var creators = await catalog.GetCreatorsAsync(false, CancellationToken.None);
|
||||
|
||||
Assert.Single(creators);
|
||||
Assert.Equal("ok", catalog.SessionState);
|
||||
Assert.Contains("id=59f94c0bdd241b70349eb72b", handler.Requests[1].RequestUri!.Query);
|
||||
|
||||
var rejected = new FakeHttpHandler()
|
||||
.On("/api/v3/user/subscriptions", _ => FakeHttpHandler.Json(Fixture.Text("error-not-logged-in.json"), System.Net.HttpStatusCode.Unauthorized));
|
||||
var api2 = new FloatplaneApiClient(new FakeHttpClientFactory(rejected), NullLogger<FloatplaneApiClient>.Instance, () => config);
|
||||
var catalog2 = new CatalogService(api2, NullLogger<CatalogService>.Instance, () => config);
|
||||
|
||||
var none = await catalog2.GetCreatorsAsync(true, CancellationToken.None);
|
||||
|
||||
Assert.Empty(none);
|
||||
Assert.Equal("invalid", catalog2.SessionState);
|
||||
Assert.NotNull(catalog2.LastError);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user