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.
179 lines
6.7 KiB
C#
179 lines
6.7 KiB
C#
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;
|
|
using Xunit;
|
|
|
|
namespace TeleWave.Integration.Tests;
|
|
|
|
[Collection("postgres")]
|
|
public sealed class TransactionIntegrationTests(PostgresFixture fixture)
|
|
{
|
|
[SkippableFact]
|
|
public async Task RegisterMediaAsset_RollsBack_WhenFilePromoteFails()
|
|
{
|
|
Skip.IfNot(fixture.Available, "Docker недоступен");
|
|
var fileName = $"clip-{Guid.NewGuid():N}.mkv";
|
|
var storage = Substitute.For<IMediaStorage>();
|
|
storage
|
|
.PromoteToOriginalAsync(
|
|
Arg.Any<MediaSource>(),
|
|
Arg.Any<string>(),
|
|
Arg.Any<Guid>(),
|
|
Arg.Any<string>(),
|
|
Arg.Any<CancellationToken>()
|
|
)
|
|
.Returns(Task.FromException(new IOException("disk full")));
|
|
|
|
await using var db = fixture.CreateContext();
|
|
var handler = new RegisterMediaAssetCommandHandler(db, storage);
|
|
|
|
await Assert.ThrowsAsync<IOException>(() =>
|
|
handler.Handle(
|
|
new RegisterMediaAssetCommand(fileName, MediaSource.Upload, fileName),
|
|
CancellationToken.None
|
|
)
|
|
);
|
|
|
|
// Перенос файла упал — регистрация должна откатиться, чтобы не осталось orphan-строки.
|
|
await using var verify = fixture.CreateContext();
|
|
var exists = await verify.MediaAssets.AnyAsync(a => a.OriginalFileName == fileName);
|
|
Assert.False(exists);
|
|
}
|
|
|
|
[SkippableFact]
|
|
public async Task DeleteShowMedia_AtomicallyRemovesEntriesAssetsAndEpisodes()
|
|
{
|
|
Skip.IfNot(fixture.Available, "Docker недоступен");
|
|
var channel = Channel.Create("c", $"c-{Guid.NewGuid():N}", DateTimeOffset.UnixEpoch);
|
|
var show = Show.Create("Show", ShowKind.Series);
|
|
var asset = MediaAsset.Register("ep.mkv", ".mkv", MediaSource.Upload);
|
|
asset.MarkReady(
|
|
new MediaReadyInfo(
|
|
TimeSpan.FromMinutes(20),
|
|
2,
|
|
600,
|
|
1920,
|
|
1080,
|
|
"h264",
|
|
"aac",
|
|
"assets/x"
|
|
)
|
|
);
|
|
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>();
|
|
storage
|
|
.DeleteAssetArtifactsAsync(
|
|
Arg.Any<Guid>(),
|
|
Arg.Any<string>(),
|
|
Arg.Any<CancellationToken>()
|
|
)
|
|
.Returns(Task.CompletedTask);
|
|
|
|
await using (var db = fixture.CreateContext())
|
|
{
|
|
var result = await new DeleteShowMediaCommandHandler(
|
|
db,
|
|
new ShowMediaEraser(db, storage)
|
|
).Handle(new DeleteShowMediaCommand(show.Id), CancellationToken.None);
|
|
Assert.True(result.IsSuccess);
|
|
}
|
|
|
|
await using var verify = fixture.CreateContext();
|
|
Assert.False(await verify.MediaAssets.AnyAsync(a => a.Id == asset.Id));
|
|
Assert.False(await verify.ScheduleEntries.AnyAsync(e => e.MediaAssetId == asset.Id));
|
|
var reloaded = await verify.Shows.Include(s => s.Episodes).FirstAsync(s => s.Id == show.Id);
|
|
Assert.Empty(reloaded.Episodes);
|
|
await storage
|
|
.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>());
|
|
}
|
|
}
|