Enhance show deletion functionality to support media removal option
ci / build-backend (push) Successful in 1m32s
ci / build-frontend (push) Successful in 1m7s
ci / tests (push) Successful in 1m45s
ci / sonar (push) Failing after 31s

Updated the DeleteShowCommand and its handler to include an optional parameter for media deletion, allowing users to remove associated media files when deleting a show. Refactored related components and API calls to accommodate this new feature, ensuring a seamless user experience. Additionally, updated localization strings to reflect the new functionality and adjusted tests to verify the correct behavior of the deletion process with and without media.
This commit is contained in:
Leonid Pershin
2026-07-28 11:06:52 +03:00
parent 6ca629c13a
commit 2e76fa1452
16 changed files with 368 additions and 71 deletions
@@ -8,6 +8,7 @@ using TeleWave.Application.Broadcast.ListChannels;
using TeleWave.Application.Common.Interfaces;
using TeleWave.Application.Library.DeleteShow;
using TeleWave.Application.Library.GetShow;
using TeleWave.Application.Media;
using TeleWave.Application.Programming.Groups;
using TeleWave.Application.Tests.Support;
using TeleWave.Domain.Broadcast;
@@ -24,6 +25,9 @@ public class QueryHandlersTests
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 GetChannel_UnknownId_ReturnsNotFound()
{
@@ -153,13 +157,13 @@ public class QueryHandlersTests
}
await using var db = fixture.New();
var missing = await new DeleteShowCommandHandler(db, GroupCleaner(db)).Handle(
var missing = await new DeleteShowCommandHandler(db, GroupCleaner(db), Eraser(db)).Handle(
new DeleteShowCommand(Guid.NewGuid()),
CancellationToken.None
);
Assert.False(missing.IsSuccess);
var ok = await new DeleteShowCommandHandler(db, GroupCleaner(db)).Handle(
var ok = await new DeleteShowCommandHandler(db, GroupCleaner(db), Eraser(db)).Handle(
new DeleteShowCommand(show.Id),
CancellationToken.None
);
@@ -1,3 +1,4 @@
using Microsoft.EntityFrameworkCore;
using NSubstitute;
using TeleWave.Application.Common.Interfaces;
using TeleWave.Application.Library;
@@ -27,6 +28,9 @@ public class DeleteGuardsTests
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()
{
@@ -53,7 +57,7 @@ public class DeleteGuardsTests
}
await using var db = fixture.New();
var result = await new DeleteShowCommandHandler(db, GroupCleaner(db)).Handle(
var result = await new DeleteShowCommandHandler(db, GroupCleaner(db), Eraser(db)).Handle(
new DeleteShowCommand(show.Id),
CancellationToken.None
);
@@ -62,6 +66,41 @@ public class DeleteGuardsTests
Assert.Equal(ShowErrors.InFutureSchedule, result.Error);
}
/// <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()
@@ -81,7 +120,7 @@ public class DeleteGuardsTests
}
await using var db = fixture.New();
var result = await new DeleteShowCommandHandler(db, GroupCleaner(db)).Handle(
var result = await new DeleteShowCommandHandler(db, GroupCleaner(db), Eraser(db)).Handle(
new DeleteShowCommand(show.Id),
CancellationToken.None
);
@@ -1,8 +1,11 @@
using Microsoft.EntityFrameworkCore;
using NSubstitute;
using TeleWave.Application.Common.Interfaces;
using TeleWave.Application.Library.DeleteShow;
using TeleWave.Application.Maintenance.DeleteShowMedia;
using TeleWave.Application.Media;
using TeleWave.Application.Media.Register;
using TeleWave.Application.Programming.Groups;
using TeleWave.Domain.Broadcast;
using TeleWave.Domain.Library;
using TeleWave.Domain.Media;
@@ -94,10 +97,10 @@ public sealed class TransactionIntegrationTests(PostgresFixture fixture)
await using (var db = fixture.CreateContext())
{
var result = await new DeleteShowMediaCommandHandler(db, storage).Handle(
new DeleteShowMediaCommand(show.Id),
CancellationToken.None
);
var result = await new DeleteShowMediaCommandHandler(
db,
new ShowMediaEraser(db, storage)
).Handle(new DeleteShowMediaCommand(show.Id), CancellationToken.None);
Assert.True(result.IsSuccess);
}
@@ -110,4 +113,66 @@ public sealed class TransactionIntegrationTests(PostgresFixture fixture)
.Received()
.DeleteAssetArtifactsAsync(asset.Id, ".mkv", Arg.Any<CancellationToken>());
}
/// <summary>
/// Удаление шоу «вместе с медиа»: ассеты, записи расписания и файлы уходят одной операцией.
/// Проверяется на настоящем Postgres — <c>ExecuteDelete</c> и транзакции InMemory не умеет.
/// </summary>
[SkippableFact]
public async Task DeleteShow_WithMedia_RemovesEverything()
{
var channel = Channel.Create("Канал", "kanal", DateTimeOffset.UnixEpoch);
var show = Show.Create("Шоу", ShowKind.Single);
var asset = MediaAsset.Register("movie.mkv", ".mkv", MediaSource.Upload);
show.AddEpisode(asset.Id);
var entry = ScheduleEntry.Program(
channel.Id,
asset.Id,
DateTimeOffset.UnixEpoch,
DateTimeOffset.UnixEpoch.AddMinutes(20),
show.Id,
0
);
await using (var seed = fixture.CreateContext())
{
seed.Channels.Add(channel);
seed.Shows.Add(show);
seed.MediaAssets.Add(asset);
seed.ScheduleEntries.Add(entry);
await seed.SaveChangesAsync(CancellationToken.None);
}
var storage = Substitute.For<IMediaStorage>();
await using (var db = fixture.CreateContext())
{
var result = await new DeleteShowCommandHandler(
db,
new GroupMembershipCleaner(
db,
new GroupStatsService(
db,
new GroupElementResolver(db),
new DynamicGroupResolver(
new GroupFilterMatcher(db),
new GroupElementResolver(db)
)
)
),
new ShowMediaEraser(db, storage)
).Handle(new DeleteShowCommand(show.Id, WithMedia: true), CancellationToken.None);
Assert.True(result.IsSuccess);
await db.SaveChangesAsync(CancellationToken.None);
}
await using var verify = fixture.CreateContext();
Assert.False(await verify.Shows.AnyAsync(s => s.Id == show.Id));
Assert.False(await verify.MediaAssets.AnyAsync(a => a.Id == asset.Id));
Assert.False(await verify.ScheduleEntries.AnyAsync(e => e.MediaAssetId == asset.Id));
await storage
.Received()
.DeleteAssetArtifactsAsync(asset.Id, ".mkv", Arg.Any<CancellationToken>());
}
}