using NSubstitute; using TeleWave.Application.Common.Interfaces; using TeleWave.Application.Library; using TeleWave.Application.Library.DeleteShow; using TeleWave.Application.Media; using TeleWave.Application.Media.Delete; using TeleWave.Application.Programming.Groups; using TeleWave.Application.Programming.Groups.DeleteGroup; using TeleWave.Application.Tests.Support; using TeleWave.Domain.Broadcast; using TeleWave.Domain.Library; using TeleWave.Domain.Media; using TeleWave.Domain.Programming; using Xunit; namespace TeleWave.Application.Tests.Library; /// /// Удаление занятой сущности: ссылки на шоу, ассет и группу живут в таблицах без внешних ключей /// (или с Restrict), поэтому БД такое удаление либо пропустит с висячей ссылкой, либо уронит /// исключением. Проверяем, что до неё дело не доходит — возвращается управляемая ошибка. /// public class DeleteGuardsTests { private static readonly DateTimeOffset T0 = new(2026, 1, 1, 0, 0, 0, TimeSpan.Zero); private static GroupMembershipCleaner GroupCleaner(IAppDbContext db) => new(db, GroupServices.Stats(db)); [Fact] public async Task DeleteShow_InFutureSchedule_Fails() { var fixture = new TestDb(); var show = Show.Create("A", ShowKind.Series); var asset = MediaAsset.Register("a.mkv", ".mkv", MediaSource.Upload); var future = DateTimeOffset.UtcNow.AddHours(1); await using (var seed = fixture.New()) { seed.Shows.Add(show); seed.MediaAssets.Add(asset); seed.ScheduleEntries.Add( ScheduleEntry.Program( Guid.NewGuid(), asset.Id, future, future.AddMinutes(20), show.Id, 0 ) ); await seed.SaveChangesAsync(CancellationToken.None); } await using var db = fixture.New(); var result = await new DeleteShowCommandHandler(db, GroupCleaner(db)).Handle( new DeleteShowCommand(show.Id), CancellationToken.None ); Assert.False(result.IsSuccess); Assert.Equal(ShowErrors.InFutureSchedule, result.Error); } /// Отыгранный эфир шоу не держит: иначе его нельзя было бы удалить никогда. [Fact] public async Task DeleteShow_OnlyPastSchedule_Succeeds() { var fixture = new TestDb(); var show = Show.Create("A", ShowKind.Series); var asset = MediaAsset.Register("a.mkv", ".mkv", MediaSource.Upload); await using (var seed = fixture.New()) { seed.Shows.Add(show); seed.MediaAssets.Add(asset); seed.ScheduleEntries.Add( ScheduleEntry.Program(Guid.NewGuid(), asset.Id, T0, T0.AddMinutes(20), show.Id, 0) ); await seed.SaveChangesAsync(CancellationToken.None); } await using var db = fixture.New(); var result = await new DeleteShowCommandHandler(db, GroupCleaner(db)).Handle( new DeleteShowCommand(show.Id), CancellationToken.None ); Assert.True(result.IsSuccess); } [Fact] public async Task DeleteMediaAsset_InSchedule_Fails() { var fixture = new TestDb(); var asset = MediaAsset.Register("a.mkv", ".mkv", MediaSource.Upload); await using (var seed = fixture.New()) { seed.MediaAssets.Add(asset); seed.ScheduleEntries.Add( ScheduleEntry.Ad(Guid.NewGuid(), asset.Id, T0, T0.AddMinutes(1)) ); await seed.SaveChangesAsync(CancellationToken.None); } var storage = Substitute.For(); await using var db = fixture.New(); var result = await new DeleteMediaAssetCommandHandler(db, storage).Handle( new DeleteMediaAssetCommand(asset.Id), CancellationToken.None ); Assert.False(result.IsSuccess); Assert.Equal(MediaErrors.InUse, result.Error); // Файлы не тронуты: до чистки хранилища дело не дошло. await storage .DidNotReceiveWithAnyArgs() .DeleteAssetArtifactsAsync(default, default!, default); } [Fact] public async Task DeleteMediaAsset_ChannelFiller_Fails() { var fixture = new TestDb(); var asset = MediaAsset.Register("filler.mkv", ".mkv", MediaSource.Upload); var channel = Channel.Create("c", "c", T0); channel.UpdateSettings(channel.Name, isEnabled: true, asset.Id); await using (var seed = fixture.New()) { seed.MediaAssets.Add(asset); seed.Channels.Add(channel); await seed.SaveChangesAsync(CancellationToken.None); } await using var db = fixture.New(); var result = await new DeleteMediaAssetCommandHandler( db, Substitute.For() ).Handle(new DeleteMediaAssetCommand(asset.Id), CancellationToken.None); Assert.False(result.IsSuccess); Assert.Equal(MediaErrors.InUse, result.Error); } [Fact] public async Task DeleteGroup_UsedBySlot_Fails() { var fixture = new TestDb(); var group = Group.Create("G"); var template = ScheduleTemplate.Create(Guid.NewGuid(), "Сетка"); var layer = template.AddLayer("Базовый", 10); var slot = layer.AddSlot("Блок", new TimeOnly(6, 0), 60); slot.UpdateContent( new SlotContent( slot.Title, SlotKind.Content, group.Id, null, null, SlotBlockMode.FillSlot, 1, OverflowPolicy.ContinueNext ) ); await using (var seed = fixture.New()) { seed.Groups.Add(group); seed.ScheduleTemplates.Add(template); await seed.SaveChangesAsync(CancellationToken.None); } await using var db = fixture.New(); var result = await new DeleteGroupCommandHandler(db).Handle( new DeleteGroupCommand(group.Id), CancellationToken.None ); Assert.False(result.IsSuccess); Assert.Equal(GroupErrors.InUse, result.Error); } /// Запасная группа шаблона внешнего ключа не имеет — её пришлось бы ловить отдельно. [Fact] public async Task DeleteGroup_UsedAsTemplateFallback_Fails() { var fixture = new TestDb(); var group = Group.Create("G"); var template = ScheduleTemplate.Create(Guid.NewGuid(), "Сетка"); template.SetFallbackGroup(group.Id); await using (var seed = fixture.New()) { seed.Groups.Add(group); seed.ScheduleTemplates.Add(template); await seed.SaveChangesAsync(CancellationToken.None); } await using var db = fixture.New(); var result = await new DeleteGroupCommandHandler(db).Handle( new DeleteGroupCommand(group.Id), CancellationToken.None ); Assert.False(result.IsSuccess); Assert.Equal(GroupErrors.InUse, result.Error); } [Fact] public async Task DeleteGroup_Unreferenced_Succeeds() { var fixture = new TestDb(); var group = Group.Create("G"); await using (var seed = fixture.New()) { seed.Groups.Add(group); await seed.SaveChangesAsync(CancellationToken.None); } await using var db = fixture.New(); var result = await new DeleteGroupCommandHandler(db).Handle( new DeleteGroupCommand(group.Id), CancellationToken.None ); Assert.True(result.IsSuccess); } }