Files
TeleWave/backend/tests/TeleWave.Application.Tests/Library/DeleteGuardsTests.cs
T
Leonid Pershin 7a164dbd90
ci / build-backend (push) Successful in 1m18s
ci / build-frontend (push) Successful in 39s
ci / tests (push) Successful in 1m22s
ci / sonar (push) Successful in 3m41s
Enhance channel settings to include icon image support for IPTV
Updated the Channel and related DTOs to incorporate an optional IconImageId property, allowing for the specification of a channel icon for external IPTV players. Modified the UpdateChannelSettings command and its handler to accommodate this new property. Adjusted frontend components to manage the icon image, including selection and display functionality. Updated localization files to support new UI elements related to the IPTV icon. This enhancement improves the user experience by providing a visual representation of channels in IPTV applications.
2026-08-02 17:07:24 +03:00

332 lines
12 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, null);
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_ReleasesReference()
{
var fixture = new TestDb();
var group = Group.Create("G");
var template = ScheduleTemplate.Create(Guid.NewGuid(), "Сетка");
template.SetFallbackGroup(group.Id);
template.MarkApplied();
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.True(result.IsSuccess);
await db.SaveChangesAsync(CancellationToken.None);
}
await using var check = fixture.New();
var stored = await check.ScheduleTemplates.FirstAsync(t => t.Id == template.Id);
Assert.Null(stored.FallbackGroupId);
Assert.True(stored.HasPendingChanges);
Assert.False(await check.Groups.AnyAsync(g => g.Id == group.Id));
}
/// <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, "Сетка");
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.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);
}
}