using System;
using System.Linq;
using Emby.Server.Implementations.Data;
using Jellyfin.Database.Implementations;
using Jellyfin.Database.Implementations.Entities;
using Jellyfin.Database.Implementations.Locking;
using Jellyfin.Database.Providers.Sqlite;
using Jellyfin.Server.Implementations.Item;
using MediaBrowser.Controller;
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 BaseItemKind = Jellyfin.Data.Enums.BaseItemKind;
namespace Jellyfin.Server.Implementations.Tests.Item;
///
/// The by-name endpoints (artists, album artists, genres, studios) all funnel through
/// GetItemValues. A query without a Limit used to have its total record count
/// silently disabled, so callers got a populated Items array next to a zero total.
///
public sealed class BaseItemRepositoryByNameTotalCountTests : IDisposable
{
private readonly SqliteConnection _connection;
private readonly DbContextOptions _dbOptions;
private readonly BaseItemRepository _repository;
private readonly ItemTypeLookup _itemTypeLookup;
public BaseItemRepositoryByNameTotalCountTests()
{
_connection = new SqliteConnection("Data Source=:memory:");
_connection.Open();
_dbOptions = new DbContextOptionsBuilder()
.UseSqlite(_connection)
.Options;
using (var ctx = CreateDbContext())
{
ctx.Database.EnsureCreated();
}
var factory = new Mock>();
factory.Setup(f => f.CreateDbContext()).Returns(CreateDbContext);
_itemTypeLookup = new ItemTypeLookup();
var serverConfigurationManager = new Mock();
serverConfigurationManager.Setup(c => c.Configuration).Returns(new ServerConfiguration());
_repository = new BaseItemRepository(
factory.Object,
new Mock().Object,
_itemTypeLookup,
serverConfigurationManager.Object,
NullLogger.Instance);
}
public void Dispose()
{
_connection.Dispose();
}
[Fact]
public void GetArtists_WithoutLimit_ReportsTotalRecordCount()
{
SeedArtists(3);
var result = _repository.GetArtists(CreateQuery(limit: null));
Assert.Equal(3, result.Items.Count);
Assert.Equal(3, result.TotalRecordCount);
}
[Fact]
public void GetArtists_WithLimit_ReportsTotalBeyondThePage()
{
SeedArtists(3);
var result = _repository.GetArtists(CreateQuery(limit: 2));
Assert.Equal(2, result.Items.Count);
Assert.Equal(3, result.TotalRecordCount);
}
[Fact]
public void GetArtists_TotalRecordCountDisabled_StaysZero()
{
SeedArtists(3);
var query = CreateQuery(limit: null);
query.EnableTotalRecordCount = false;
var result = _repository.GetArtists(query);
Assert.Equal(3, result.Items.Count);
Assert.Equal(0, result.TotalRecordCount);
}
[Fact]
public void GetArtists_WithoutLimit_DoesNotMutateCallerQuery()
{
SeedArtists(1);
var query = CreateQuery(limit: null);
Assert.True(query.EnableTotalRecordCount);
_repository.GetArtists(query);
// The repository used to flip this flag on the caller's own query object, so a
// reused query silently lost its total on every subsequent call.
Assert.True(query.EnableTotalRecordCount);
}
private static InternalItemsQuery CreateQuery(int? limit)
{
return new InternalItemsQuery(new User("test", "auth", "reset"))
{
Limit = limit
};
}
///
/// Creates artists, each credited on one song, which is what
/// makes them visible to the item-value join behind the by-name endpoints.
///
private void SeedArtists(int count)
{
using var ctx = CreateDbContext();
for (var i = 0; i < count; i++)
{
var name = $"Artist {i}";
var cleanName = name.ToLowerInvariant();
var artistId = Guid.Parse($"aaaaaaaa-0000-0000-0000-{i:D12}");
var songId = Guid.Parse($"55555555-0000-0000-0000-{i:D12}");
var valueId = Guid.Parse($"cccccccc-0000-0000-0000-{i:D12}");
var artist = new BaseItemEntity
{
Id = artistId,
Type = _itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicArtist],
Name = name,
CleanName = cleanName,
PresentationUniqueKey = artistId.ToString("N"),
IsFolder = true,
IsVirtualItem = false
};
var song = new BaseItemEntity
{
Id = songId,
Type = _itemTypeLookup.BaseItemKindNames[BaseItemKind.Audio],
Name = $"Song {i}",
CleanName = $"song {i}",
PresentationUniqueKey = songId.ToString("N"),
MediaType = "Audio",
IsFolder = false,
IsVirtualItem = false
};
var itemValue = new ItemValue
{
ItemValueId = valueId,
Type = ItemValueType.Artist,
Value = name,
CleanValue = cleanName
};
ctx.BaseItems.Add(artist);
ctx.BaseItems.Add(song);
ctx.ItemValues.Add(itemValue);
ctx.ItemValuesMap.Add(new ItemValueMap
{
ItemId = songId,
ItemValueId = valueId,
Item = song,
ItemValue = itemValue
});
}
ctx.SaveChanges();
}
private JellyfinDbContext CreateDbContext()
{
return new JellyfinDbContext(
_dbOptions,
NullLogger.Instance,
new SqliteDatabaseProvider(null!, NullLogger.Instance),
new NoLockBehavior(NullLogger.Instance));
}
}