using Microsoft.EntityFrameworkCore; using NSubstitute; using TeleWave.Application.Broadcast.Bumpers; using TeleWave.Application.Common.Interfaces; using TeleWave.Application.Images; using TeleWave.Application.Tests.Support; using TeleWave.Domain.Broadcast; using TeleWave.Domain.Images; using TeleWave.Domain.Programming; using Xunit; namespace TeleWave.Application.Tests.Broadcast; /// /// Команды блока заставки. Блок общий для всех каналов, поэтому удаление используемого запрещено: /// иначе оно молча выключило бы заставки в чужих каналах. /// public class BumperTemplateHandlersTests { private static async Task<(TestDb Fixture, BumperTemplate Template)> SeedAsync() { var fixture = new TestDb(); var template = BumperTemplate.Create("Блок", "Текст 1"); await using var seed = fixture.New(); seed.BumperTemplates.Add(template); await seed.SaveChangesAsync(CancellationToken.None); return (fixture, template); } [Fact] public async Task Create_StartsFromNowNextPreset() { var fixture = new TestDb(); await using var db = fixture.New(); var result = await new CreateBumperTemplateCommandHandler(db).Handle( new CreateBumperTemplateCommand("Ночной"), CancellationToken.None ); await db.SaveChangesAsync(CancellationToken.None); Assert.True(result.IsSuccess); await using var verify = fixture.New(); var template = await verify.BumperTemplates.Include(t => t.Variants).SingleAsync(); var variant = Assert.Single(template.Variants); // Пустой блок в редакторе выглядит поломанным — новый начинается с «Сейчас/Далее». Assert.Equal( ["СЕЙЧАС", "{now.title}", "ДАЛЕЕ", "{next.title}"], variant.Lines.OrderBy(l => l.Position).Select(l => l.Text) ); Assert.Equal(BumperFont.Sans, template.Font); } [Fact] public async Task UpdateStyle_BumpsRevision() { var (fixture, template) = await SeedAsync(); await using var db = fixture.New(); var result = await new UpdateBumperTemplateCommandHandler(db).Handle( new UpdateBumperTemplateCommand( template.Id, new BumperStyle( "Ночной", BumperFont.Serif, "0x111111", "0x222222", "0x333333", "black" ) ), CancellationToken.None ); await db.SaveChangesAsync(CancellationToken.None); Assert.True(result.IsSuccess); await using var verify = fixture.New(); var stored = await verify.BumperTemplates.AsNoTracking().SingleAsync(); Assert.Equal("Ночной", stored.Name); Assert.Equal(BumperFont.Serif, stored.Font); // Оформление входит в сигнатуру рендера: без роста ревизии в эфире остались бы старые кадры. Assert.Equal(1, stored.Revision); } [Fact] public async Task UpdateStyle_UnknownTemplate_ReturnsNotFound() { var fixture = new TestDb(); await using var db = fixture.New(); var result = await new UpdateBumperTemplateCommandHandler(db).Handle( new UpdateBumperTemplateCommand( Guid.NewGuid(), new BumperStyle("x", BumperFont.Sans, "a", "b", "c", "d") ), CancellationToken.None ); Assert.Equal(BumperErrors.TemplateNotFound, result.Error); } [Fact] public async Task Audio_SetAndClear_TouchStorageAndDuration() { var (fixture, template) = await SeedAsync(); var storage = Substitute.For(); await using (var db = fixture.New()) { var set = await new SetBumperTemplateAudioCommandHandler(db).Handle( new SetBumperTemplateAudioCommand(template.Id, ".mp3", 12.5), CancellationToken.None ); Assert.True(set.IsSuccess); await db.SaveChangesAsync(CancellationToken.None); } await using (var verify = fixture.New()) { var stored = await verify.BumperTemplates.AsNoTracking().SingleAsync(); Assert.Equal(".mp3", stored.AudioExtension); Assert.Equal(12.5, stored.AudioDurationSeconds); } await using (var db = fixture.New()) { var cleared = await new ClearBumperTemplateAudioCommandHandler(db, storage).Handle( new ClearBumperTemplateAudioCommand(template.Id), CancellationToken.None ); Assert.True(cleared.IsSuccess); await db.SaveChangesAsync(CancellationToken.None); } // Файл на диске должен уйти вместе с отметкой в базе. await storage.Received(1).DeleteAudioAsync(template.Id, Arg.Any()); await using var final = fixture.New(); Assert.Null((await final.BumperTemplates.AsNoTracking().SingleAsync()).AudioExtension); } [Fact] public async Task Audio_UnknownTemplate_ReturnsNotFound() { var fixture = new TestDb(); await using var db = fixture.New(); var storage = Substitute.For(); var set = await new SetBumperTemplateAudioCommandHandler(db).Handle( new SetBumperTemplateAudioCommand(Guid.NewGuid(), ".mp3", 1), CancellationToken.None ); var cleared = await new ClearBumperTemplateAudioCommandHandler(db, storage).Handle( new ClearBumperTemplateAudioCommand(Guid.NewGuid()), CancellationToken.None ); Assert.Equal(BumperErrors.TemplateNotFound, set.Error); Assert.Equal(BumperErrors.TemplateNotFound, cleared.Error); } [Fact] public async Task Background_SetAndClear_UseImageRegistry() { var (fixture, template) = await SeedAsync(); var image = Image.Create(ImageCategory.BumperBackground, ".png", "bg.png"); await using (var seed = fixture.New()) { seed.Images.Add(image); await seed.SaveChangesAsync(CancellationToken.None); } await using (var db = fixture.New()) { var unknownImage = await new SetBumperTemplateBackgroundCommandHandler(db).Handle( new SetBumperTemplateBackgroundCommand(template.Id, Guid.NewGuid()), CancellationToken.None ); Assert.Equal(ImageErrors.NotFound, unknownImage.Error); var set = await new SetBumperTemplateBackgroundCommandHandler(db).Handle( new SetBumperTemplateBackgroundCommand(template.Id, image.Id), CancellationToken.None ); Assert.True(set.IsSuccess); await db.SaveChangesAsync(CancellationToken.None); } await using (var verify = fixture.New()) Assert.Equal( image.Id, (await verify.BumperTemplates.AsNoTracking().SingleAsync()).BackgroundImageId ); await using (var db = fixture.New()) { var cleared = await new ClearBumperTemplateBackgroundCommandHandler(db).Handle( new ClearBumperTemplateBackgroundCommand(template.Id), CancellationToken.None ); Assert.True(cleared.IsSuccess); await db.SaveChangesAsync(CancellationToken.None); } await using var final = fixture.New(); Assert.Null((await final.BumperTemplates.AsNoTracking().SingleAsync()).BackgroundImageId); } [Fact] public async Task Background_UnknownTemplate_ReturnsNotFound() { var fixture = new TestDb(); await using var db = fixture.New(); var set = await new SetBumperTemplateBackgroundCommandHandler(db).Handle( new SetBumperTemplateBackgroundCommand(Guid.NewGuid(), Guid.NewGuid()), CancellationToken.None ); var cleared = await new ClearBumperTemplateBackgroundCommandHandler(db).Handle( new ClearBumperTemplateBackgroundCommand(Guid.NewGuid()), CancellationToken.None ); Assert.Equal(BumperErrors.TemplateNotFound, set.Error); Assert.Equal(BumperErrors.TemplateNotFound, cleared.Error); } [Fact] public async Task Delete_RefusesUsedTemplate_AndRemovesFreeOne() { var (fixture, template) = await SeedAsync(); var storage = Substitute.For(); var junction = JunctionTemplate.Create("Прайм"); await using (var seed = fixture.New()) { junction .AddElement(JunctionElementKind.Bumper) .Update( new JunctionElementSettings( JunctionElementKind.Bumper, null, template.Id, null, JunctionAmountMode.Count, 1, false, null, 1, null ) ); seed.JunctionTemplates.Add(junction); await seed.SaveChangesAsync(CancellationToken.None); } await using (var db = fixture.New()) { var used = await new DeleteBumperTemplateCommandHandler(db, storage).Handle( new DeleteBumperTemplateCommand(template.Id), CancellationToken.None ); Assert.Equal(BumperErrors.TemplateInUse, used.Error); } await using (var db = fixture.New()) { var stored = await db .JunctionTemplates.Include(j => j.Elements) .SingleAsync(j => j.Id == junction.Id); stored.RemoveElement(stored.Elements[0].Id); await db.SaveChangesAsync(CancellationToken.None); } await using (var db = fixture.New()) { var removed = await new DeleteBumperTemplateCommandHandler(db, storage).Handle( new DeleteBumperTemplateCommand(template.Id), CancellationToken.None ); Assert.True(removed.IsSuccess); await db.SaveChangesAsync(CancellationToken.None); } await storage.Received(1).DeleteAudioAsync(template.Id, Arg.Any()); await using var final = fixture.New(); Assert.Empty(final.BumperTemplates); } [Fact] public async Task Delete_UnknownTemplate_ReturnsNotFound() { var fixture = new TestDb(); await using var db = fixture.New(); var result = await new DeleteBumperTemplateCommandHandler( db, Substitute.For() ).Handle(new DeleteBumperTemplateCommand(Guid.NewGuid()), CancellationToken.None); Assert.Equal(BumperErrors.TemplateNotFound, result.Error); } [Fact] public async Task List_MapsTemplatesWithUsageCount() { var fixture = new TestDb(); var used = BumperTemplate.Create("Анонсовый", "Текст 1"); used.UpdateStyle( new BumperStyle( "Анонсовый", BumperFont.Serif, "0x111111", "0x222222", "0x333333", "white" ) ); used.SetAudio(".mp3", 9); used.Variants[0] .SetLines([ BumperLine.Create(0, BumperLineStyle.Title, BumperLineColor.Text, "{next.title}"), ]); var free = BumperTemplate.Create("Блок", "Текст 1"); var junction = JunctionTemplate.Create("Прайм"); foreach (var _ in Enumerable.Range(0, 2)) junction .AddElement(JunctionElementKind.Bumper) .Update( new JunctionElementSettings( JunctionElementKind.Bumper, null, used.Id, null, JunctionAmountMode.Count, 1, false, null, 1, null ) ); await using (var seed = fixture.New()) { seed.BumperTemplates.AddRange(used, free); seed.JunctionTemplates.Add(junction); await seed.SaveChangesAsync(CancellationToken.None); } await using var db = fixture.New(); var templates = await new ListBumperTemplatesQueryHandler(db).Handle( new ListBumperTemplatesQuery(), CancellationToken.None ); // Сортировка по имени: «Анонсовый» раньше «Блока». Assert.Equal(["Анонсовый", "Блок"], templates.Select(t => t.Name)); var dto = templates[0]; Assert.Equal(2, dto.UsageCount); Assert.Equal(BumperFont.Serif, dto.Font); Assert.True(dto.HasAudio); Assert.Equal(9, dto.AudioDurationSeconds); Assert.Equal("{next.title}", Assert.Single(Assert.Single(dto.Variants).Lines).Text); Assert.Equal(0, templates[1].UsageCount); } }