Added new BumperEndpoints to the API for managing bumper templates and variants, enhancing the channel management capabilities. Removed outdated bumper-related commands and handlers from the application, streamlining the codebase and improving maintainability. Updated ChannelEndpoints to reflect these changes and ensure proper routing for the new endpoints.
208 lines
8.1 KiB
C#
208 lines
8.1 KiB
C#
using Microsoft.Extensions.Options;
|
|
using NSubstitute;
|
|
using TeleWave.Application.Broadcast.Bumpers;
|
|
using TeleWave.Application.Common.Interfaces;
|
|
using TeleWave.Application.Streaming;
|
|
using TeleWave.Application.Tests.Support;
|
|
using TeleWave.Domain.Broadcast;
|
|
using TeleWave.Domain.Images;
|
|
using TeleWave.Domain.Library;
|
|
using TeleWave.Domain.Programming;
|
|
using TeleWave.Infrastructure.Persistence;
|
|
using Xunit;
|
|
|
|
namespace TeleWave.Application.Tests.Broadcast;
|
|
|
|
/// <summary>
|
|
/// Превью ТВ-заставки: рендерится каждый подблок блока, длительность выравнивается по длине
|
|
/// сегмента HLS, а плейсхолдеры подставляются образцами выбранного канала — блок общий, но
|
|
/// смотреть на него надо глазами конкретного канала.
|
|
/// </summary>
|
|
public class BumperPreviewTests
|
|
{
|
|
private static readonly DateTimeOffset T0 = new(2026, 1, 1, 0, 0, 0, TimeSpan.Zero);
|
|
|
|
private static RenderBumperPreviewCommandHandler Handler(
|
|
AppDbContext db,
|
|
IBumperRenderer? renderer = null,
|
|
IBumperTemplateStorage? storage = null,
|
|
IImageStore? imageStore = null
|
|
) =>
|
|
new(
|
|
db,
|
|
renderer ?? Substitute.For<IBumperRenderer>(),
|
|
storage ?? Substitute.For<IBumperTemplateStorage>(),
|
|
imageStore ?? Substitute.For<IImageStore>(),
|
|
Options.Create(new BumperOptions()),
|
|
Options.Create(new StreamingOptions { SegmentSeconds = 5 })
|
|
);
|
|
|
|
private static BumperTemplate NewTemplate()
|
|
{
|
|
var template = BumperTemplate.Create("Блок", "Текст 1");
|
|
template
|
|
.Variants[0]
|
|
.SetLines([
|
|
BumperLine.Create(0, BumperLineStyle.Label, BumperLineColor.Accent, "СЕЙЧАС"),
|
|
BumperLine.Create(1, BumperLineStyle.Title, BumperLineColor.Text, "{now.title}"),
|
|
BumperLine.Create(2, BumperLineStyle.Label, BumperLineColor.Accent, "ДАЛЕЕ"),
|
|
BumperLine.Create(3, BumperLineStyle.Title, BumperLineColor.Text, "{next.title}"),
|
|
]);
|
|
return template;
|
|
}
|
|
|
|
private static IReadOnlyList<BumperRenderSpec> Specs(IBumperRenderer renderer) =>
|
|
renderer
|
|
.ReceivedCalls()
|
|
.Select(c => c.GetArguments()[1])
|
|
.OfType<BumperRenderSpec>()
|
|
.ToList();
|
|
|
|
[Fact]
|
|
public async Task Render_UnknownTemplate_ReturnsNotFound()
|
|
{
|
|
var fixture = new TestDb();
|
|
await using var db = fixture.New();
|
|
|
|
var result = await Handler(db)
|
|
.Handle(new RenderBumperPreviewCommand(Guid.NewGuid(), null), CancellationToken.None);
|
|
|
|
Assert.Equal(BumperErrors.TemplateNotFound, result.Error);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Render_DrawsEveryVariant_WithChannelSamplesAndAlignedDuration()
|
|
{
|
|
var fixture = new TestDb();
|
|
var channel = Channel.Create("Первый", "one", T0);
|
|
var template = NewTemplate();
|
|
var second = template.AddVariant("Второй подблок");
|
|
second.SetLines([
|
|
BumperLine.Create(0, BumperLineStyle.Title, BumperLineColor.Text, "{channel}"),
|
|
]);
|
|
|
|
// Шоу канала: слот сетки ссылается на группу, в группе — шоу.
|
|
var show = Show.Create("Наше шоу", ShowKind.Series);
|
|
var group = Group.Create("Кино");
|
|
group.AddElement(GroupElementKind.Show, show.Id);
|
|
var grid = ScheduleTemplate.Create(channel.Id, "Сетка");
|
|
var slot = grid.Background!.AddSlot("Круглосуточно", new TimeOnly(6, 0), 24 * 60);
|
|
slot.UpdateContent(
|
|
new SlotContent(
|
|
slot.Title,
|
|
SlotKind.Content,
|
|
group.Id,
|
|
null,
|
|
null,
|
|
SlotBlockMode.FillSlot,
|
|
1,
|
|
OverflowPolicy.ContinueNext
|
|
)
|
|
);
|
|
channel.SetTemplate(grid.Id);
|
|
|
|
await using (var seed = fixture.New())
|
|
{
|
|
seed.Channels.Add(channel);
|
|
seed.BumperTemplates.Add(template);
|
|
seed.Shows.Add(show);
|
|
seed.Groups.Add(group);
|
|
seed.ScheduleTemplates.Add(grid);
|
|
await seed.SaveChangesAsync(CancellationToken.None);
|
|
}
|
|
|
|
var renderer = Substitute.For<IBumperRenderer>();
|
|
await using var db = fixture.New();
|
|
var result = await Handler(db, renderer)
|
|
.Handle(
|
|
new RenderBumperPreviewCommand(template.Id, channel.Id),
|
|
CancellationToken.None
|
|
);
|
|
|
|
Assert.True(result.IsSuccess);
|
|
// Оба подблока отрисованы, каждый — в свой ассет-превью.
|
|
await renderer
|
|
.Received(1)
|
|
.RenderAsync(
|
|
BumperPreview.AssetId(template.Variants[0].Id),
|
|
Arg.Any<BumperRenderSpec>(),
|
|
Arg.Any<CancellationToken>()
|
|
);
|
|
await renderer
|
|
.Received(1)
|
|
.RenderAsync(
|
|
BumperPreview.AssetId(second.Id),
|
|
Arg.Any<BumperRenderSpec>(),
|
|
Arg.Any<CancellationToken>()
|
|
);
|
|
|
|
var specs = Specs(renderer);
|
|
// Своего звука у блока нет: 8 секунд по умолчанию, выровненные вверх до сегмента в 5 сек.
|
|
Assert.All(specs, s => Assert.Equal(10, s.DurationSeconds));
|
|
|
|
var nowNext = specs.First(s => s.Lines.Count == 4);
|
|
Assert.Equal("Наше шоу", nowNext.Lines[1].Text);
|
|
|
|
var channelLine = specs.First(s => s.Lines.Count == 1);
|
|
Assert.Equal("Первый", channelLine.Lines[0].Text);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Render_WithBackgroundImage_ResolvesPathFromRegistry()
|
|
{
|
|
var fixture = new TestDb();
|
|
var template = NewTemplate();
|
|
var image = Image.Create(ImageCategory.BumperBackground, ".png", "bg.png");
|
|
template.SetBackgroundImage(image.Id);
|
|
|
|
await using (var seed = fixture.New())
|
|
{
|
|
seed.BumperTemplates.Add(template);
|
|
seed.Images.Add(image);
|
|
await seed.SaveChangesAsync(CancellationToken.None);
|
|
}
|
|
|
|
var renderer = Substitute.For<IBumperRenderer>();
|
|
var imageStore = Substitute.For<IImageStore>();
|
|
imageStore.ResolvePath(image.Id, ".png").Returns("/data/images/bg.png");
|
|
|
|
await using var db = fixture.New();
|
|
var result = await Handler(db, renderer, imageStore: imageStore)
|
|
.Handle(new RenderBumperPreviewCommand(template.Id, null), CancellationToken.None);
|
|
|
|
Assert.True(result.IsSuccess);
|
|
var spec = Specs(renderer).First();
|
|
Assert.Equal("/data/images/bg.png", spec.BackgroundFile);
|
|
// Канала нет — образцы заглушечные, но кадр всё равно собирается.
|
|
Assert.Equal("Первое шоу", spec.Lines[1].Text);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Render_DropsLinesThatResolveToNothing()
|
|
{
|
|
var fixture = new TestDb();
|
|
var template = BumperTemplate.Create("Блок", "Текст 1");
|
|
template
|
|
.Variants[0]
|
|
.SetLines([
|
|
BumperLine.Create(0, BumperLineStyle.Label, BumperLineColor.Accent, "{next.genre}"),
|
|
BumperLine.Create(1, BumperLineStyle.Title, BumperLineColor.Text, "{next.title}"),
|
|
]);
|
|
|
|
await using (var seed = fixture.New())
|
|
{
|
|
seed.BumperTemplates.Add(template);
|
|
await seed.SaveChangesAsync(CancellationToken.None);
|
|
}
|
|
|
|
var renderer = Substitute.For<IBumperRenderer>();
|
|
await using var db = fixture.New();
|
|
await Handler(db, renderer)
|
|
.Handle(new RenderBumperPreviewCommand(template.Id, null), CancellationToken.None);
|
|
|
|
// Жанра у образца нет — строка схлопнулась, а не оставила дыру в кадре.
|
|
var spec = Specs(renderer).First();
|
|
Assert.Single(spec.Lines);
|
|
}
|
|
}
|