Add error handling for future schedule conflicts and media asset usage
Introduced new error types for handling conflicts when attempting to delete shows or media assets that are currently scheduled or in use. Updated the DeleteShowCommandHandler and DeleteMediaAssetCommandHandler to check for these conditions and return appropriate error responses. This enhances the robustness of the application by preventing unintended deletions and improving user feedback.
This commit is contained in:
@@ -0,0 +1,231 @@
|
||||
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;
|
||||
|
||||
/// <summary>
|
||||
/// Удаление занятой сущности: ссылки на шоу, ассет и группу живут в таблицах без внешних ключей
|
||||
/// (или с Restrict), поэтому БД такое удаление либо пропустит с висячей ссылкой, либо уронит
|
||||
/// исключением. Проверяем, что до неё дело не доходит — возвращается управляемая ошибка.
|
||||
/// </summary>
|
||||
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, new GroupStatsService(db, new GroupElementResolver(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);
|
||||
}
|
||||
|
||||
/// <summary>Отыгранный эфир шоу не держит: иначе его нельзя было бы удалить никогда.</summary>
|
||||
[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<IMediaStorage>();
|
||||
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, bumpersEnabled: false, 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<IMediaStorage>()
|
||||
).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);
|
||||
}
|
||||
|
||||
/// <summary>Запасная группа шаблона внешнего ключа не имеет — её пришлось бы ловить отдельно.</summary>
|
||||
[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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user