Files
jellyfin-multilang/tests/Jellyfin.Plugin.Multilang.Tests/ItemsProxyRequestBuilderTests.cs
T
ajp_anton 096c6be8c7 Release 0.3.4: Jellyfin 12 stable support and audit fixes
Harden proxy permissions and cache behavior, streamline metadata fetching, remove obsolete rules, and validate the stable SDK with expanded regression coverage. Document backup format changes.
2026-09-09 01:18:43 +00:00

185 lines
6.7 KiB
C#

using Jellyfin.Plugin.Multilang.Services;
using Microsoft.AspNetCore.Http;
namespace Jellyfin.Plugin.Multilang.Tests;
public sealed class ItemsProxyRequestBuilderTests
{
private const string UserId = "11111111111111111111111111111111";
private const string OtherUserId = "22222222222222222222222222222222";
[Fact]
public void BuildNormalizesRootItemsPathToCurrentUser()
{
var request = HttpRequest();
var result = ItemsProxyRequestBuilder.Build(
request,
"/Items?Recursive=true&api_key=old",
UserId,
multilangEnabled: true);
Assert.NotNull(result);
Assert.Equal($"/Users/{UserId}/Items", result.Upstream.AbsolutePath);
Assert.DoesNotContain("api_key=old", result.Upstream.Query);
Assert.DoesNotContain("api_key", result.Upstream.Query, StringComparison.OrdinalIgnoreCase);
Assert.DoesNotContain("api_key", result.NormalizedUrlForCache, StringComparison.OrdinalIgnoreCase);
Assert.Contains("userId=" + UserId, result.NormalizedUrlForCache);
}
[Fact]
public void BuildReplacesForeignUserIdInUpstreamPath()
{
var request = HttpRequest();
var result = ItemsProxyRequestBuilder.Build(
request,
$"/Users/{OtherUserId}/Items?Limit=5",
UserId,
multilangEnabled: true);
Assert.NotNull(result);
Assert.Equal($"/Users/{UserId}/Items", result.Upstream.AbsolutePath);
}
[Fact]
public void BuildKeepsLocalListControlsOutOfUpstreamWhenEnabled()
{
var request = HttpRequest("?mlLocale=fi-FI");
var result = ItemsProxyRequestBuilder.Build(
request,
"/Items?SortBy=SortName&SortOrder=Descending&StartIndex=20&Limit=10&NameStartsWith=L",
UserId,
multilangEnabled: true);
Assert.NotNull(result);
Assert.Equal("SortName", result.Controls.SortBy);
Assert.Equal("Descending", result.Controls.SortOrder);
Assert.Equal(20, result.Controls.StartIndex);
Assert.Equal(10, result.Controls.Limit);
Assert.Equal("L", result.Controls.NameStartsWith);
Assert.Equal("fi-FI", result.Controls.ClientLocale);
Assert.DoesNotContain("SortBy=", result.Upstream.Query);
Assert.DoesNotContain("Limit=", result.Upstream.Query);
Assert.DoesNotContain("SortBy=", result.CacheKey);
}
[Fact]
public void BuildUsesOneUpstreamKeyForDifferentLocalPagingControls()
{
var request = HttpRequest();
var first = ItemsProxyRequestBuilder.Build(
request,
"/Items?SortBy=SortName&StartIndex=0&Limit=100",
UserId,
multilangEnabled: true);
var second = ItemsProxyRequestBuilder.Build(
request,
"/Items?SortBy=SortName&StartIndex=100&Limit=100",
UserId,
multilangEnabled: true);
Assert.NotNull(first);
Assert.NotNull(second);
Assert.Equal(first.NormalizedUrlForCache, second.NormalizedUrlForCache);
Assert.Equal(first.CacheKey, second.CacheKey);
}
[Fact]
public void BuildLeavesSortAndPagingUpstreamWhenMultilangDisabled()
{
var request = HttpRequest();
var result = ItemsProxyRequestBuilder.Build(
request,
"/Items?SortBy=SortName&Limit=10",
UserId,
multilangEnabled: false);
Assert.NotNull(result);
Assert.Equal(string.Empty, result.Controls.SortBy);
Assert.Equal(0, result.Controls.Limit);
Assert.Contains("SortBy=SortName", result.Upstream.Query);
Assert.Contains("Limit=10", result.Upstream.Query);
}
[Fact]
public void BuildRejectsAbsoluteUrlForAnotherHost()
{
var request = HttpRequest();
var result = ItemsProxyRequestBuilder.Build(
request,
"http://other-host:8096/Items",
UserId,
multilangEnabled: true);
Assert.Null(result);
}
[Fact]
public void ParseLocalGenreIdsOnlyReturnsTmdbIds()
{
Assert.Equal([1, 2], ItemsProxyRequestBuilder.ParseLocalGenreIds("tmdb-1,abc,2,tmdb-2,tmdb-x,1"));
}
[Fact]
public void GenreFilteringRetainsTheFieldsNeededForLocalFiltering()
{
var result = ItemsProxyRequestBuilder.Build(HttpRequest(), "/Items?GenreIds=tmdb-18&Fields=Overview", UserId, true);
Assert.NotNull(result);
var query = Microsoft.AspNetCore.WebUtilities.QueryHelpers.ParseQuery(result.Upstream.Query);
Assert.False(query.ContainsKey("GenreIds"));
Assert.Equal(1, query["Fields"].Count);
Assert.Contains("GenreItems", query["Fields"].ToString());
Assert.Contains("Overview", query["Fields"].ToString());
}
private static HttpRequest HttpRequest(string query = "")
{
var context = new DefaultHttpContext();
context.Request.Scheme = "http";
context.Request.Host = new HostString("server.local", 8096);
context.Request.QueryString = new QueryString(query);
return context.Request;
}
[Theory]
[InlineData("//other-host:8096/Items")]
[InlineData("/\\other-host/Items")]
[InlineData("https://server.local:8096/Items")]
[InlineData("/Multilang/ItemsProxy")]
[InlineData("/System/Configuration")]
[InlineData("/Items/Filters")]
[InlineData("/Items/../System/Info")]
public void RejectsUnsupportedDestinations(string url)
=> Assert.Null(ItemsProxyRequestBuilder.Build(HttpRequest(), url, UserId, true));
[Fact]
public void EquivalentQueryOrderSharesCacheAndInFlightKeys()
{
var first = ItemsProxyRequestBuilder.Build(HttpRequest(), "/Items?Fields=Overview&Recursive=true", UserId, true)!;
var second = ItemsProxyRequestBuilder.Build(HttpRequest(), "/Items?Recursive=true&Fields=Overview", UserId, true)!;
Assert.Equal(first.CacheKey, second.CacheKey);
Assert.Equal(first.NormalizedUrlForCache, second.NormalizedUrlForCache);
}
[Fact]
public void BasePathAndDashedUserIdsAreNormalized()
{
var request = HttpRequest();
request.PathBase = "/jellyfin";
var result = ItemsProxyRequestBuilder.Build(request, "/jellyfin/Users/22222222-2222-2222-2222-222222222222/Items", UserId, true)!;
Assert.Equal($"/jellyfin/Users/{UserId}/Items", result.Upstream.AbsolutePath);
Assert.Null(ItemsProxyRequestBuilder.Build(request, "/Items", UserId, true));
}
[Theory]
[InlineData("/Items/Resume?StartIndex=100&Limit=20")]
[InlineData("/Shows/NextUp?StartIndex=100&Limit=20")]
public void BoundedListsKeepUpstreamPaging(string url)
=> Assert.False(ItemsProxyRequestBuilder.Build(HttpRequest(), url, UserId, true)!.Controls.ApplyLocalPaging);
}