aboutsummaryrefslogtreecommitdiff
path: root/tests/Jellyfin.Server.Implementations.Tests/Library
diff options
context:
space:
mode:
Diffstat (limited to 'tests/Jellyfin.Server.Implementations.Tests/Library')
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Library/LibraryManager/FindExtrasTests.cs41
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Library/LibraryManagerSortTests.cs89
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Library/PathExtensionsTests.cs63
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Library/UserDataManagerTests.cs216
4 files changed, 408 insertions, 1 deletions
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Library/LibraryManager/FindExtrasTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Library/LibraryManager/FindExtrasTests.cs
index 562711337f..07c537aee1 100644
--- a/tests/Jellyfin.Server.Implementations.Tests/Library/LibraryManager/FindExtrasTests.cs
+++ b/tests/Jellyfin.Server.Implementations.Tests/Library/LibraryManager/FindExtrasTests.cs
@@ -306,6 +306,47 @@ public class FindExtrasTests
}
[Fact]
+ public void FindExtras_TrailerWithYearInFilename_SetsProductionYearFromFilename()
+ {
+ var owner = new Movie { Name = "Up", Path = "/movies/Up/Up.mkv" };
+ var paths = new List<string>
+ {
+ "/movies/Up/Up.mkv",
+ "/movies/Up/trailers"
+ };
+
+ _fileSystemMock.Setup(f => f.GetFiles(
+ "/movies/Up/trailers",
+ It.IsAny<string[]>(),
+ false,
+ false))
+ .Returns(new List<FileSystemMetadata>
+ {
+ new()
+ {
+ FullName = "/movies/Up/trailers/Trailer 1 (2013).mkv",
+ Name = "Trailer 1 (2013).mkv",
+ IsDirectory = false
+ }
+ }).Verifiable();
+
+ var files = paths.Select(p => new FileSystemMetadata
+ {
+ FullName = p,
+ Name = Path.GetFileName(p),
+ IsDirectory = !Path.HasExtension(p)
+ }).ToList();
+
+ var extras = _libraryManager.FindExtras(owner, files, new DirectoryService(_fileSystemMock.Object)).ToList();
+
+ _fileSystemMock.Verify();
+ var trailer = Assert.Single(extras);
+ Assert.Equal(ExtraType.Trailer, trailer.ExtraType);
+ Assert.Equal(typeof(Trailer), trailer.GetType());
+ Assert.Equal(2013, trailer.ProductionYear);
+ }
+
+ [Fact]
public void FindExtras_SeriesWithTrailers_FindsCorrectExtras()
{
var owner = new Series { Name = "Dexter", Path = "/series/Dexter" };
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Library/LibraryManagerSortTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Library/LibraryManagerSortTests.cs
new file mode 100644
index 0000000000..65ec41291d
--- /dev/null
+++ b/tests/Jellyfin.Server.Implementations.Tests/Library/LibraryManagerSortTests.cs
@@ -0,0 +1,89 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using AutoFixture;
+using AutoFixture.AutoMoq;
+using Emby.Naming.Common;
+using Emby.Server.Implementations.Library;
+using Emby.Server.Implementations.Sorting;
+using Jellyfin.Data.Enums;
+using Jellyfin.Database.Implementations.Enums;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Audio;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Persistence;
+using MediaBrowser.Controller.Resolvers;
+using MediaBrowser.Controller.Sorting;
+using MediaBrowser.Model.IO;
+using Moq;
+using Xunit;
+using BaseItem = MediaBrowser.Controller.Entities.BaseItem;
+
+namespace Jellyfin.Server.Implementations.Tests.Library;
+
+public class LibraryManagerSortTests
+{
+ [Fact]
+ public void Sort_UserDependentKey_NullUser_ThrowsArgumentException()
+ {
+ var libraryManager = CreateLibraryManager(
+ new IBaseItemComparer[] { new PlayCountComparer(), new SortNameComparer() });
+
+ BaseItem[] items =
+ {
+ new Audio { Name = "Zulu", SortName = "Zulu", Id = Guid.NewGuid() },
+ new Audio { Name = "Alpha", SortName = "Alpha", Id = Guid.NewGuid() },
+ };
+
+ Assert.Throws<ArgumentException>(() => libraryManager.Sort(
+ items,
+ user: null,
+ new[] { (ItemSortBy.PlayCount, SortOrder.Descending) }).ToArray());
+ }
+
+ [Fact]
+ public void Sort_DateLastContentAdded_NullUser_OrdersByDateNotSortName()
+ {
+ var libraryManager = CreateLibraryManager(
+ new IBaseItemComparer[] { new DateLastMediaAddedComparer(), new SortNameComparer() });
+
+ BaseItem[] items =
+ {
+ MakeFolder("Alpha", new DateTime(2026, 1, 1)),
+ MakeFolder("Mike", new DateTime(2025, 1, 1)),
+ MakeFolder("Zulu", new DateTime(2024, 1, 1))
+ };
+
+ var sorted = libraryManager.Sort(
+ items,
+ user: null,
+ new[] { (ItemSortBy.DateLastContentAdded, SortOrder.Descending) }).ToArray();
+
+ Assert.Equal(new[] { "Alpha", "Mike", "Zulu" }, sorted.Select(i => i.Name));
+ }
+
+ private static Folder MakeFolder(string name, DateTime dateLastMediaAdded)
+ => new() { Name = name, Id = Guid.NewGuid(), DateLastMediaAdded = dateLastMediaAdded };
+
+ private static Emby.Server.Implementations.Library.LibraryManager CreateLibraryManager(IReadOnlyCollection<IBaseItemComparer> comparers)
+ {
+ var fixture = new Fixture().Customize(new AutoMoqCustomization());
+ fixture.Register(() => new NamingOptions());
+ var configMock = fixture.Freeze<Mock<IServerConfigurationManager>>();
+ configMock.Setup(c => c.ApplicationPaths.ProgramDataPath).Returns("/data");
+ BaseItem.ConfigurationManager ??= configMock.Object;
+ var itemRepository = fixture.Freeze<Mock<IItemRepository>>();
+ itemRepository.Setup(i => i.RetrieveItem(It.IsAny<Guid>())).Returns<BaseItem>(null);
+ var fileSystemMock = fixture.Freeze<Mock<IFileSystem>>();
+ fileSystemMock.Setup(f => f.GetFileInfo(It.IsAny<string>())).Returns<string>(path => new FileSystemMetadata { FullName = path });
+
+ return fixture.Build<Emby.Server.Implementations.Library.LibraryManager>().Do(s => s.AddParts(
+ fixture.Create<IEnumerable<IResolverIgnoreRule>>(),
+ fixture.Create<IEnumerable<IItemResolver>>(),
+ fixture.Create<IEnumerable<IIntroProvider>>(),
+ comparers,
+ fixture.Create<IEnumerable<ILibraryPostScanTask>>()))
+ .Create();
+ }
+}
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Library/PathExtensionsTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Library/PathExtensionsTests.cs
index 650d67b195..e65bc1d31f 100644
--- a/tests/Jellyfin.Server.Implementations.Tests/Library/PathExtensionsTests.cs
+++ b/tests/Jellyfin.Server.Implementations.Tests/Library/PathExtensionsTests.cs
@@ -9,44 +9,105 @@ namespace Jellyfin.Server.Implementations.Tests.Library
{
[Theory]
[InlineData("Superman: Red Son [imdbid=tt10985510]", "imdbid", "tt10985510")]
+ [InlineData("Superman: Red Son [imdb=tt10985510]", "imdbid", "tt10985510")]
[InlineData("Superman: Red Son [imdbid-tt10985510]", "imdbid", "tt10985510")]
+ [InlineData("Superman: Red Son [imdb-tt10985510]", "imdbid", "tt10985510")]
[InlineData("Superman: Red Son - tt10985510", "imdbid", "tt10985510")]
[InlineData("Superman: Red Son {imdbid=tt10985510}", "imdbid", "tt10985510")]
+ [InlineData("Superman: Red Son {imdb=tt10985510}", "imdbid", "tt10985510")]
+ [InlineData("Superman: Red Son {imdbid-tt10985510}", "imdbid", "tt10985510")]
+ [InlineData("Superman: Red Son {imdb-tt10985510}", "imdbid", "tt10985510")]
+ [InlineData("Superman: Red Son (imdbid=tt10985510)", "imdbid", "tt10985510")]
+ [InlineData("Superman: Red Son (imdb=tt10985510)", "imdbid", "tt10985510")]
[InlineData("Superman: Red Son (imdbid-tt10985510)", "imdbid", "tt10985510")]
+ [InlineData("Superman: Red Son (imdb-tt10985510)", "imdbid", "tt10985510")]
[InlineData("Superman: Red Son", "imdbid", null)]
[InlineData("Superman: Red Son [imdbid1=tt11111111][imdbid=tt10985510]", "imdbid", "tt10985510")]
+ [InlineData("Superman: Red Son [imdbid1=tt11111111][imdb=tt10985510]", "imdbid", "tt10985510")]
[InlineData("Superman: Red Son {imdbid1=tt11111111}(imdbid=tt10985510)", "imdbid", "tt10985510")]
+ [InlineData("Superman: Red Son {imdbid1=tt11111111}(imdb=tt10985510)", "imdbid", "tt10985510")]
[InlineData("Superman: Red Son (imdbid1-tt11111111)[imdbid=tt10985510]", "imdbid", "tt10985510")]
+ [InlineData("Superman: Red Son (imdbid1-tt11111111)[imdb=tt10985510]", "imdbid", "tt10985510")]
[InlineData("Superman: Red Son [tmdbid=618355][imdbid=tt10985510]", "imdbid", "tt10985510")]
+ [InlineData("Superman: Red Son [tmdbid=618355][imdb=tt10985510]", "imdbid", "tt10985510")]
[InlineData("Superman: Red Son [tmdbid-618355]{imdbid-tt10985510}", "imdbid", "tt10985510")]
+ [InlineData("Superman: Red Son [tmdbid-618355]{imdb-tt10985510}", "imdbid", "tt10985510")]
[InlineData("Superman: Red Son (tmdbid-618355)[imdbid-tt10985510]", "tmdbid", "618355")]
+ [InlineData("Superman: Red Son (tmdbid-618355)[imdb-tt10985510]", "tmdbid", "618355")]
[InlineData("Superman: Red Son [providera-id=1]", "providera-id", "1")]
[InlineData("Superman: Red Son [providerb-id=2]", "providerb-id", "2")]
[InlineData("Superman: Red Son [providera id=4]", "providera id", "4")]
[InlineData("Superman: Red Son [providerb id=5]", "providerb id", "5")]
+ [InlineData("Superman: Red Son [provider=99][providerid=5]", "providerid", "5")]
[InlineData("Superman: Red Son [tmdbid=3]", "tmdbid", "3")]
- [InlineData("Superman: Red Son [tvdbid-6]", "tvdbid", "6")]
+ [InlineData("Superman: Red Son [tmdb=3]", "tmdbid", "3")]
+ [InlineData("Superman: Red Son [tmdbid-3]", "tmdbid", "3")]
+ [InlineData("Superman: Red Son [tmdb-3]", "tmdbid", "3")]
[InlineData("Superman: Red Son {tmdbid=3}", "tmdbid", "3")]
+ [InlineData("Superman: Red Son {tmdb=3}", "tmdbid", "3")]
+ [InlineData("Superman: Red Son {tmdbid-3}", "tmdbid", "3")]
+ [InlineData("Superman: Red Son {tmdb-3}", "tmdbid", "3")]
+ [InlineData("Superman: Red Son (tmdbid=6)", "tmdbid", "6")]
+ [InlineData("Superman: Red Son (tmdb=6)", "tmdbid", "6")]
+ [InlineData("Superman: Red Son (tmdbid-6)", "tmdbid", "6")]
+ [InlineData("Superman: Red Son (tmdb-6)", "tmdbid", "6")]
+ [InlineData("Superman: Red Son [tvdbid=6]", "tvdbid", "6")]
+ [InlineData("Superman: Red Son [tvdb=6]", "tvdbid", "6")]
+ [InlineData("Superman: Red Son [tvdbid-6]", "tvdbid", "6")]
+ [InlineData("Superman: Red Son [tvdb-6]", "tvdbid", "6")]
+ [InlineData("Superman: Red Son {tvdbid=3}", "tvdbid", "3")]
+ [InlineData("Superman: Red Son {tvdb=3}", "tvdbid", "3")]
+ [InlineData("Superman: Red Son {tvdbid-3}", "tvdbid", "3")]
+ [InlineData("Superman: Red Son {tvdb-3}", "tvdbid", "3")]
+ [InlineData("Superman: Red Son (tvdbid=6)", "tvdbid", "6")]
+ [InlineData("Superman: Red Son (tvdb=6)", "tvdbid", "6")]
[InlineData("Superman: Red Son (tvdbid-6)", "tvdbid", "6")]
+ [InlineData("Superman: Red Son (tvdb-6)", "tvdbid", "6")]
[InlineData("[tmdbid=618355]", "tmdbid", "618355")]
+ [InlineData("[tmdb=618355]", "tmdbid", "618355")]
[InlineData("{tmdbid=618355}", "tmdbid", "618355")]
+ [InlineData("{tmdb=618355}", "tmdbid", "618355")]
[InlineData("(tmdbid=618355)", "tmdbid", "618355")]
+ [InlineData("(tmdb=618355)", "tmdbid", "618355")]
[InlineData("[tmdbid-618355]", "tmdbid", "618355")]
+ [InlineData("[tmdb-618355]", "tmdbid", "618355")]
[InlineData("{tmdbid-618355)", "tmdbid", null)]
+ [InlineData("{tmdb-618355)", "tmdbid", null)]
[InlineData("[tmdbid-618355}", "tmdbid", null)]
+ [InlineData("[tmdb-618355}", "tmdbid", null)]
[InlineData("tmdbid=111111][tmdbid=618355]", "tmdbid", "618355")]
+ [InlineData("tmdbid=111111][tmdb=618355]", "tmdbid", "618355")]
[InlineData("[tmdbid=618355]tmdbid=111111]", "tmdbid", "618355")]
+ [InlineData("[tmdb=618355]tmdbid=111111]", "tmdbid", "618355")]
[InlineData("tmdbid=618355]", "tmdbid", null)]
+ [InlineData("tmdb=618355]", "tmdbid", null)]
[InlineData("[tmdbid=618355", "tmdbid", null)]
+ [InlineData("[tmdb=618355", "tmdbid", null)]
[InlineData("tmdbid=618355", "tmdbid", null)]
+ [InlineData("tmdb=618355", "tmdbid", null)]
[InlineData("tmdbid=", "tmdbid", null)]
+ [InlineData("tmdb=", "tmdbid", null)]
[InlineData("tmdbid", "tmdbid", null)]
+ [InlineData("tmdb", "tmdbid", null)]
+ [InlineData("[tmdbid= ][tmdbid=223344]", "tmdbid", "223344")]
+ [InlineData("[tmdb= ][tmdb=223344]", "tmdbid", "223344")]
+ [InlineData("[tmdbid= ][tmdb=223344]", "tmdbid", "223344")]
+ [InlineData("[tmdb= ][tmdbid=223344]", "tmdbid", "223344")]
[InlineData("[tmdbid=][imdbid=tt10985510]", "tmdbid", null)]
+ [InlineData("[tmdb=][imdbid=tt10985510]", "tmdbid", null)]
[InlineData("[tmdbid-][imdbid-tt10985510]", "tmdbid", null)]
+ [InlineData("[tmdb-][imdbid-tt10985510]", "tmdbid", null)]
[InlineData("Superman: Red Son [tmdbid-618355][tmdbid=1234567]", "tmdbid", "618355")]
+ [InlineData("Superman: Red Son [tmdb-618355][tmdbid=1234567]", "tmdbid", "618355")]
[InlineData("{tmdbid=}{imdbid=tt10985510}", "tmdbid", null)]
+ [InlineData("{tmdb=}{imdbid=tt10985510}", "tmdbid", null)]
[InlineData("(tmdbid-)(imdbid-tt10985510)", "tmdbid", null)]
+ [InlineData("(tmdb-)(imdbid-tt10985510)", "tmdbid", null)]
[InlineData("Superman: Red Son {tmdbid-618355}{tmdbid=1234567}", "tmdbid", "618355")]
+ [InlineData("Superman: Red Son {tmdb-618355}{tmdbid=1234567}", "tmdbid", "618355")]
+ [InlineData("Superman: Red Son - tt10985510 [imdbid1=tt11]", "imdbid", "tt10985510")]
+ [InlineData("Superman: Red Son [tmdb=618355][tmdbid1=1]", "tmdbid", "618355")]
+ [InlineData("Superman: Red Son [tmdb=618355][tmdbid=12345]", "tmdbid", "618355")]
public void GetAttributeValue_ValidArgs_Correct(string input, string attribute, string? expectedResult)
{
Assert.Equal(expectedResult, PathExtensions.GetAttributeValue(input, attribute));
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Library/UserDataManagerTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Library/UserDataManagerTests.cs
new file mode 100644
index 0000000000..ba3127bc08
--- /dev/null
+++ b/tests/Jellyfin.Server.Implementations.Tests/Library/UserDataManagerTests.cs
@@ -0,0 +1,216 @@
+using System;
+using System.Collections.Generic;
+using Emby.Server.Implementations.Library;
+using Jellyfin.Database.Implementations;
+using Jellyfin.Database.Implementations.Entities;
+using Jellyfin.Database.Implementations.Locking;
+using Jellyfin.Database.Providers.Sqlite;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Model.Configuration;
+using Microsoft.Data.Sqlite;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Logging.Abstractions;
+using Moq;
+using Xunit;
+using AudioBook = MediaBrowser.Controller.Entities.AudioBook;
+
+namespace Jellyfin.Server.Implementations.Tests.Library;
+
+public sealed class UserDataManagerTests : IDisposable
+{
+ private readonly SqliteConnection _connection;
+ private readonly DbContextOptions<JellyfinDbContext> _dbOptions;
+ private readonly UserDataManager _userDataManager;
+ private readonly User _user;
+
+ public UserDataManagerTests()
+ {
+ _connection = new SqliteConnection("Data Source=:memory:");
+ _connection.Open();
+
+ _dbOptions = new DbContextOptionsBuilder<JellyfinDbContext>()
+ .UseSqlite(_connection)
+ .Options;
+
+ using (var ctx = CreateDbContext())
+ {
+ ctx.Database.EnsureCreated();
+ }
+
+ var factory = new Mock<IDbContextFactory<JellyfinDbContext>>();
+ factory.Setup(f => f.CreateDbContext()).Returns(CreateDbContext);
+
+ var config = new Mock<IServerConfigurationManager>();
+ config.SetupGet(c => c.Configuration).Returns(new ServerConfiguration());
+
+ _userDataManager = new UserDataManager(config.Object, factory.Object);
+ _user = new User("user", "auth-provider", "reset-provider")
+ {
+ Id = Guid.NewGuid()
+ };
+ }
+
+ public void Dispose()
+ {
+ _connection.Dispose();
+ }
+
+ private JellyfinDbContext CreateDbContext()
+ {
+ return new JellyfinDbContext(
+ _dbOptions,
+ NullLogger<JellyfinDbContext>.Instance,
+ new SqliteDatabaseProvider(null!, NullLogger<SqliteDatabaseProvider>.Instance),
+ new NoLockBehavior(NullLogger<NoLockBehavior>.Instance));
+ }
+
+ private AudioBook CreateAudioBook()
+ {
+ // GetUserDataKeys(): ["Author-Series-0001Book Title", "<item id N>"]
+ return new AudioBook
+ {
+ Id = Guid.NewGuid(),
+ Name = "Book Title",
+ Album = "Series",
+ AlbumArtists = new[] { "Author" },
+ IndexNumber = 1
+ };
+ }
+
+ private UserData CreateUserDataRow(AudioBook item, string key, long positionTicks)
+ {
+ return new UserData
+ {
+ ItemId = item.Id,
+ Item = null,
+ UserId = _user.Id,
+ User = null,
+ CustomDataKey = key,
+ PlaybackPositionTicks = positionTicks
+ };
+ }
+
+ [Fact]
+ public void GetUserData_RowsUnderCurrentAndRetiredKeys_PrefersCurrentKeyRow()
+ {
+ var item = CreateAudioBook();
+ var currentKey = item.GetUserDataKeys()[0];
+
+ // the retired-key row comes first to ensure selection is by key, not row order
+ item.UserData = new List<UserData>
+ {
+ CreateUserDataRow(item, "Author-Old Album-0001Old File Name", 111),
+ CreateUserDataRow(item, currentKey, 222)
+ };
+
+ var userData = _userDataManager.GetUserData(_user, item);
+
+ Assert.NotNull(userData);
+ Assert.Equal(currentKey, userData.Key);
+ Assert.Equal(222, userData.PlaybackPositionTicks);
+ }
+
+ [Fact]
+ public void GetUserData_NoPrimaryKeyRow_UsesNextCurrentKeyRow()
+ {
+ var item = CreateAudioBook();
+ var idKey = item.GetUserDataKeys()[1];
+
+ item.UserData = new List<UserData>
+ {
+ CreateUserDataRow(item, "Author-Old Album-0001Old File Name", 111),
+ CreateUserDataRow(item, idKey, 333)
+ };
+
+ var userData = _userDataManager.GetUserData(_user, item);
+
+ Assert.NotNull(userData);
+ Assert.Equal(idKey, userData.Key);
+ Assert.Equal(333, userData.PlaybackPositionTicks);
+ }
+
+ [Fact]
+ public void GetUserData_OnlyRetiredKeyRows_ReturnsRetiredKeyRow()
+ {
+ var item = CreateAudioBook();
+
+ item.UserData = new List<UserData>
+ {
+ CreateUserDataRow(item, "Author-Old Album-0001Old File Name", 111)
+ };
+
+ var userData = _userDataManager.GetUserData(_user, item);
+
+ Assert.NotNull(userData);
+ Assert.Equal(111, userData.PlaybackPositionTicks);
+ }
+
+ [Fact]
+ public void GetUserData_NoRows_ReturnsDefaultWithPrimaryKey()
+ {
+ var item = CreateAudioBook();
+ item.UserData = new List<UserData>();
+
+ var userData = _userDataManager.GetUserData(_user, item);
+
+ Assert.NotNull(userData);
+ Assert.Equal(item.GetUserDataKeys()[0], userData.Key);
+ Assert.Equal(0, userData.PlaybackPositionTicks);
+ }
+
+ [Fact]
+ public void GetUserData_RowsForOtherUsers_AreIgnored()
+ {
+ var item = CreateAudioBook();
+ var currentKey = item.GetUserDataKeys()[0];
+
+ var otherUserRow = CreateUserDataRow(item, currentKey, 999);
+ otherUserRow.UserId = Guid.NewGuid();
+
+ item.UserData = new List<UserData>
+ {
+ otherUserRow,
+ CreateUserDataRow(item, currentKey, 222)
+ };
+
+ var userData = _userDataManager.GetUserData(_user, item);
+
+ Assert.NotNull(userData);
+ Assert.Equal(222, userData.PlaybackPositionTicks);
+ }
+
+ [Fact]
+ public void GetUserDataBatch_DatabaseFallback_ResolvesRowsByKeyOrder()
+ {
+ // no preloaded navigation data, so the batch takes the database fallback
+ var fossilItem = CreateAudioBook();
+ var retiredItem = CreateAudioBook();
+
+ using (var ctx = CreateDbContext())
+ {
+ ctx.Users.Add(_user);
+ ctx.BaseItems.Add(new BaseItemEntity { Id = fossilItem.Id, Type = typeof(AudioBook).FullName! });
+ ctx.BaseItems.Add(new BaseItemEntity { Id = retiredItem.Id, Type = typeof(AudioBook).FullName! });
+
+ // the stale id-key row is inserted first so selection by row order would return it
+ ctx.UserData.AddRange(
+ CreateUserDataRow(fossilItem, fossilItem.GetUserDataKeys()[1], 111),
+ CreateUserDataRow(fossilItem, fossilItem.GetUserDataKeys()[0], 222),
+ CreateUserDataRow(retiredItem, "Author-Old Album-0001Old File Name", 333));
+ ctx.SaveChanges();
+ }
+
+ var result = _userDataManager.GetUserDataBatch([fossilItem, retiredItem], _user);
+
+ Assert.Equal(222, result[fossilItem.Id].PlaybackPositionTicks);
+ Assert.Equal(333, result[retiredItem.Id].PlaybackPositionTicks);
+ }
+
+ [Fact]
+ public void GetUserData_NullUser_ThrowsArgumentNullException()
+ {
+ var item = CreateAudioBook();
+ Assert.Throws<ArgumentNullException>(() => _userDataManager.GetUserData(null!, item));
+ }
+}