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.
185 lines
6.7 KiB
C#
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);
|
|
}
|