aboutsummaryrefslogtreecommitdiff
path: root/tests/Jellyfin.Server.Implementations.Tests/Library
diff options
context:
space:
mode:
authorShadowghost <Shadowghost@users.noreply.github.com>2026-09-15 11:13:48 -0400
committerCody Robibero <cody@robibe.ro>2026-09-15 11:13:48 -0400
commit8150ee2484dabed61718ef43d5c98cdd2c0fa01c (patch)
treecb81b848e8931122d4771de780746be51a0e1d94 /tests/Jellyfin.Server.Implementations.Tests/Library
parent7baca5f2b891cbc92e48145e633e1417d9bf2de3 (diff)
Backport pull request #17842 from jellyfin/release-12.z
Fix versions of a video still listing separately from their group and preserve manual merges Original-merge: cabec7ec28df671e3bb1c360c32db60b085f4084 Merged-by: crobibero <cody@robibe.ro> Backported-by: Cody Robibero <cody@robibe.ro>
Diffstat (limited to 'tests/Jellyfin.Server.Implementations.Tests/Library')
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Library/MovieSimilarItemsProviderTests.cs104
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Library/SqlSearchProviderTests.cs126
2 files changed, 224 insertions, 6 deletions
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Library/MovieSimilarItemsProviderTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Library/MovieSimilarItemsProviderTests.cs
index 297f930205..421671b520 100644
--- a/tests/Jellyfin.Server.Implementations.Tests/Library/MovieSimilarItemsProviderTests.cs
+++ b/tests/Jellyfin.Server.Implementations.Tests/Library/MovieSimilarItemsProviderTests.cs
@@ -10,6 +10,7 @@ using Jellyfin.Database.Implementations.Entities;
using Jellyfin.Server.Implementations.Tests.Item;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Dto;
+using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Movies;
using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Configuration;
@@ -26,9 +27,14 @@ namespace Jellyfin.Server.Implementations.Tests.Library;
/// </summary>
public sealed class MovieSimilarItemsProviderTests : SqliteDbTestFixture
{
+ private static readonly Guid _movieLibraryId = Guid.Parse("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa");
+ private static readonly Guid _movie4KLibraryId = Guid.Parse("bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb");
+
private readonly MovieSimilarItemsProvider _provider;
+ private readonly Mock<ILibraryManager> _libraryManager = new();
private readonly User _user = new("test", "auth-provider", "reset-provider");
private readonly string _movieTypeName;
+ private readonly string _folderTypeName;
private readonly Guid _source = Guid.NewGuid();
private readonly Guid _sourceAlternate = Guid.NewGuid();
@@ -36,10 +42,19 @@ public sealed class MovieSimilarItemsProviderTests : SqliteDbTestFixture
private readonly Guid _similarAlternate = Guid.NewGuid();
private readonly Guid _unrelated = Guid.NewGuid();
+ // A second scenario, in two libraries and on a genre of its own, for the group whose primary the
+ // user may not be able to reach at all.
+ private readonly Guid _crossSource = Guid.NewGuid();
+ private readonly Guid _crossLibraryPrimary = Guid.NewGuid();
+ private readonly Guid _crossLibraryVersion = Guid.NewGuid();
+ private readonly Guid _sameLibraryPrimary = Guid.NewGuid();
+ private readonly Guid _sameLibraryVersion = Guid.NewGuid();
+
public MovieSimilarItemsProviderTests()
{
var itemTypeLookup = new ItemTypeLookup();
_movieTypeName = itemTypeLookup.BaseItemKindNames[BaseItemKind.Movie]!;
+ _folderTypeName = itemTypeLookup.BaseItemKindNames[BaseItemKind.Folder]!;
using (var context = CreateDbContext())
{
@@ -53,7 +68,7 @@ public sealed class MovieSimilarItemsProviderTests : SqliteDbTestFixture
CreateDbContextFactory(),
CreateBaseItemRepository(itemTypeLookup),
serverConfigurationManager.Object,
- new Mock<ILibraryManager>().Object);
+ _libraryManager.Object);
}
[Fact]
@@ -80,10 +95,52 @@ public sealed class MovieSimilarItemsProviderTests : SqliteDbTestFixture
Assert.DoesNotContain(_sourceAlternate, items);
}
- private async Task<List<Guid>> GetSimilarItemsAsync()
+ [Fact]
+ public async Task GetSimilarItems_UserWithoutThePrimarysLibrary_OffersTheVersion()
+ {
+ // The user may only open the library the 1080p version is in, so its primary is out of reach
+ // and the version is all that is left to stand in for the group.
+ RestrictUserTo(_movieLibraryId);
+
+ var items = await GetSimilarItemsAsync(_crossSource).ConfigureAwait(true);
+
+ Assert.Contains(_crossLibraryVersion, items);
+ Assert.DoesNotContain(_crossLibraryPrimary, items);
+ }
+
+ [Fact]
+ public async Task GetSimilarItems_UserWithBothLibraries_OffersThePrimaryOfTheGroupOnce()
+ {
+ RestrictUserTo(_movieLibraryId, _movie4KLibraryId);
+
+ var items = await GetSimilarItemsAsync(_crossSource).ConfigureAwait(true);
+
+ Assert.Contains(_crossLibraryPrimary, items);
+ Assert.DoesNotContain(_crossLibraryVersion, items);
+ }
+
+ [Fact]
+ public async Task GetSimilarItems_GroupMergedInsideOneLibrary_StillOffersOnlyThePrimary()
+ {
+ RestrictUserTo(_movieLibraryId, _movie4KLibraryId);
+
+ var items = await GetSimilarItemsAsync(_crossSource).ConfigureAwait(true);
+
+ Assert.Contains(_sameLibraryPrimary, items);
+ Assert.DoesNotContain(_sameLibraryVersion, items);
+ }
+
+ private void RestrictUserTo(params Guid[] libraryIds)
+ {
+ _libraryManager
+ .Setup(l => l.ConfigureUserAccess(It.IsAny<InternalItemsQuery>(), It.IsAny<User>()))
+ .Callback<InternalItemsQuery, User>((query, _) => query.TopParentIds = libraryIds);
+ }
+
+ private async Task<List<Guid>> GetSimilarItemsAsync(Guid? sourceId = null)
{
var results = await _provider.GetSimilarItemsAsync(
- new Movie { Id = _source, Name = "Source" },
+ new Movie { Id = sourceId ?? _source, Name = "Source" },
new SimilarItemsQuery { User = _user, Limit = 10, DtoOptions = new DtoOptions() },
CancellationToken.None).ConfigureAwait(false);
@@ -102,19 +159,52 @@ public sealed class MovieSimilarItemsProviderTests : SqliteDbTestFixture
var similarAlternate = AddMovie(context, _similarAlternate, "Similar 4K", primaryVersionId: _similar);
var unrelated = AddMovie(context, _unrelated, "Unrelated", primaryVersionId: null);
+ // The second scenario scores on a genre of its own, so it stays out of the results above.
+ var crossLibrary = CreateItemValue("Science Fiction", "science fiction");
+
+ AddLibrary(context, _movieLibraryId, "Movies");
+ AddLibrary(context, _movie4KLibraryId, "Movies-4K");
+
+ var crossSource = AddMovie(context, _crossSource, "Cross Source", primaryVersionId: null, libraryId: _movieLibraryId);
+
+ // The 4K version heads the group and lives in a library of its own.
+ var crossLibraryPrimary = AddMovie(context, _crossLibraryPrimary, "Coco 4K", primaryVersionId: null, libraryId: _movie4KLibraryId);
+ var crossLibraryVersion = AddMovie(context, _crossLibraryVersion, "Coco", primaryVersionId: _crossLibraryPrimary, libraryId: _movieLibraryId);
+
+ // A group merged inside one library, as a control.
+ var sameLibraryPrimary = AddMovie(context, _sameLibraryPrimary, "Up 4K", primaryVersionId: null, libraryId: _movieLibraryId);
+ var sameLibraryVersion = AddMovie(context, _sameLibraryVersion, "Up", primaryVersionId: _sameLibraryPrimary, libraryId: _movieLibraryId);
+
context.Users.Add(_user);
- context.ItemValues.AddRange(shared, other);
+ context.ItemValues.AddRange(shared, other, crossLibrary);
context.ItemValuesMap.AddRange(
CreateMap(source, shared),
CreateMap(sourceAlternate, shared),
CreateMap(similar, shared),
CreateMap(similarAlternate, shared),
- CreateMap(unrelated, other));
+ CreateMap(unrelated, other),
+ CreateMap(crossSource, crossLibrary),
+ CreateMap(crossLibraryPrimary, crossLibrary),
+ CreateMap(crossLibraryVersion, crossLibrary),
+ CreateMap(sameLibraryPrimary, crossLibrary),
+ CreateMap(sameLibraryVersion, crossLibrary));
context.SaveChanges();
}
- private BaseItemEntity AddMovie(JellyfinDbContext context, Guid id, string name, Guid? primaryVersionId)
+ private void AddLibrary(JellyfinDbContext context, Guid id, string name)
+ {
+ context.BaseItems.Add(new BaseItemEntity
+ {
+ Id = id,
+ Type = _folderTypeName,
+ Name = name,
+ Path = "/" + name,
+ IsFolder = true
+ });
+ }
+
+ private BaseItemEntity AddMovie(JellyfinDbContext context, Guid id, string name, Guid? primaryVersionId, Guid? libraryId = null)
{
var item = new BaseItemEntity
{
@@ -122,6 +212,8 @@ public sealed class MovieSimilarItemsProviderTests : SqliteDbTestFixture
Type = _movieTypeName,
Name = name,
SortName = name,
+ ParentId = libraryId,
+ TopParentId = libraryId,
MediaType = "Video",
IsMovie = true,
IsFolder = false,
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Library/SqlSearchProviderTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Library/SqlSearchProviderTests.cs
new file mode 100644
index 0000000000..5aa770b9b3
--- /dev/null
+++ b/tests/Jellyfin.Server.Implementations.Tests/Library/SqlSearchProviderTests.cs
@@ -0,0 +1,126 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using Emby.Server.Implementations.Data;
+using Emby.Server.Implementations.Library.Search;
+using Jellyfin.Database.Implementations;
+using Jellyfin.Database.Implementations.Entities;
+using Jellyfin.Server.Implementations.Item;
+using Jellyfin.Server.Implementations.Tests.Item;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Library;
+using Moq;
+using Xunit;
+using BaseItemKind = Jellyfin.Data.Enums.BaseItemKind;
+
+namespace Jellyfin.Server.Implementations.Tests.Library;
+
+/// <summary>
+/// Covers what <see cref="SqlSearchProvider"/> returns for a version group merged across two
+/// libraries: the primary represents the group wherever it is visible, and the version stands in
+/// for it for a user who cannot open the library the primary lives in.
+/// </summary>
+public sealed class SqlSearchProviderTests : SqliteDbTestFixture
+{
+ private static readonly Guid _movieLibraryId = Guid.Parse("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa");
+ private static readonly Guid _movie4KLibraryId = Guid.Parse("bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb");
+ private static readonly Guid _primaryId = Guid.Parse("cccccccc-cccc-cccc-cccc-cccccccccccc");
+ private static readonly Guid _versionId = Guid.Parse("dddddddd-dddd-dddd-dddd-dddddddddddd");
+
+ private readonly SqlSearchProvider _provider;
+ private readonly Mock<ILibraryManager> _libraryManager = new();
+ private readonly User _user = new("test", "auth-provider", "reset-provider");
+
+ public SqlSearchProviderTests()
+ {
+ var itemTypeLookup = new ItemTypeLookup();
+ var movieTypeName = itemTypeLookup.BaseItemKindNames[BaseItemKind.Movie]!;
+ var folderTypeName = itemTypeLookup.BaseItemKindNames[BaseItemKind.Folder]!;
+
+ using (var context = CreateDbContext())
+ {
+ context.Users.Add(_user);
+ context.BaseItems.Add(CreateLibrary(_movieLibraryId, folderTypeName, "Movies", "/movies"));
+ context.BaseItems.Add(CreateLibrary(_movie4KLibraryId, folderTypeName, "Movies-4K", "/movies-4k"));
+ context.BaseItems.Add(CreateMovie(_primaryId, movieTypeName, _movie4KLibraryId, null));
+ context.BaseItems.Add(CreateMovie(_versionId, movieTypeName, _movieLibraryId, _primaryId));
+ context.SaveChanges();
+ }
+
+ var userManager = new Mock<IUserManager>();
+ userManager.Setup(u => u.GetUserById(_user.Id)).Returns(_user);
+
+ _provider = new SqlSearchProvider(
+ CreateDbContextFactory(),
+ itemTypeLookup,
+ _libraryManager.Object,
+ userManager.Object,
+ CreateBaseItemRepository(itemTypeLookup));
+ }
+
+ [Fact]
+ public async Task SearchAsync_UserWithoutThePrimarysLibrary_FindsTheVersion()
+ {
+ RestrictUserTo(_movieLibraryId);
+
+ var hits = await SearchAsync().ConfigureAwait(true);
+
+ Assert.Equal([_versionId], hits);
+ }
+
+ [Fact]
+ public async Task SearchAsync_UserWithBothLibraries_FindsThePrimaryOnce()
+ {
+ RestrictUserTo(_movieLibraryId, _movie4KLibraryId);
+
+ var hits = await SearchAsync().ConfigureAwait(true);
+
+ Assert.Equal([_primaryId], hits);
+ }
+
+ private void RestrictUserTo(params Guid[] libraryIds)
+ {
+ _libraryManager
+ .Setup(l => l.ConfigureUserAccess(It.IsAny<InternalItemsQuery>(), It.IsAny<User>()))
+ .Callback<InternalItemsQuery, User>((query, _) => query.TopParentIds = libraryIds);
+ }
+
+ private async Task<List<Guid>> SearchAsync()
+ {
+ var results = await _provider.SearchAsync(
+ new SearchProviderQuery { SearchTerm = "coco", UserId = _user.Id, Limit = 10 },
+ CancellationToken.None).ConfigureAwait(false);
+
+ return results.Select(r => r.ItemId).ToList();
+ }
+
+ private static BaseItemEntity CreateLibrary(Guid id, string typeName, string name, string path)
+ => new()
+ {
+ Id = id,
+ Type = typeName,
+ Name = name,
+ Path = path,
+ IsFolder = true
+ };
+
+ private static BaseItemEntity CreateMovie(Guid id, string typeName, Guid libraryId, Guid? primaryVersionId)
+ => new()
+ {
+ Id = id,
+ Type = typeName,
+ Name = "Coco",
+ CleanName = "coco",
+ SortName = "Coco",
+ MediaType = "Video",
+ IsMovie = true,
+ IsFolder = false,
+ IsVirtualItem = false,
+ ParentId = libraryId,
+ TopParentId = libraryId,
+ PresentationUniqueKey = (primaryVersionId ?? id).ToString("N"),
+ PrimaryVersionId = primaryVersionId
+ };
+}