Merge pull request 'Add placeholder item handling and related unit test for deletion' (#2) from pgsql_testing_branch into main
Reviewed-on: #2
This commit was merged in pull request #2.
This commit is contained in:
@@ -63,6 +63,9 @@ public sealed class BaseItemRepository
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public static readonly Guid PlaceholderId = Guid.Parse("00000000-0000-0000-0000-000000000001");
|
public static readonly Guid PlaceholderId = Guid.Parse("00000000-0000-0000-0000-000000000001");
|
||||||
|
|
||||||
|
private const string PlaceholderType = "PLACEHOLDER";
|
||||||
|
private const string PlaceholderName = "This is a placeholder item for UserData that has been detacted from its original item";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// This holds all the types in the running assemblies
|
/// This holds all the types in the running assemblies
|
||||||
/// so that we can de-serialize properly when we don't have strong types.
|
/// so that we can de-serialize properly when we don't have strong types.
|
||||||
@@ -129,6 +132,8 @@ public sealed class BaseItemRepository
|
|||||||
{
|
{
|
||||||
var date = (DateTime?)DateTime.UtcNow;
|
var date = (DateTime?)DateTime.UtcNow;
|
||||||
|
|
||||||
|
await EnsurePlaceholderItemAsync(context, cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
var relatedItems = ids.SelectMany(f => TraverseHirachyDown(f, context)).ToArray();
|
var relatedItems = ids.SelectMany(f => TraverseHirachyDown(f, context)).ToArray();
|
||||||
|
|
||||||
// Remove any UserData entries for the placeholder item that would conflict with the UserData
|
// Remove any UserData entries for the placeholder item that would conflict with the UserData
|
||||||
@@ -196,6 +201,38 @@ public sealed class BaseItemRepository
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task EnsurePlaceholderItemAsync(JellyfinDbContext context, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (await context.BaseItems.AnyAsync(e => e.Id == PlaceholderId, cancellationToken).ConfigureAwait(false))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var placeholder = new BaseItemEntity
|
||||||
|
{
|
||||||
|
Id = PlaceholderId,
|
||||||
|
Type = PlaceholderType,
|
||||||
|
Name = PlaceholderName
|
||||||
|
};
|
||||||
|
|
||||||
|
await context.BaseItems.AddAsync(placeholder, cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await context.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
_logger.LogWarning("The detached UserData placeholder item was missing and has been recreated.");
|
||||||
|
}
|
||||||
|
catch (DbUpdateException)
|
||||||
|
{
|
||||||
|
context.Entry(placeholder).State = EntityState.Detached;
|
||||||
|
|
||||||
|
if (!await context.BaseItems.AnyAsync(e => e.Id == PlaceholderId, cancellationToken).ConfigureAwait(false))
|
||||||
|
{
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public void UpdateInheritedValues()
|
public void UpdateInheritedValues()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -5,9 +5,17 @@
|
|||||||
namespace Jellyfin.Server.Implementations.Tests.Item;
|
namespace Jellyfin.Server.Implementations.Tests.Item;
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Jellyfin.Database.Implementations;
|
||||||
using Jellyfin.Database.Implementations.Entities;
|
using Jellyfin.Database.Implementations.Entities;
|
||||||
|
using Jellyfin.Database.Implementations.Locking;
|
||||||
using Jellyfin.Server.Implementations.Item;
|
using Jellyfin.Server.Implementations.Item;
|
||||||
using MediaBrowser.Controller;
|
using MediaBrowser.Controller;
|
||||||
|
using MediaBrowser.Controller.Configuration;
|
||||||
|
using MediaBrowser.Controller.Persistence;
|
||||||
|
using Microsoft.Data.Sqlite;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Microsoft.Extensions.Logging.Abstractions;
|
using Microsoft.Extensions.Logging.Abstractions;
|
||||||
using Moq;
|
using Moq;
|
||||||
@@ -73,4 +81,112 @@ public class BaseItemRepositoryTests
|
|||||||
// Assert
|
// Assert
|
||||||
Assert.NotNull(result);
|
Assert.NotNull(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task DeleteItemAsync_RecreatesMissingPlaceholderBeforeDetachingUserData()
|
||||||
|
{
|
||||||
|
await using var dbFactory = new TestDbContextFactory();
|
||||||
|
|
||||||
|
Guid itemId;
|
||||||
|
await using (var context = await dbFactory.CreateDbContextAsync(CancellationToken.None))
|
||||||
|
{
|
||||||
|
await context.Database.EnsureCreatedAsync();
|
||||||
|
|
||||||
|
var user = new User("test-user", "Default", "Default");
|
||||||
|
var item = new BaseItemEntity
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid(),
|
||||||
|
Type = "TestItem",
|
||||||
|
Name = "Delete me"
|
||||||
|
};
|
||||||
|
|
||||||
|
context.Users.Add(user);
|
||||||
|
context.BaseItems.Add(item);
|
||||||
|
context.UserData.Add(new UserData
|
||||||
|
{
|
||||||
|
CustomDataKey = "userdata-key",
|
||||||
|
ItemId = item.Id,
|
||||||
|
Item = item,
|
||||||
|
UserId = user.Id,
|
||||||
|
User = user,
|
||||||
|
Played = true,
|
||||||
|
PlayCount = 1
|
||||||
|
});
|
||||||
|
|
||||||
|
await context.SaveChangesAsync();
|
||||||
|
|
||||||
|
var placeholder = await context.BaseItems.SingleAsync(e => e.Id == BaseItemRepository.PlaceholderId);
|
||||||
|
context.BaseItems.Remove(placeholder);
|
||||||
|
await context.SaveChangesAsync();
|
||||||
|
|
||||||
|
itemId = item.Id;
|
||||||
|
}
|
||||||
|
|
||||||
|
var repository = new BaseItemRepository(
|
||||||
|
dbFactory,
|
||||||
|
Mock.Of<IServerApplicationHost>(),
|
||||||
|
Mock.Of<IItemTypeLookup>(),
|
||||||
|
Mock.Of<IServerConfigurationManager>(),
|
||||||
|
NullLogger<BaseItemRepository>.Instance);
|
||||||
|
|
||||||
|
await repository.DeleteItemAsync([itemId], CancellationToken.None);
|
||||||
|
|
||||||
|
await using var verificationContext = await dbFactory.CreateDbContextAsync(CancellationToken.None);
|
||||||
|
var recreatedPlaceholder = await verificationContext.BaseItems.SingleOrDefaultAsync(e => e.Id == BaseItemRepository.PlaceholderId);
|
||||||
|
var detachedUserData = await verificationContext.UserData.SingleAsync();
|
||||||
|
|
||||||
|
Assert.NotNull(recreatedPlaceholder);
|
||||||
|
Assert.Null(await verificationContext.BaseItems.SingleOrDefaultAsync(e => e.Id == itemId));
|
||||||
|
Assert.Equal(BaseItemRepository.PlaceholderId, detachedUserData.ItemId);
|
||||||
|
Assert.NotNull(detachedUserData.RetentionDate);
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class TestDbContextFactory : IDbContextFactory<JellyfinDbContext>, IAsyncDisposable
|
||||||
|
{
|
||||||
|
private readonly SqliteConnection _connection;
|
||||||
|
private readonly DbContextOptions<JellyfinDbContext> _options;
|
||||||
|
private readonly Mock<IJellyfinDatabaseProvider> _databaseProviderMock;
|
||||||
|
private readonly Mock<IEntityFrameworkCoreLockingBehavior> _lockingBehaviorMock;
|
||||||
|
|
||||||
|
public TestDbContextFactory()
|
||||||
|
{
|
||||||
|
_connection = new SqliteConnection("Data Source=:memory:");
|
||||||
|
_connection.Open();
|
||||||
|
|
||||||
|
_options = new DbContextOptionsBuilder<JellyfinDbContext>()
|
||||||
|
.UseSqlite(_connection)
|
||||||
|
.Options;
|
||||||
|
|
||||||
|
_databaseProviderMock = new Mock<IJellyfinDatabaseProvider>();
|
||||||
|
_databaseProviderMock.Setup(m => m.OnModelCreating(It.IsAny<ModelBuilder>()));
|
||||||
|
_databaseProviderMock.Setup(m => m.ConfigureConventions(It.IsAny<ModelConfigurationBuilder>()));
|
||||||
|
|
||||||
|
_lockingBehaviorMock = new Mock<IEntityFrameworkCoreLockingBehavior>();
|
||||||
|
_lockingBehaviorMock
|
||||||
|
.Setup(m => m.OnSaveChanges(It.IsAny<JellyfinDbContext>(), It.IsAny<Action>()))
|
||||||
|
.Callback<JellyfinDbContext, Action>((_, saveChanges) => saveChanges());
|
||||||
|
_lockingBehaviorMock
|
||||||
|
.Setup(m => m.OnSaveChangesAsync(It.IsAny<JellyfinDbContext>(), It.IsAny<Func<Task>>()))
|
||||||
|
.Returns<JellyfinDbContext, Func<Task>>((_, saveChanges) => saveChanges());
|
||||||
|
}
|
||||||
|
|
||||||
|
public JellyfinDbContext CreateDbContext()
|
||||||
|
{
|
||||||
|
return new JellyfinDbContext(
|
||||||
|
_options,
|
||||||
|
NullLogger<JellyfinDbContext>.Instance,
|
||||||
|
_databaseProviderMock.Object,
|
||||||
|
_lockingBehaviorMock.Object);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ValueTask<JellyfinDbContext> CreateDbContextAsync(CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
return ValueTask.FromResult(CreateDbContext());
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ValueTask DisposeAsync()
|
||||||
|
{
|
||||||
|
await _connection.DisposeAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user