using NSubstitute; using TeleWave.Application.Common.Interfaces; using TeleWave.Application.Media.ManualInbox; using TeleWave.Domain.Library; using TeleWave.Domain.Media; using TeleWave.Infrastructure.Persistence; using Xunit; namespace TeleWave.Integration.Tests; /// /// Ручной разбор manual/: выбранные файлы уходят из каталога в шоу. Проверяется против /// настоящей БД, потому что вся суть операции — в связке «ассет + серия шоу + файл на диске». /// [Collection("postgres")] public sealed class ManualInboxIntegrationTests(PostgresFixture fixture) { [SkippableFact] public async Task Import_AddsEpisodesToShow_AndConsumesFiles() { Skip.IfNot(fixture.Available, "Docker недоступен"); await using var seedDb = fixture.CreateContext(); var showId = await SeedShowAsync(seedDb); var tag = Guid.NewGuid().ToString("N")[..8]; var first = $"{tag}-s01e01.mkv"; var second = $"{tag}-s01e02.mkv"; var storage = Substitute.For(); storage .ListManualInbox(Arg.Any()) .Returns( [ new IMediaStorage.ManualInboxFile($"Сериал/{first}", first, 1000), new IMediaStorage.ManualInboxFile($"Сериал/{second}", second, 1000), ] ); await using var db = fixture.CreateContext(); var result = await new ImportManualInboxCommandHandler( db, storage, Substitute.For() ).Handle( new ImportManualInboxCommand( [Item($"Сериал/{first}"), Item($"Сериал/{second}")], showId ), default ); Assert.True(result.IsSuccess); Assert.Equal(2, result.Value.Imported); Assert.Empty(result.Value.Failed); await db.SaveChangesAsync(); // Файлы должны быть перенесены из manual/ — ровно как из обычного inbox. await storage .Received(2) .PromoteToOriginalAsync( MediaSource.ManualInbox, Arg.Any(), Arg.Any(), ".mkv", Arg.Any() ); await using var verify = fixture.CreateContext(); var episodes = verify .Shows.Where(s => s.Id == showId) .SelectMany(s => s.Episodes) .OrderBy(e => e.Position) .ToList(); Assert.Equal(2, episodes.Count); // Номера сезона и серии разбираются из имени файла, как при обычном добавлении. Assert.Equal(1, episodes[0].Season); Assert.Equal(1, episodes[0].Episode); Assert.Equal(2, episodes[1].Episode); // Считаем ассеты своего шоу: база в интеграционных тестах общая, и соседний тест тоже // заводит записи из ручного inbox. var assetIds = episodes.Select(e => e.MediaAssetId).ToList(); var assets = verify.MediaAssets.Where(a => assetIds.Contains(a.Id)).ToList(); Assert.Equal(2, assets.Count); Assert.All(assets, a => Assert.Equal(MediaSource.ManualInbox, a.Source)); Assert.All(assets, a => Assert.Equal(MediaAssetStatus.Pending, a.Status)); } [SkippableFact] public async Task Import_SkipsUnsupportedAndMissing_WithoutFailingTheBatch() { Skip.IfNot(fixture.Available, "Docker недоступен"); await using var seedDb = fixture.CreateContext(); var showId = await SeedShowAsync(seedDb); var tag = Guid.NewGuid().ToString("N")[..8]; var good = $"{tag}-ok.mkv"; var storage = Substitute.For(); storage .ListManualInbox(Arg.Any()) .Returns( [ new IMediaStorage.ManualInboxFile(good, good, 1000), new IMediaStorage.ManualInboxFile("readme.txt", "readme.txt", 10), ] ); await using var db = fixture.CreateContext(); var result = await new ImportManualInboxCommandHandler( db, storage, Substitute.For() ).Handle( new ImportManualInboxCommand( [Item(good), Item("readme.txt"), Item("ушёл.mkv")], showId ), default ); // Один взяли, два отклонили — и каждый со своей причиной, а не одной ошибкой на пакет. Assert.True(result.IsSuccess); Assert.Equal(1, result.Value.Imported); Assert.Equal(2, result.Value.Failed.Count); Assert.Contains(result.Value.Failed, f => f.RelativePath == "readme.txt"); Assert.Contains(result.Value.Failed, f => f.RelativePath == "ушёл.mkv"); } [SkippableFact] public async Task Import_WithExplicitNumbers_UsesThemInsteadOfFileName() { Skip.IfNot(fixture.Available, "Docker недоступен"); await using var seedDb = fixture.CreateContext(); var showId = await SeedShowAsync(seedDb); var name = $"{Guid.NewGuid():N}"[..8] + "-без-номеров.mkv"; var storage = Substitute.For(); storage .ListManualInbox(Arg.Any()) .Returns([new IMediaStorage.ManualInboxFile(name, name, 1000)]); await using var db = fixture.CreateContext(); var result = await new ImportManualInboxCommandHandler( db, storage, Substitute.For() ).Handle(new ImportManualInboxCommand([new ManualImportItem(name, 4, 12)], showId), default); Assert.True(result.IsSuccess); await db.SaveChangesAsync(); await using var verify = fixture.CreateContext(); var episode = verify.Shows.Where(s => s.Id == showId).SelectMany(s => s.Episodes).Single(); // Из имени номера не вытащить — значит сохранилось ровно то, что видел пользователь. Assert.Equal(4, episode.Season); Assert.Equal(12, episode.Episode); // И спутники рядом с забранным файлом убираются. await storage.Received(1).CleanupManualLeftoversAsync(name, Arg.Any()); } /// Файл без явных номеров — сервер разбирает имя сам, как при обычном добавлении. private static ManualImportItem Item(string relativePath) => new(relativePath, null, null); private static async Task SeedShowAsync(AppDbContext db) { var show = Show.Create($"Сериал {Guid.NewGuid():N}"[..20], ShowKind.Series); db.Shows.Add(show); await db.SaveChangesAsync(); return show.Id; } }