using Microsoft.EntityFrameworkCore; using NSubstitute; using TeleWave.Application.Common.Interfaces; using TeleWave.Application.Library.Genres; using TeleWave.Application.Media.MovieImport; using TeleWave.Application.Metadata; using TeleWave.Application.Tests.Support; using TeleWave.Domain.Library; using TeleWave.Domain.Media; using Xunit; namespace TeleWave.Application.Tests.Media; /// /// Массовое заведение фильмов: шоу на файл, метаданные из источника, файл в очередь. Фильмы /// разбираются независимо — одна битая строка не должна отменять остальные сорок. /// public class ImportMoviesTests { private const string Provider = "tmdb"; private static IMediaStorage StorageWith(params string[] files) { var storage = Substitute.For(); storage .ListManualInbox(Arg.Any()) .Returns([ .. files.Select(f => new IMediaStorage.ManualInboxFile( f, Path.GetFileName(f), 1024 )), ]); return storage; } private static IMetadataProviderResolver Resolver(ShowMetadata? metadata = null) { var provider = Substitute.For(); provider .GetShowAsync(Arg.Any(), Arg.Any(), Arg.Any()) .Returns(metadata); var resolver = Substitute.For(); resolver.Resolve(Provider).Returns(provider); resolver.AvailableKeys.Returns([Provider]); return resolver; } private static async Task ImportAsync( TestDb fixture, IMediaStorage storage, IMetadataProviderResolver resolver, IMediaProcessingQueue queue, params MovieImportItem[] items ) { await using var db = fixture.New(); var applier = new ShowMetadataApplier( db, resolver, Substitute.For(), Substitute.For(), new GenreMatcher(db) ); var result = await new ImportMoviesCommandHandler(db, storage, queue, applier).Handle( new ImportMoviesCommand(items, Provider), CancellationToken.None ); Assert.True(result.IsSuccess); await db.SaveChangesAsync(CancellationToken.None); return result.Value; } [Fact] public async Task CreatesShowPerFile_AndSendsItToProcessing() { var fixture = new TestDb(); var storage = StorageWith("Gladiator.2000.BDRip.mkv"); var queue = Substitute.For(); var result = await ImportAsync( fixture, storage, Resolver(new ShowMetadata("98", "Gladiator", 2000, "desc", null, [], null)), queue, new MovieImportItem("Gladiator.2000.BDRip.mkv", null, "Gladiator", 2000, "98") ); Assert.Equal(1, result.Created); Assert.Equal(1, result.Enriched); Assert.Empty(result.Failed); await using var db = fixture.New(); var show = await db.Shows.Include(s => s.Episodes).SingleAsync(CancellationToken.None); Assert.Equal(ShowKind.Single, show.Kind); Assert.Equal("Gladiator", show.Name); Assert.Equal(2000, show.Year); Assert.Single(show.Episodes); // Файл ушёл в очередь обработки, а спутники из manual/ подчищены. queue.Received(1).Enqueue(Arg.Any()); await storage .Received(1) .CleanupManualLeftoversAsync("Gladiator.2000.BDRip.mkv", Arg.Any()); } [Fact] public async Task CreatesShowWithoutMetadata_WhenCandidateIsNotChosen() { // Строка, где источник ничего не дал: шоу с файлом уже полезно, метаданные дотянет // массовое обогащение. var fixture = new TestDb(); var result = await ImportAsync( fixture, StorageWith("Shou.Trumana.1998.BDRip.mkv"), Resolver(), Substitute.For(), new MovieImportItem("Shou.Trumana.1998.BDRip.mkv", null, "Шоу Трумана", 1998, null) ); Assert.Equal(1, result.Created); Assert.Equal(0, result.Enriched); Assert.Empty(result.Failed); await using var db = fixture.New(); var show = await db.Shows.SingleAsync(CancellationToken.None); Assert.Equal("Шоу Трумана", show.Name); Assert.Null(show.MetadataExternalId); } [Fact] public async Task AttachesAlreadyUploadedAsset_WithoutQueueingItTwice() { var fixture = new TestDb(); var asset = MediaAsset.Register("Die Hard 2.avi", ".avi", MediaSource.Upload); await using (var seed = fixture.New()) { seed.MediaAssets.Add(asset); await seed.SaveChangesAsync(CancellationToken.None); } var queue = Substitute.For(); var result = await ImportAsync( fixture, StorageWith(), Resolver(), queue, new MovieImportItem(null, asset.Id, "Die Hard 2", 1990, null) ); Assert.Equal(1, result.Created); // Загруженный файл встал в очередь ещё при загрузке — второй раз ставить нечего. queue.DidNotReceive().Enqueue(Arg.Any()); await using var db = fixture.New(); var show = await db.Shows.Include(s => s.Episodes).SingleAsync(CancellationToken.None); Assert.Equal(asset.Id, Assert.Single(show.Episodes).MediaAssetId); } [Fact] public async Task ReportsMissingFile_AndImportsTheRest() { var fixture = new TestDb(); var result = await ImportAsync( fixture, StorageWith("Gladiator.2000.BDRip.mkv"), Resolver(), Substitute.For(), new MovieImportItem("Ушёл.из.каталога.mkv", null, "Пропавший", null, null), new MovieImportItem("Gladiator.2000.BDRip.mkv", null, "Gladiator", 2000, null) ); Assert.Equal(1, result.Created); var failure = Assert.Single(result.Failed); Assert.Equal("Пропавший", failure.Title); } [Fact] public async Task RejectsFileAlreadyInLibrary() { var fixture = new TestDb(); await using (var seed = fixture.New()) { seed.MediaAssets.Add( MediaAsset.Register("Gladiator.2000.BDRip.mkv", ".mkv", MediaSource.Upload) ); await seed.SaveChangesAsync(CancellationToken.None); } var result = await ImportAsync( fixture, StorageWith("Gladiator.2000.BDRip.mkv"), Resolver(), Substitute.For(), new MovieImportItem("Gladiator.2000.BDRip.mkv", null, "Gladiator", 2000, null) ); Assert.Equal(0, result.Created); Assert.Single(result.Failed); } [Fact] public async Task AutoAttach_CreatesMovie_OnConfidentMatch() { var fixture = new TestDb(); var asset = MediaAsset.Register("Gladiator.2000.BDRip.mkv", ".mkv", MediaSource.Inbox); await using (var seed = fixture.New()) { seed.MediaAssets.Add(asset); await seed.SaveChangesAsync(CancellationToken.None); } var provider = Substitute.For(); provider .SearchShowsAsync(Arg.Any(), Arg.Any(), Arg.Any()) .Returns([new MetadataCandidate("98", "Gladiator", 2000, null, null, ShowKind.Single)]); provider .GetShowAsync("98", Arg.Any(), Arg.Any()) .Returns(new ShowMetadata("98", "Gladiator", 2000, "desc", null, [], null)); var resolver = Substitute.For(); resolver.Resolve(Provider).Returns(provider); resolver.AvailableKeys.Returns([Provider]); await using (var db = fixture.New()) { var applier = new ShowMetadataApplier( db, resolver, Substitute.For(), Substitute.For(), new GenreMatcher(db) ); var result = await new AutoAttachMovieCommandHandler( db, resolver, new MovieMatcher(db, resolver), applier ).Handle( new AutoAttachMovieCommand(asset.Id, "Gladiator.2000.BDRip.mkv"), CancellationToken.None ); Assert.True(result.IsSuccess); await db.SaveChangesAsync(CancellationToken.None); } await using var check = fixture.New(); var show = await check.Shows.Include(s => s.Episodes).SingleAsync(CancellationToken.None); Assert.Equal("Gladiator", show.Name); Assert.Equal("98", show.MetadataExternalId); Assert.Equal(asset.Id, Assert.Single(show.Episodes).MediaAssetId); } [Fact] public async Task AutoAttach_DoesNothing_WhenMatchIsLoose() { // Файл остаётся ассетом без шоу — его разберёт человек в таблице разбора. var fixture = new TestDb(); var asset = MediaAsset.Register("Voron.1994.TRIPLE.mkv", ".mkv", MediaSource.Inbox); await using (var seed = fixture.New()) { seed.MediaAssets.Add(asset); await seed.SaveChangesAsync(CancellationToken.None); } var provider = Substitute.For(); provider .SearchShowsAsync(Arg.Any(), Arg.Any(), Arg.Any()) .Returns([new MetadataCandidate("7", "The Crow", 1994, null, null, ShowKind.Single)]); var resolver = Substitute.For(); resolver.Resolve(Provider).Returns(provider); resolver.AvailableKeys.Returns([Provider]); await using var db = fixture.New(); var applier = new ShowMetadataApplier( db, resolver, Substitute.For(), Substitute.For(), new GenreMatcher(db) ); var result = await new AutoAttachMovieCommandHandler( db, resolver, new MovieMatcher(db, resolver), applier ).Handle( new AutoAttachMovieCommand(asset.Id, "Voron.1994.TRIPLE.mkv"), CancellationToken.None ); Assert.True(result.IsSuccess); Assert.False(result.Value); Assert.Empty(await db.Shows.ToListAsync(CancellationToken.None)); } }