Files
TeleWave/backend/tests/TeleWave.Application.Tests/Library/DeleteGuardsTests.cs
T
Leonid Pershin f924891f3b
ci / build-backend (push) Successful in 1m35s
ci / build-frontend (push) Successful in 51s
ci / tests (push) Successful in 1m46s
ci / sonar (push) Successful in 4m43s
Refactor error handling in Bumper, Media, Group, and Junction components to include contextual information
Updated error definitions in BumperErrors, MediaErrors, GroupErrors, and TemplateErrors to accept a parameter for contextual information, enhancing the clarity of error messages. Refactored related command handlers to utilize these updated error messages, ensuring that users receive specific details about the usage context when attempting to delete resources. Adjusted unit tests to verify the correctness of the new error handling logic.
2026-07-28 13:19:30 +03:00

306 lines
11 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using Microsoft.EntityFrameworkCore;
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, GroupServices.Stats(db));
private static ShowMediaEraser Eraser(IAppDbContext db) =>
new(db, Substitute.For<IMediaStorage>());
[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 channel = Channel.Create("Первый", "first", T0);
var future = DateTimeOffset.UtcNow.AddHours(1);
await using (var seed = fixture.New())
{
seed.Shows.Add(show);
seed.MediaAssets.Add(asset);
seed.Channels.Add(channel);
seed.ScheduleEntries.Add(
ScheduleEntry.Program(
channel.Id,
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), Eraser(db)).Handle(
new DeleteShowCommand(show.Id),
CancellationToken.None
);
Assert.False(result.IsSuccess);
Assert.Equal(ShowErrors.InFutureSchedule(string.Empty).Code, result.Error.Code);
// В тексте — канал и время: по ним видно, что именно пересобирать.
Assert.Contains("Первый", result.Error.Message, StringComparison.Ordinal);
}
/// <summary>
/// Без флага медиа переживает шоу: серия — это ссылка на ассет по значению, внешнего ключа
/// между ними нет, и каскад БД до файлов не доходит.
/// </summary>
[Fact]
public async Task DeleteShow_KeepsMedia_ByDefault()
{
var fixture = new TestDb();
var show = Show.Create("A", ShowKind.Single);
var asset = MediaAsset.Register("a.mkv", ".mkv", MediaSource.Upload);
show.AddEpisode(asset.Id);
await using (var seed = fixture.New())
{
seed.Shows.Add(show);
seed.MediaAssets.Add(asset);
await seed.SaveChangesAsync(CancellationToken.None);
}
await using (var db = fixture.New())
{
var result = await new DeleteShowCommandHandler(
db,
GroupCleaner(db),
Eraser(db)
).Handle(new DeleteShowCommand(show.Id), CancellationToken.None);
Assert.True(result.IsSuccess);
await db.SaveChangesAsync(CancellationToken.None);
}
await using var check = fixture.New();
Assert.Empty(await check.Shows.ToListAsync(CancellationToken.None));
Assert.Single(await check.MediaAssets.ToListAsync(CancellationToken.None));
}
/// <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), Eraser(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(string.Empty).Code, result.Error.Code);
// Файлы не тронуты: до чистки хранилища дело не дошло.
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<IMediaStorage>()
).Handle(new DeleteMediaAssetCommand(asset.Id), CancellationToken.None);
Assert.False(result.IsSuccess);
Assert.Equal(MediaErrors.InUse(string.Empty).Code, result.Error.Code);
}
[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(string.Empty).Code, result.Error.Code);
}
/// <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(string.Empty).Code, result.Error.Code);
}
/// <summary>
/// Отказ должен называть адрес: «используется в сетке» без имени канала отправляет обходить
/// их все руками. Связи полные — значит и канал в тексте есть.
/// </summary>
[Fact]
public async Task DeleteGroup_UsedByChannel_NamesIt()
{
var fixture = new TestDb();
var group = Group.Create("G");
var channel = Channel.Create("Первый", $"first-{Guid.NewGuid():N}", T0);
var template = ScheduleTemplate.Create(channel.Id, "Сетка");
template.SetFallbackGroup(group.Id);
await using (var seed = fixture.New())
{
seed.Groups.Add(group);
seed.Channels.Add(channel);
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.Contains("Первый", result.Error.Message, StringComparison.Ordinal);
}
[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);
}
}