using System.Threading.Channels; using ErsatzTV.Application; using ErsatzTV.Application.Libraries; using ErsatzTV.Application.Plex; using ErsatzTV.Application.Subtitles; using ErsatzTV.Core; using ErsatzTV.Core.Domain; using ErsatzTV.Core.Interfaces.Locking; using ErsatzTV.Infrastructure.Data; using ErsatzTV.Tests.Support; using LanguageExt; using MediatR; using Microsoft.Extensions.Logging.Abstractions; using NSubstitute; using NUnit.Framework; using Shouldly; using static LanguageExt.Prelude; using ThreadingChannel = System.Threading.Channels.Channel; namespace ErsatzTV.Tests.Application.Libraries; [TestFixture] public class QueueShowScanByLibraryIdHandlerTests { private InMemoryTvContext _db = null!; [SetUp] public async Task SetUp() => _db = await InMemoryTvContext.CreateAsync(); [TearDown] public async Task TearDown() => await _db.DisposeAsync(); [Test] public async Task Handle_Should_Return_NotFound_When_Library_Missing() { var locker = Substitute.For(); var mediator = Substitute.For(); QueueShowScanByLibraryIdHandler handler = CreateHandler(locker, mediator, out _); QueueShowScanResult result = await handler.Handle( new QueueShowScanByLibraryId(9999, 1, "Show", false), CancellationToken.None); result.ShouldBe(QueueShowScanResult.NotFound); locker.DidNotReceive().LockLibrary(Arg.Any()); } [Test] public async Task Handle_Should_Return_SyncDisabled_When_Item_Sync_Off() { int libraryId = await SeedPlexLibrary(shouldSyncItems: false); var locker = Substitute.For(); var mediator = Substitute.For(); QueueShowScanByLibraryIdHandler handler = CreateHandler(locker, mediator, out _); QueueShowScanResult result = await handler.Handle( new QueueShowScanByLibraryId(libraryId, 1, "Show", false), CancellationToken.None); result.ShouldBe(QueueShowScanResult.SyncDisabled); locker.DidNotReceive().LockLibrary(Arg.Any()); } [Test] public async Task Handle_Should_Return_AlreadyScanning_When_Lock_Not_Acquired() { int libraryId = await SeedPlexLibrary(shouldSyncItems: true); var locker = Substitute.For(); locker.LockLibrary(libraryId).Returns(false); var mediator = Substitute.For(); QueueShowScanByLibraryIdHandler handler = CreateHandler(locker, mediator, out _); QueueShowScanResult result = await handler.Handle( new QueueShowScanByLibraryId(libraryId, 1, "Show", false), CancellationToken.None); result.ShouldBe(QueueShowScanResult.AlreadyScanning); await mediator.DidNotReceive().Send(Arg.Any(), Arg.Any()); } [Test] public async Task Handle_Should_Return_Unsupported_For_Local_Library() { int libraryId = await SeedLocalLibrary(); var locker = Substitute.For(); locker.LockLibrary(libraryId).Returns(true); var mediator = Substitute.For(); QueueShowScanByLibraryIdHandler handler = CreateHandler(locker, mediator, out _); QueueShowScanResult result = await handler.Handle( new QueueShowScanByLibraryId(libraryId, 1, "Show", false), CancellationToken.None); result.ShouldBe(QueueShowScanResult.Unsupported); locker.Received(1).UnlockLibrary(libraryId); } [Test] public async Task Handle_Should_Return_Queued_And_Extract_Subtitles_On_Deep_Success() { int libraryId = await SeedPlexLibrary(shouldSyncItems: true); var locker = Substitute.For(); locker.LockLibrary(libraryId).Returns(true); var mediator = Substitute.For(); mediator.Send(Arg.Any(), Arg.Any()) .Returns(Right("ok")); QueueShowScanByLibraryIdHandler handler = CreateHandler( locker, mediator, out Channel worker); QueueShowScanResult result = await handler.Handle( new QueueShowScanByLibraryId(libraryId, 42, "Show", DeepScan: true), CancellationToken.None); result.ShouldBe(QueueShowScanResult.Queued); locker.Received(1).UnlockLibrary(libraryId); worker.Reader.TryRead(out IBackgroundServiceRequest? request).ShouldBeTrue(); request.ShouldBeOfType().ShowId.ShouldBe(42); } [Test] public async Task Handle_Should_Return_ScanFailed_When_SubScan_Left() { int libraryId = await SeedPlexLibrary(shouldSyncItems: true); var locker = Substitute.For(); locker.LockLibrary(libraryId).Returns(true); var mediator = Substitute.For(); mediator.Send(Arg.Any(), Arg.Any()) .Returns(Left(BaseError.New("scan error"))); QueueShowScanByLibraryIdHandler handler = CreateHandler( locker, mediator, out Channel worker); QueueShowScanResult result = await handler.Handle( new QueueShowScanByLibraryId(libraryId, 42, "Show", DeepScan: true), CancellationToken.None); result.ShouldBe(QueueShowScanResult.ScanFailed); locker.Received(1).UnlockLibrary(libraryId); // no subtitle extraction on a failed scan worker.Reader.TryRead(out _).ShouldBeFalse(); } private QueueShowScanByLibraryIdHandler CreateHandler( IEntityLocker locker, IMediator mediator, out Channel worker) { worker = ThreadingChannel.CreateUnbounded(); return new QueueShowScanByLibraryIdHandler( _db.Factory, locker, mediator, worker.Writer, NullLogger.Instance); } private async Task SeedLocalLibrary() { await using TvContext context = _db.CreateContext(); var source = new LocalMediaSource { Libraries = [ new LocalLibrary { Name = "Local Movies", MediaKind = LibraryMediaKind.Movies, Paths = [] } ] }; await context.LocalMediaSources.AddAsync(source); await context.SaveChangesAsync(); return source.Libraries[0].Id; } private async Task SeedPlexLibrary(bool shouldSyncItems) { await using TvContext context = _db.CreateContext(); var source = new PlexMediaSource { ServerName = "Plex Server", ProductVersion = "1", Platform = "Linux", PlatformVersion = "1", ClientIdentifier = "plex", Connections = [], PathReplacements = [], Libraries = [ new PlexLibrary { Name = "Plex Shows", MediaKind = LibraryMediaKind.Shows, Key = "1", ShouldSyncItems = shouldSyncItems, Paths = [] } ] }; await context.PlexMediaSources.AddAsync(source); await context.SaveChangesAsync(); return source.Libraries[0].Id; } }