Implement BumperEndpoints and remove deprecated bumper-related functionality
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.
This commit is contained in:
@@ -1,6 +1,5 @@
|
||||
using Microsoft.Extensions.Options;
|
||||
using NSubstitute;
|
||||
using TeleWave.Application.Broadcast;
|
||||
using TeleWave.Application.Broadcast.Bumpers;
|
||||
using TeleWave.Application.Common.Interfaces;
|
||||
using TeleWave.Application.Streaming;
|
||||
@@ -16,8 +15,8 @@ namespace TeleWave.Application.Tests.Broadcast;
|
||||
|
||||
/// <summary>
|
||||
/// Превью ТВ-заставки: рендерится каждый подблок блока, длительность выравнивается по длине
|
||||
/// сегмента HLS, а названия «из/в» берутся из групп этого канала — иначе превью показывало бы
|
||||
/// случайные шоу из библиотеки.
|
||||
/// сегмента HLS, а плейсхолдеры подставляются образцами выбранного канала — блок общий, но
|
||||
/// смотреть на него надо глазами конкретного канала.
|
||||
/// </summary>
|
||||
public class BumperPreviewTests
|
||||
{
|
||||
@@ -38,49 +37,49 @@ public class BumperPreviewTests
|
||||
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_UnknownChannel_ReturnsNotFound()
|
||||
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(), Guid.NewGuid()),
|
||||
CancellationToken.None
|
||||
);
|
||||
.Handle(new RenderBumperPreviewCommand(Guid.NewGuid(), null), CancellationToken.None);
|
||||
|
||||
Assert.Equal(ChannelErrors.NotFound, result.Error);
|
||||
Assert.Equal(BumperErrors.TemplateNotFound, result.Error);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Render_UnknownTemplate_ReturnsBumperTemplateNotFound()
|
||||
public async Task Render_DrawsEveryVariant_WithChannelSamplesAndAlignedDuration()
|
||||
{
|
||||
var fixture = new TestDb();
|
||||
var channel = Channel.Create("Первый", "one", T0);
|
||||
await using (var seed = fixture.New())
|
||||
{
|
||||
seed.Channels.Add(channel);
|
||||
await seed.SaveChangesAsync(CancellationToken.None);
|
||||
}
|
||||
|
||||
await using var db = fixture.New();
|
||||
var result = await Handler(db)
|
||||
.Handle(
|
||||
new RenderBumperPreviewCommand(channel.Id, Guid.NewGuid()),
|
||||
CancellationToken.None
|
||||
);
|
||||
|
||||
Assert.Equal(ChannelErrors.BumperTemplateNotFound, result.Error);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Render_DrawsEveryVariant_WithChannelShowNamesAndAlignedDuration()
|
||||
{
|
||||
var fixture = new TestDb();
|
||||
var channel = Channel.Create("Первый", "one", T0);
|
||||
var template = channel.BumperTemplates[0];
|
||||
var template = NewTemplate();
|
||||
var second = template.AddVariant("Второй подблок");
|
||||
second.SetLines([
|
||||
BumperLine.Create(0, BumperLineStyle.Title, BumperLineColor.Text, "{channel}"),
|
||||
]);
|
||||
|
||||
// Шоу канала: слот сетки ссылается на группу, в группе — шоу.
|
||||
var show = Show.Create("Наше шоу", ShowKind.Series);
|
||||
@@ -105,6 +104,7 @@ public class BumperPreviewTests
|
||||
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);
|
||||
@@ -115,7 +115,7 @@ public class BumperPreviewTests
|
||||
await using var db = fixture.New();
|
||||
var result = await Handler(db, renderer)
|
||||
.Handle(
|
||||
new RenderBumperPreviewCommand(channel.Id, template.Id),
|
||||
new RenderBumperPreviewCommand(template.Id, channel.Id),
|
||||
CancellationToken.None
|
||||
);
|
||||
|
||||
@@ -136,32 +136,28 @@ public class BumperPreviewTests
|
||||
Arg.Any<CancellationToken>()
|
||||
);
|
||||
|
||||
var specs = renderer
|
||||
.ReceivedCalls()
|
||||
.Select(c => c.GetArguments()[1])
|
||||
.OfType<BumperRenderSpec>()
|
||||
.ToList();
|
||||
var specs = Specs(renderer);
|
||||
// Своего звука у блока нет: 8 секунд по умолчанию, выровненные вверх до сегмента в 5 сек.
|
||||
Assert.All(specs, s => Assert.Equal(10, s.DurationSeconds));
|
||||
Assert.All(specs, s => Assert.Equal("Наше шоу", s.NowTitle));
|
||||
// Второго шоу у канала нет — подставляется заглушка.
|
||||
Assert.All(specs, s => Assert.Equal("Второе шоу", s.NextTitle));
|
||||
Assert.All(specs, s => Assert.Null(s.BackgroundFile));
|
||||
Assert.All(specs, s => Assert.Null(s.PosterFile));
|
||||
|
||||
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 channel = Channel.Create("Первый", "one", T0);
|
||||
var template = channel.BumperTemplates[0];
|
||||
var template = NewTemplate();
|
||||
var image = Image.Create(ImageCategory.BumperBackground, ".png", "bg.png");
|
||||
template.SetBackgroundImage(image.Id);
|
||||
|
||||
await using (var seed = fixture.New())
|
||||
{
|
||||
seed.Channels.Add(channel);
|
||||
seed.BumperTemplates.Add(template);
|
||||
seed.Images.Add(image);
|
||||
await seed.SaveChangesAsync(CancellationToken.None);
|
||||
}
|
||||
@@ -172,19 +168,40 @@ public class BumperPreviewTests
|
||||
|
||||
await using var db = fixture.New();
|
||||
var result = await Handler(db, renderer, imageStore: imageStore)
|
||||
.Handle(
|
||||
new RenderBumperPreviewCommand(channel.Id, template.Id),
|
||||
CancellationToken.None
|
||||
);
|
||||
.Handle(new RenderBumperPreviewCommand(template.Id, null), CancellationToken.None);
|
||||
|
||||
Assert.True(result.IsSuccess);
|
||||
var spec = renderer
|
||||
.ReceivedCalls()
|
||||
.Select(c => c.GetArguments()[1])
|
||||
.OfType<BumperRenderSpec>()
|
||||
.First();
|
||||
var spec = Specs(renderer).First();
|
||||
Assert.Equal("/data/images/bg.png", spec.BackgroundFile);
|
||||
// Шоу у канала нет — обе подписи заглушечные.
|
||||
Assert.Equal("Первое шоу", spec.NowTitle);
|
||||
// Канала нет — образцы заглушечные, но кадр всё равно собирается.
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using TeleWave.Application.Broadcast;
|
||||
using TeleWave.Application.Broadcast.Bumpers;
|
||||
using TeleWave.Application.Tests.Support;
|
||||
using TeleWave.Domain.Broadcast;
|
||||
@@ -9,23 +8,25 @@ namespace TeleWave.Application.Tests.Broadcast;
|
||||
|
||||
public class BumperVariantHandlersTests
|
||||
{
|
||||
private static readonly DateTimeOffset T0 = new(2026, 1, 1, 0, 0, 0, TimeSpan.Zero);
|
||||
private static BumperTemplate NewTemplate() => BumperTemplate.Create("Блок", "Текст 1");
|
||||
|
||||
private static BumperLineDto Line(string text) =>
|
||||
new(BumperLineStyle.Title, BumperLineColor.Text, text);
|
||||
|
||||
[Fact]
|
||||
public async Task AddVariant_AutoNames_WhenBlank()
|
||||
public async Task AddVariant_StartsFromDefaultPreset()
|
||||
{
|
||||
var fixture = new TestDb();
|
||||
var channel = Channel.Create("c", "c", T0);
|
||||
var template = channel.BumperTemplates[0];
|
||||
var template = NewTemplate();
|
||||
await using (var seed = fixture.New())
|
||||
{
|
||||
seed.Channels.Add(channel);
|
||||
seed.BumperTemplates.Add(template);
|
||||
await seed.SaveChangesAsync(CancellationToken.None);
|
||||
}
|
||||
|
||||
await using var db = fixture.New();
|
||||
var result = await new AddBumperTextVariantCommandHandler(db).Handle(
|
||||
new AddBumperTextVariantCommand(channel.Id, template.Id, " "),
|
||||
var result = await new AddBumperVariantCommandHandler(db).Handle(
|
||||
new AddBumperVariantCommand(template.Id, "Текст 2"),
|
||||
CancellationToken.None
|
||||
);
|
||||
Assert.True(result.IsSuccess);
|
||||
@@ -33,60 +34,60 @@ public class BumperVariantHandlersTests
|
||||
|
||||
await using var verify = fixture.New();
|
||||
var stored = await verify
|
||||
.Channels.Include(c => c.BumperTemplates)
|
||||
.ThenInclude(t => t.Variants)
|
||||
.FirstAsync(c => c.Id == channel.Id);
|
||||
var variants = stored.BumperTemplates[0].Variants;
|
||||
Assert.Equal(2, variants.Count);
|
||||
Assert.Contains(variants, v => v.Name == "Текст 2");
|
||||
.BumperTemplates.Include(t => t.Variants)
|
||||
.FirstAsync(t => t.Id == template.Id);
|
||||
Assert.Equal(2, stored.Variants.Count);
|
||||
var added = stored.Variants.First(v => v.Name == "Текст 2");
|
||||
// Пустой подблок в редакторе выглядит поломанным — новый начинается с «Сейчас/Далее».
|
||||
Assert.Equal(4, added.Lines.Count);
|
||||
Assert.Contains(added.Lines, l => l.Text == "{next.title}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AddVariant_UnknownTemplate_ReturnsNotFound()
|
||||
{
|
||||
var fixture = new TestDb();
|
||||
var channel = Channel.Create("c", "c", T0);
|
||||
await using (var seed = fixture.New())
|
||||
{
|
||||
seed.Channels.Add(channel);
|
||||
await seed.SaveChangesAsync(CancellationToken.None);
|
||||
}
|
||||
|
||||
await using var db = fixture.New();
|
||||
var result = await new AddBumperTextVariantCommandHandler(db).Handle(
|
||||
new AddBumperTextVariantCommand(channel.Id, Guid.NewGuid(), "x"),
|
||||
|
||||
var result = await new AddBumperVariantCommandHandler(db).Handle(
|
||||
new AddBumperVariantCommand(Guid.NewGuid(), "x"),
|
||||
CancellationToken.None
|
||||
);
|
||||
Assert.Equal(ChannelErrors.BumperTemplateNotFound, result.Error);
|
||||
|
||||
Assert.Equal(BumperErrors.TemplateNotFound, result.Error);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UpdateVariant_ChangesFields()
|
||||
public async Task UpdateVariant_ReplacesLinesAndFields()
|
||||
{
|
||||
var fixture = new TestDb();
|
||||
var channel = Channel.Create("c", "c", T0);
|
||||
var template = channel.BumperTemplates[0];
|
||||
var template = NewTemplate();
|
||||
var variant = template.Variants[0];
|
||||
await using (var seed = fixture.New())
|
||||
{
|
||||
seed.Channels.Add(channel);
|
||||
seed.BumperTemplates.Add(template);
|
||||
await seed.SaveChangesAsync(CancellationToken.None);
|
||||
}
|
||||
|
||||
await using var db = fixture.New();
|
||||
var result = await new UpdateBumperTextVariantCommandHandler(db).Handle(
|
||||
new UpdateBumperTextVariantCommand(
|
||||
channel.Id,
|
||||
var result = await new UpdateBumperVariantCommandHandler(db).Handle(
|
||||
new UpdateBumperVariantCommand(
|
||||
template.Id,
|
||||
variant.Id,
|
||||
"Custom",
|
||||
BumperTextKind.Free,
|
||||
"NOW",
|
||||
"NEXT",
|
||||
"line1",
|
||||
"line2",
|
||||
BumperTrigger.Both,
|
||||
7
|
||||
new BumperVariantInput(
|
||||
"Custom",
|
||||
BumperTrigger.Both,
|
||||
BumperBackground.Template,
|
||||
7,
|
||||
[
|
||||
new BumperLineDto(
|
||||
BumperLineStyle.Label,
|
||||
BumperLineColor.Accent,
|
||||
"ДАЛЕЕ В {next.time}"
|
||||
),
|
||||
Line("{next.title}"),
|
||||
]
|
||||
)
|
||||
),
|
||||
CancellationToken.None
|
||||
);
|
||||
@@ -95,45 +96,75 @@ public class BumperVariantHandlersTests
|
||||
|
||||
await using var verify = fixture.New();
|
||||
var stored = await verify
|
||||
.Channels.Include(c => c.BumperTemplates)
|
||||
.ThenInclude(t => t.Variants)
|
||||
.FirstAsync(c => c.Id == channel.Id);
|
||||
var v = stored.BumperTemplates[0].Variants[0];
|
||||
.BumperTemplates.Include(t => t.Variants)
|
||||
.FirstAsync(t => t.Id == template.Id);
|
||||
var v = stored.Variants[0];
|
||||
Assert.Equal("Custom", v.Name);
|
||||
Assert.Equal(BumperTextKind.Free, v.Kind);
|
||||
Assert.Equal(7, v.Weight);
|
||||
Assert.Equal(BumperTrigger.Both, v.Trigger);
|
||||
Assert.Equal(BumperBackground.Template, v.Background);
|
||||
Assert.Equal(7, v.Weight);
|
||||
Assert.Equal(2, v.Lines.Count);
|
||||
Assert.Equal("ДАЛЕЕ В {next.time}", v.Lines[0].Text);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UpdateVariant_UnknownPlaceholder_IsRejected()
|
||||
{
|
||||
var fixture = new TestDb();
|
||||
var template = NewTemplate();
|
||||
var variant = template.Variants[0];
|
||||
await using (var seed = fixture.New())
|
||||
{
|
||||
seed.BumperTemplates.Add(template);
|
||||
await seed.SaveChangesAsync(CancellationToken.None);
|
||||
}
|
||||
|
||||
await using var db = fixture.New();
|
||||
var result = await new UpdateBumperVariantCommandHandler(db).Handle(
|
||||
new UpdateBumperVariantCommand(
|
||||
template.Id,
|
||||
variant.Id,
|
||||
new BumperVariantInput(
|
||||
"Custom",
|
||||
BumperTrigger.Both,
|
||||
BumperBackground.Template,
|
||||
1,
|
||||
[Line("Далее {next.tittle}")]
|
||||
)
|
||||
),
|
||||
CancellationToken.None
|
||||
);
|
||||
|
||||
// В эфире опечатка превратилась бы в пустоту, и заметить это было бы уже некому.
|
||||
Assert.Equal("Bumpers.UnknownPlaceholders", result.Error.Code);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RemoveVariant_CannotRemoveLast_ButRemovesExtra()
|
||||
{
|
||||
var fixture = new TestDb();
|
||||
var channel = Channel.Create("c", "c", T0);
|
||||
var template = channel.BumperTemplates[0];
|
||||
var template = NewTemplate();
|
||||
var only = template.Variants[0];
|
||||
var extra = template.AddVariant("Text 2");
|
||||
var extra = template.AddVariant("Текст 2");
|
||||
await using (var seed = fixture.New())
|
||||
{
|
||||
seed.Channels.Add(channel);
|
||||
seed.BumperTemplates.Add(template);
|
||||
await seed.SaveChangesAsync(CancellationToken.None);
|
||||
}
|
||||
|
||||
await using var db = fixture.New();
|
||||
var handler = new RemoveBumperTextVariantCommandHandler(db);
|
||||
|
||||
var okRemove = await handler.Handle(
|
||||
new RemoveBumperTextVariantCommand(channel.Id, template.Id, extra.Id),
|
||||
var okRemove = await new RemoveBumperVariantCommandHandler(db).Handle(
|
||||
new RemoveBumperVariantCommand(template.Id, extra.Id),
|
||||
CancellationToken.None
|
||||
);
|
||||
Assert.True(okRemove.IsSuccess);
|
||||
await db.SaveChangesAsync(CancellationToken.None);
|
||||
|
||||
await using var db2 = fixture.New();
|
||||
var lastGuard = await new RemoveBumperTextVariantCommandHandler(db2).Handle(
|
||||
new RemoveBumperTextVariantCommand(channel.Id, template.Id, only.Id),
|
||||
var lastGuard = await new RemoveBumperVariantCommandHandler(db2).Handle(
|
||||
new RemoveBumperVariantCommand(template.Id, only.Id),
|
||||
CancellationToken.None
|
||||
);
|
||||
Assert.Equal(ChannelErrors.CannotRemoveLastBumperTextVariant, lastGuard.Error);
|
||||
Assert.Equal(BumperErrors.CannotRemoveLastVariant, lastGuard.Error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,26 +7,23 @@ using Xunit;
|
||||
namespace TeleWave.Application.Tests.Broadcast;
|
||||
|
||||
/// <summary>
|
||||
/// Карточка канала: собственные свойства канала плюс блоки заставки с подблоками. Сетка сюда не
|
||||
/// входит — она запрашивается отдельно.
|
||||
/// Карточка канала: только собственные свойства канала. Сетка, стыки и заставки сюда не входят —
|
||||
/// они общие и запрашиваются отдельно.
|
||||
/// </summary>
|
||||
public class ChannelDetailsTests
|
||||
{
|
||||
private static readonly DateTimeOffset T0 = new(2026, 1, 1, 0, 0, 0, TimeSpan.Zero);
|
||||
|
||||
[Fact]
|
||||
public async Task GetChannel_MapsSettingsAndBumperTemplates()
|
||||
public async Task GetChannel_MapsOwnSettings()
|
||||
{
|
||||
var fixture = new TestDb();
|
||||
var channel = Channel.Create("Первый", "one", T0);
|
||||
var fillerId = Guid.NewGuid();
|
||||
var logoId = Guid.NewGuid();
|
||||
channel.UpdateSettings("Первый", isEnabled: true, bumpersEnabled: true, fillerId);
|
||||
channel.UpdateSettings("Первый", isEnabled: true, fillerId);
|
||||
channel.UpdateTimeSettings(3, 120, new TimeOnly(5, 0));
|
||||
channel.UpdateBumperSettings(BumperFont.Serif, BumperSelection.Random);
|
||||
channel.UpdateViewerSettings(logoId, LogoCorner.BottomLeft, 0.4, showClock: true, 0.25);
|
||||
// Второй блок должен приехать после дефолтного — порядок задаёт позиция.
|
||||
var extra = channel.AddBumperTemplate("Ночной");
|
||||
|
||||
await using (var seed = fixture.New())
|
||||
{
|
||||
@@ -48,22 +45,11 @@ public class ChannelDetailsTests
|
||||
Assert.Equal(120, dto.UtcOffsetMinutes);
|
||||
Assert.Equal(new TimeOnly(5, 0), dto.DayStartTime);
|
||||
Assert.Equal(fillerId, dto.FillerAssetId);
|
||||
Assert.True(dto.BumpersEnabled);
|
||||
Assert.Equal(BumperFont.Serif, dto.Bumper.Font);
|
||||
Assert.Equal(BumperSelection.Random, dto.Bumper.Selection);
|
||||
Assert.Equal(logoId, dto.Viewer.LogoImageId);
|
||||
Assert.Equal(LogoCorner.BottomLeft, dto.Viewer.LogoCorner);
|
||||
Assert.Equal(0.4, dto.Viewer.LogoOpacity);
|
||||
Assert.True(dto.Viewer.ShowClock);
|
||||
Assert.Equal(0.25, dto.Viewer.AnalogFilterStrength);
|
||||
|
||||
Assert.Equal(2, dto.BumperTemplates.Count);
|
||||
Assert.True(dto.BumperTemplates[0].IsDefault);
|
||||
Assert.Equal(extra.Id, dto.BumperTemplates[1].Id);
|
||||
Assert.Equal("Ночной", dto.BumperTemplates[1].Name);
|
||||
// Своего звука у свежего блока нет — джингл синтезируется.
|
||||
Assert.False(dto.BumperTemplates[1].HasAudio);
|
||||
Assert.NotEmpty(dto.BumperTemplates[0].Variants);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
@@ -48,14 +48,7 @@ public class ChannelHandlersTests
|
||||
|
||||
await using var db = fixture.New();
|
||||
var result = await new UpdateChannelSettingsCommandHandler(db).Handle(
|
||||
new UpdateChannelSettingsCommand(
|
||||
channel.Id,
|
||||
"c",
|
||||
true,
|
||||
true,
|
||||
new BumperSettingsInput(BumperFont.Sans, BumperSelection.WeightedRandom),
|
||||
null
|
||||
),
|
||||
new UpdateChannelSettingsCommand(channel.Id, "c2", true, null),
|
||||
CancellationToken.None
|
||||
);
|
||||
Assert.True(result.IsSuccess);
|
||||
@@ -63,7 +56,7 @@ public class ChannelHandlersTests
|
||||
|
||||
await using var verify = fixture.New();
|
||||
var stored = await verify.Channels.FindAsync(channel.Id);
|
||||
Assert.Equal(BumperFont.Sans, stored!.BumperFont);
|
||||
Assert.Equal(BumperSelection.WeightedRandom, stored.BumperSelection);
|
||||
Assert.Equal("c2", stored!.Name);
|
||||
Assert.True(stored.IsEnabled);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,8 +61,19 @@ public class QueryHandlersTests
|
||||
var fixture = new TestDb();
|
||||
var channel = Channel.Create("c", "c", T0);
|
||||
var show = Show.Create("Show A", ShowKind.Series);
|
||||
var variant = channel.BumperTemplates[0].Variants[0];
|
||||
var bumperTemplate = BumperTemplate.Create("Блок", "Текст 1");
|
||||
var variant = bumperTemplate.Variants[0];
|
||||
var asset = MediaAsset.Register("Show.A.S01E01.mkv", ".mkv", MediaSource.Upload);
|
||||
var bumperAsset = MediaAsset.RegisterGenerated("Блок: СЕЙЧАС / Show A");
|
||||
// Метка в расписании берётся из кэша заставки — там лежит ровно тот текст, что играл.
|
||||
var cache = BumperAsset.Create(
|
||||
bumperTemplate.Id,
|
||||
variant.Id,
|
||||
"sig",
|
||||
"""[{"style":"Label","color":"Accent","text":"СЕЙЧАС"},{"style":"Title","color":"Text","text":"Show A"}]""",
|
||||
null,
|
||||
bumperAsset.Id
|
||||
);
|
||||
var program = ScheduleEntry.Program(
|
||||
channel.Id,
|
||||
asset.Id,
|
||||
@@ -73,7 +84,7 @@ public class QueryHandlersTests
|
||||
);
|
||||
var bumper = ScheduleEntry.Bumper(
|
||||
channel.Id,
|
||||
Guid.NewGuid(),
|
||||
bumperAsset.Id,
|
||||
T0.AddMinutes(20),
|
||||
T0.AddMinutes(20).AddSeconds(8),
|
||||
show.Id,
|
||||
@@ -83,7 +94,10 @@ public class QueryHandlersTests
|
||||
{
|
||||
seed.Shows.Add(show);
|
||||
seed.Channels.Add(channel);
|
||||
seed.BumperTemplates.Add(bumperTemplate);
|
||||
seed.MediaAssets.Add(asset);
|
||||
seed.MediaAssets.Add(bumperAsset);
|
||||
seed.BumperAssets.Add(cache);
|
||||
seed.ScheduleEntries.Add(program);
|
||||
seed.ScheduleEntries.Add(bumper);
|
||||
await seed.SaveChangesAsync(CancellationToken.None);
|
||||
@@ -98,7 +112,8 @@ public class QueryHandlersTests
|
||||
Assert.True(result.IsSuccess);
|
||||
Assert.Equal(2, result.Value.Count);
|
||||
var bumperDto = result.Value.Single(e => e.Kind == ScheduleEntryKind.Bumper);
|
||||
Assert.NotNull(bumperDto.BumperName);
|
||||
Assert.Equal("Текст 1", bumperDto.BumperName);
|
||||
Assert.Equal("СЕЙЧАС · Show A", bumperDto.BumperText);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -150,52 +165,4 @@ public class QueryHandlersTests
|
||||
);
|
||||
Assert.True(ok.IsSuccess);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AddAndRemoveBumperTemplate_WorkThroughStorage()
|
||||
{
|
||||
var fixture = new TestDb();
|
||||
var channel = Channel.Create("c", "c", T0);
|
||||
await using (var seed = fixture.New())
|
||||
{
|
||||
seed.Channels.Add(channel);
|
||||
await seed.SaveChangesAsync(CancellationToken.None);
|
||||
}
|
||||
|
||||
Guid templateId;
|
||||
await using (var db = fixture.New())
|
||||
{
|
||||
var added = await new AddBumperTemplateCommandHandler(db).Handle(
|
||||
new AddBumperTemplateCommand(channel.Id, ""),
|
||||
CancellationToken.None
|
||||
);
|
||||
Assert.True(added.IsSuccess);
|
||||
templateId = added.Value;
|
||||
await db.SaveChangesAsync(CancellationToken.None);
|
||||
}
|
||||
|
||||
var storage = Substitute.For<IBumperTemplateStorage>();
|
||||
await using (var db = fixture.New())
|
||||
{
|
||||
var removed = await new RemoveBumperTemplateCommandHandler(db, storage).Handle(
|
||||
new RemoveBumperTemplateCommand(channel.Id, templateId),
|
||||
CancellationToken.None
|
||||
);
|
||||
Assert.True(removed.IsSuccess);
|
||||
}
|
||||
await storage.Received(1).DeleteTemplateAsync(templateId, Arg.Any<CancellationToken>());
|
||||
|
||||
// дефолтный блок удалить нельзя
|
||||
await using (var db = fixture.New())
|
||||
{
|
||||
var def = await db
|
||||
.Channels.Include(c => c.BumperTemplates)
|
||||
.FirstAsync(c => c.Id == channel.Id);
|
||||
var result = await new RemoveBumperTemplateCommandHandler(db, storage).Handle(
|
||||
new RemoveBumperTemplateCommand(channel.Id, def.BumperTemplates[0].Id),
|
||||
CancellationToken.None
|
||||
);
|
||||
Assert.Equal(ChannelErrors.CannotRemoveDefaultBumperTemplate, result.Error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -125,7 +125,7 @@ public class DeleteGuardsTests
|
||||
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, bumpersEnabled: false, asset.Id);
|
||||
channel.UpdateSettings(channel.Name, isEnabled: true, asset.Id);
|
||||
|
||||
await using (var seed = fixture.New())
|
||||
{
|
||||
|
||||
@@ -52,7 +52,7 @@ public class EntryTraceTests
|
||||
var show = Show.Create("Фильм", ShowKind.Single);
|
||||
var asset = MediaAsset.Register("film.mkv", ".mkv", MediaSource.Upload);
|
||||
|
||||
var junction = JunctionTemplate.Create(channel.Id, "Прайм");
|
||||
var junction = JunctionTemplate.Create("Прайм");
|
||||
var template = ScheduleTemplate.Create(channel.Id, "Сетка");
|
||||
var layer = template.AddLayer("Прайм", 10);
|
||||
var slot = layer.AddSlot("Вечернее кино", new TimeOnly(20, 0), 120);
|
||||
|
||||
@@ -31,8 +31,8 @@ public class ListPublicChannelsTests
|
||||
var disabled = Channel.Create("Выключенный", "off", T0);
|
||||
|
||||
foreach (var channel in new[] { numbered, first, unnumbered })
|
||||
channel.UpdateSettings(channel.Name, isEnabled: true, bumpersEnabled: false, null);
|
||||
disabled.UpdateSettings("Выключенный", isEnabled: false, bumpersEnabled: false, null);
|
||||
channel.UpdateSettings(channel.Name, isEnabled: true, null);
|
||||
disabled.UpdateSettings("Выключенный", isEnabled: false, null);
|
||||
|
||||
await using (var seed = fixture.New())
|
||||
{
|
||||
@@ -57,7 +57,7 @@ public class ListPublicChannelsTests
|
||||
{
|
||||
var fixture = new TestDb();
|
||||
var channel = Channel.Create("Первый", "one", T0);
|
||||
channel.UpdateSettings("Первый", isEnabled: true, bumpersEnabled: false, null);
|
||||
channel.UpdateSettings("Первый", isEnabled: true, null);
|
||||
var logoId = Guid.NewGuid();
|
||||
channel.UpdateViewerSettings(logoId, LogoCorner.TopRight, 0.5, showClock: true, 0.3);
|
||||
|
||||
|
||||
@@ -11,44 +11,57 @@ namespace TeleWave.Application.Tests.Validators;
|
||||
public class ValidatorTests
|
||||
{
|
||||
[Fact]
|
||||
public void UpdateBumperTextVariant_ChecksLengthsAndWeight()
|
||||
public void UpdateBumperVariant_ChecksNameWeightAndLines()
|
||||
{
|
||||
var v = new UpdateBumperTextVariantCommandValidator();
|
||||
var v = new UpdateBumperVariantCommandValidator();
|
||||
|
||||
var good = new UpdateBumperTextVariantCommand(
|
||||
var good = new UpdateBumperVariantCommand(
|
||||
Guid.NewGuid(),
|
||||
Guid.NewGuid(),
|
||||
Guid.NewGuid(),
|
||||
"Name",
|
||||
BumperTextKind.NowNext,
|
||||
"NOW",
|
||||
"NEXT",
|
||||
"l1",
|
||||
"l2",
|
||||
BumperTrigger.Both,
|
||||
3
|
||||
new BumperVariantInput(
|
||||
"Name",
|
||||
BumperTrigger.Both,
|
||||
BumperBackground.NextPoster,
|
||||
3,
|
||||
[new BumperLineDto(BumperLineStyle.Title, BumperLineColor.Text, "{next.title}")]
|
||||
)
|
||||
);
|
||||
Assert.True(v.Validate(good).IsValid);
|
||||
|
||||
Assert.False(v.Validate(good with { Name = "" }).IsValid);
|
||||
Assert.False(v.Validate(good with { Weight = -1 }).IsValid);
|
||||
Assert.False(v.Validate(good with { Line1 = new string('x', 121) }).IsValid);
|
||||
Assert.False(v.Validate(Patch(good, input => input with { Name = "" })).IsValid);
|
||||
Assert.False(v.Validate(Patch(good, input => input with { Weight = -1 })).IsValid);
|
||||
Assert.False(
|
||||
v.Validate(
|
||||
Patch(
|
||||
good,
|
||||
input =>
|
||||
input with
|
||||
{
|
||||
Lines =
|
||||
[
|
||||
new BumperLineDto(
|
||||
BumperLineStyle.Title,
|
||||
BumperLineColor.Text,
|
||||
new string('x', 121)
|
||||
),
|
||||
],
|
||||
}
|
||||
)
|
||||
).IsValid
|
||||
);
|
||||
}
|
||||
|
||||
private static UpdateBumperVariantCommand Patch(
|
||||
UpdateBumperVariantCommand command,
|
||||
Func<BumperVariantInput, BumperVariantInput> change
|
||||
) => command with { Input = change(command.Input) };
|
||||
|
||||
[Fact]
|
||||
public void UpdateChannelSettings_ChecksRanges()
|
||||
{
|
||||
var v = new UpdateChannelSettingsCommandValidator();
|
||||
var bumper = new BumperSettingsInput(BumperFont.Sans, BumperSelection.WeightedRandom);
|
||||
|
||||
var good = new UpdateChannelSettingsCommand(
|
||||
Guid.NewGuid(),
|
||||
"Name",
|
||||
true,
|
||||
true,
|
||||
bumper,
|
||||
null
|
||||
);
|
||||
var good = new UpdateChannelSettingsCommand(Guid.NewGuid(), "Name", true, null);
|
||||
Assert.True(v.Validate(good).IsValid);
|
||||
|
||||
Assert.False(v.Validate(good with { Name = "" }).IsValid);
|
||||
|
||||
@@ -5,8 +5,7 @@ namespace TeleWave.Domain.Tests.Broadcast;
|
||||
|
||||
public class BumperTemplateTests
|
||||
{
|
||||
private static BumperTemplate NewTemplate() =>
|
||||
Channel.Create("c", "c", DateTimeOffset.UnixEpoch).AddBumperTemplate("Block");
|
||||
private static BumperTemplate NewTemplate() => BumperTemplate.Create("Block", "Текст 1");
|
||||
|
||||
[Fact]
|
||||
public void Create_HasDefaultsAndOneVariant()
|
||||
@@ -53,12 +52,17 @@ public class BumperTemplateTests
|
||||
{
|
||||
var t = NewTemplate();
|
||||
|
||||
t.UpdateStyle("New", "0x111111", "0x222222", "0x333333", "black");
|
||||
t.UpdateStyle(
|
||||
new BumperStyle("New", BumperFont.Serif, "0x111111", "0x222222", "0x333333", "black")
|
||||
);
|
||||
|
||||
Assert.Equal("New", t.Name);
|
||||
Assert.Equal(BumperFont.Serif, t.Font);
|
||||
Assert.Equal("0x111111", t.BackgroundColor);
|
||||
Assert.Equal("0x333333", t.AccentColor);
|
||||
Assert.Equal("black", t.TextColor);
|
||||
// Оформление входит в сигнатуру рендера — правка обязана пересобрать заставки.
|
||||
Assert.Equal(1, t.Revision);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
@@ -6,21 +6,17 @@ namespace TeleWave.Domain.Tests.Broadcast;
|
||||
public class BumperTextVariantTests
|
||||
{
|
||||
private static BumperTextVariant NewVariant() =>
|
||||
Channel
|
||||
.Create("c", "c", DateTimeOffset.UnixEpoch)
|
||||
.AddBumperTemplate("b")
|
||||
.AddVariant("Text");
|
||||
BumperTemplate.Create("b", "Текст 1").AddVariant("Text");
|
||||
|
||||
[Fact]
|
||||
public void Create_HasDefaultLabelsAndWeight()
|
||||
public void Create_HasDefaults()
|
||||
{
|
||||
var v = NewVariant();
|
||||
|
||||
Assert.Equal(BumperTextKind.NowNext, v.Kind);
|
||||
Assert.Equal(BumperTextVariant.DefaultNowLabel, v.NowLabel);
|
||||
Assert.Equal(BumperTextVariant.DefaultNextLabel, v.NextLabel);
|
||||
Assert.Equal(BumperTextVariant.DefaultWeight, v.Weight);
|
||||
Assert.Equal(string.Empty, v.Line1);
|
||||
Assert.Equal(BumperTrigger.OnShowChange, v.Trigger);
|
||||
Assert.Equal(BumperBackground.NextPoster, v.Background);
|
||||
Assert.Empty(v.Lines);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -28,21 +24,31 @@ public class BumperTextVariantTests
|
||||
{
|
||||
var v = NewVariant();
|
||||
|
||||
v.Update(
|
||||
"Name",
|
||||
new BumperTextContent(BumperTextKind.Free, "NOW", "NEXT", "l1", "l2"),
|
||||
BumperTrigger.Both,
|
||||
-5
|
||||
);
|
||||
v.Update("Name", BumperTrigger.Both, BumperBackground.Template, -5);
|
||||
|
||||
Assert.Equal("Name", v.Name);
|
||||
Assert.Equal(BumperTextKind.Free, v.Kind);
|
||||
Assert.Equal("NOW", v.NowLabel);
|
||||
Assert.Equal("l1", v.Line1);
|
||||
Assert.Equal(BumperTrigger.Both, v.Trigger);
|
||||
Assert.Equal(BumperBackground.Template, v.Background);
|
||||
Assert.Equal(0, v.Weight);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SetLines_ReplacesAndRenumbers()
|
||||
{
|
||||
var v = NewVariant();
|
||||
|
||||
v.SetLines([
|
||||
BumperLine.Create(7, BumperLineStyle.Label, BumperLineColor.Accent, " ДАЛЕЕ "),
|
||||
BumperLine.Create(9, BumperLineStyle.Title, BumperLineColor.Text, "{next.title}"),
|
||||
]);
|
||||
|
||||
Assert.Equal([0, 1], v.Lines.Select(l => l.Position));
|
||||
Assert.Equal("ДАЛЕЕ", v.Lines[0].Text);
|
||||
|
||||
v.SetLines([BumperLine.Create(0, BumperLineStyle.Title, BumperLineColor.Text, "один")]);
|
||||
Assert.Single(v.Lines);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(BumperTrigger.OnShowChange, true, true)]
|
||||
[InlineData(BumperTrigger.OnShowChange, false, false)]
|
||||
@@ -53,7 +59,7 @@ public class BumperTextVariantTests
|
||||
public void Matches_FollowsTrigger(BumperTrigger trigger, bool isShowChange, bool expected)
|
||||
{
|
||||
var v = NewVariant();
|
||||
v.Update("n", new BumperTextContent(BumperTextKind.NowNext, "a", "b", "", ""), trigger, 1);
|
||||
v.Update("n", trigger, BumperBackground.Template, 1);
|
||||
|
||||
Assert.Equal(expected, v.Matches(isShowChange));
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ public class ChannelTests
|
||||
private static Channel NewChannel() => Channel.Create("News", "news", Epoch);
|
||||
|
||||
[Fact]
|
||||
public void Create_SetsDefaults_AndSeedsDefaultBumperTemplate()
|
||||
public void Create_SetsDefaults()
|
||||
{
|
||||
var channel = NewChannel();
|
||||
|
||||
@@ -19,15 +19,8 @@ public class ChannelTests
|
||||
Assert.Equal("news", channel.Slug);
|
||||
Assert.True(channel.IsEnabled);
|
||||
Assert.Equal(Epoch, channel.EpochUtc);
|
||||
Assert.False(channel.BumpersEnabled);
|
||||
Assert.Equal(BumperSelection.WeightedRandom, channel.BumperSelection);
|
||||
Assert.Equal(Channel.DefaultUtcOffsetMinutes, channel.UtcOffsetMinutes);
|
||||
Assert.Equal(Channel.DefaultDayStartTime, channel.DayStartTime);
|
||||
|
||||
var template = Assert.Single(channel.BumperTemplates);
|
||||
Assert.True(template.IsDefault);
|
||||
Assert.Equal(0, template.Position);
|
||||
Assert.Single(template.Variants); // дефолтный подблок
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -36,25 +29,13 @@ public class ChannelTests
|
||||
var channel = NewChannel();
|
||||
var filler = Guid.NewGuid();
|
||||
|
||||
channel.UpdateSettings("N2", false, true, filler);
|
||||
channel.UpdateSettings("N2", false, filler);
|
||||
|
||||
Assert.Equal("N2", channel.Name);
|
||||
Assert.False(channel.IsEnabled);
|
||||
Assert.True(channel.BumpersEnabled);
|
||||
Assert.Equal(filler, channel.FillerAssetId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UpdateBumperSettings_ChangesFontAndSelection()
|
||||
{
|
||||
var channel = NewChannel();
|
||||
|
||||
channel.UpdateBumperSettings(BumperFont.Serif, BumperSelection.AlwaysFirst);
|
||||
|
||||
Assert.Equal(BumperFont.Serif, channel.BumperFont);
|
||||
Assert.Equal(BumperSelection.AlwaysFirst, channel.BumperSelection);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(-1.0, 2.0, 0.0, 1.0)]
|
||||
[InlineData(0.4, 0.25, 0.4, 0.25)]
|
||||
@@ -88,35 +69,6 @@ public class ChannelTests
|
||||
Assert.Equal(0.0, channel.AnalogFilterStrength);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AddBumperTemplate_AppendsWithNextPosition()
|
||||
{
|
||||
var channel = NewChannel();
|
||||
|
||||
var t1 = channel.AddBumperTemplate("Block 2");
|
||||
var t2 = channel.AddBumperTemplate("Block 3");
|
||||
|
||||
Assert.Equal(1, t1.Position);
|
||||
Assert.Equal(2, t2.Position);
|
||||
Assert.False(t1.IsDefault);
|
||||
Assert.Equal(3, channel.BumperTemplates.Count);
|
||||
Assert.Equal(t1, channel.FindBumperTemplate(t1.Id));
|
||||
Assert.Null(channel.FindBumperTemplate(Guid.NewGuid()));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RemoveBumperTemplate_CannotRemoveDefault_CanRemoveOthers()
|
||||
{
|
||||
var channel = NewChannel();
|
||||
var def = channel.BumperTemplates[0];
|
||||
var extra = channel.AddBumperTemplate("Block 2");
|
||||
|
||||
Assert.False(channel.RemoveBumperTemplate(def.Id));
|
||||
Assert.True(channel.RemoveBumperTemplate(extra.Id));
|
||||
Assert.False(channel.RemoveBumperTemplate(Guid.NewGuid()));
|
||||
Assert.Single(channel.BumperTemplates);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UpdateTimeSettings_ClampsOffsetAndDropsNonPositiveNumber()
|
||||
{
|
||||
|
||||
@@ -8,23 +8,20 @@ namespace TeleWave.Domain.Tests.Broadcast;
|
||||
public class DomainRecordFactoryTests
|
||||
{
|
||||
[Fact]
|
||||
public void BumperAsset_Create_StoresPair()
|
||||
public void BumperAsset_Create_StoresRenderedText()
|
||||
{
|
||||
var channel = Guid.NewGuid();
|
||||
var template = Guid.NewGuid();
|
||||
var variant = Guid.NewGuid();
|
||||
var from = Guid.NewGuid();
|
||||
var to = Guid.NewGuid();
|
||||
var poster = Guid.NewGuid();
|
||||
var asset = Guid.NewGuid();
|
||||
|
||||
var b = BumperAsset.Create(channel, template, variant, from, to, "sig", asset);
|
||||
var b = BumperAsset.Create(template, variant, "sig", "[]", poster, asset);
|
||||
|
||||
Assert.Equal(channel, b.ChannelId);
|
||||
Assert.Equal(template, b.TemplateId);
|
||||
Assert.Equal(variant, b.VariantId);
|
||||
Assert.Equal(from, b.FromShowId);
|
||||
Assert.Equal(to, b.ToShowId);
|
||||
Assert.Equal("sig", b.Signature);
|
||||
Assert.Equal("[]", b.RenderedLinesJson);
|
||||
Assert.Equal(poster, b.PosterShowId);
|
||||
Assert.Equal(asset, b.MediaAssetId);
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,12 @@ public class JunctionFillerTests
|
||||
public int Next(int maxExclusive) => 0;
|
||||
}
|
||||
|
||||
/// <summary>Всегда последний вариант — по нему видно, что жребий вообще участвует в выборе.</summary>
|
||||
private sealed class LastAlways : IRandomSource
|
||||
{
|
||||
public int Next(int maxExclusive) => Math.Max(0, maxExclusive - 1);
|
||||
}
|
||||
|
||||
private static PlanningUnit Unit(double minutes, Guid? showId = null, int index = 0) =>
|
||||
new(Guid.NewGuid(), TimeSpan.FromMinutes(minutes), showId ?? Guid.NewGuid(), index);
|
||||
|
||||
@@ -31,16 +37,26 @@ public class JunctionFillerTests
|
||||
double minutesEach = 0.5,
|
||||
bool required = false,
|
||||
int minMinutesBetween = 0,
|
||||
bool onlyOnElementChange = false
|
||||
bool onlyOnElementChange = false,
|
||||
int chance = 100,
|
||||
PlanningTimeWindow? window = null,
|
||||
string? choiceKey = null,
|
||||
int choiceWeight = 1,
|
||||
int poolSize = 8
|
||||
) =>
|
||||
new(
|
||||
Guid.NewGuid(),
|
||||
JunctionElementKind.Ad,
|
||||
Enumerable.Range(0, 8).Select(_ => Unit(minutesEach)).ToList(),
|
||||
Enumerable.Range(0, poolSize).Select(_ => Unit(minutesEach)).ToList(),
|
||||
JunctionAmountMode.Count,
|
||||
count,
|
||||
required,
|
||||
onlyOnElementChange,
|
||||
minMinutesBetween
|
||||
minMinutesBetween,
|
||||
chance,
|
||||
window,
|
||||
choiceKey,
|
||||
choiceWeight
|
||||
);
|
||||
|
||||
private static PlanningSlot Slot(
|
||||
@@ -68,7 +84,11 @@ public class JunctionFillerTests
|
||||
JunctionAfter: after
|
||||
);
|
||||
|
||||
private static PlanningResult Run(PlanningSlot slot) =>
|
||||
private static PlanningResult Run(
|
||||
PlanningSlot slot,
|
||||
IRandomSource? random = null,
|
||||
int utcOffsetMinutes = 0
|
||||
) =>
|
||||
GridPlanner.Plan(
|
||||
new PlanningInput(
|
||||
Guid.NewGuid(),
|
||||
@@ -76,9 +96,10 @@ public class JunctionFillerTests
|
||||
T0.AddHours(5),
|
||||
[slot],
|
||||
[Unit(1)],
|
||||
SegmentSeconds: 2
|
||||
SegmentSeconds: 2,
|
||||
UtcOffsetMinutes: utcOffsetMinutes
|
||||
),
|
||||
new FirstAlways()
|
||||
random ?? new FirstAlways()
|
||||
);
|
||||
|
||||
[Fact]
|
||||
@@ -112,6 +133,7 @@ public class JunctionFillerTests
|
||||
{
|
||||
var element = Series(2, 20, out _);
|
||||
var ads = new PlanningJunctionElement(
|
||||
Guid.NewGuid(),
|
||||
JunctionElementKind.Ad,
|
||||
Enumerable.Range(0, 6).Select(_ => Unit(0.5)).ToList(),
|
||||
JunctionAmountMode.Duration,
|
||||
@@ -141,6 +163,32 @@ public class JunctionFillerTests
|
||||
Assert.Equal(1, result.Items.Count(i => i.Kind == PlannedItemKind.Ad));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MinInterval_IsPerElement_NotPerKind()
|
||||
{
|
||||
var element = Series(2, 20, out _);
|
||||
var frequent = Ads(1, minMinutesBetween: 0);
|
||||
var rare = Ads(1, minMinutesBetween: 600);
|
||||
|
||||
var result = Run(
|
||||
Slot(element, 2, between: new PlanningJunction(Guid.NewGuid(), [frequent, rare]))
|
||||
);
|
||||
|
||||
// Обе врезки рекламные, но ограничение у каждой своё — общий счётчик на вид склеил бы их.
|
||||
Assert.Equal(2, result.Items.Count(i => i.Kind == PlannedItemKind.Ad));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Units_RotateBetweenJunctions()
|
||||
{
|
||||
var element = Series(3, 20, out _);
|
||||
var result = Run(Slot(element, 3, between: new PlanningJunction(Guid.NewGuid(), [Ads(1)])));
|
||||
|
||||
var ads = result.Items.Where(i => i.Kind == PlannedItemKind.Ad).ToList();
|
||||
// Иначе каждый рекламный блок начинался бы с одного и того же ролика.
|
||||
Assert.Equal(2, ads.Select(a => a.MediaAssetId).Distinct().Count());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OnlyOnElementChange_SkipsWithinSameShow()
|
||||
{
|
||||
@@ -157,17 +205,94 @@ public class JunctionFillerTests
|
||||
Assert.DoesNotContain(result.Items, i => i.Kind == PlannedItemKind.Ad);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Chance_Zero_SkipsElement()
|
||||
{
|
||||
var element = Series(3, 20, out _);
|
||||
var result = Run(
|
||||
Slot(element, 3, between: new PlanningJunction(Guid.NewGuid(), [Ads(1, chance: 0)]))
|
||||
);
|
||||
|
||||
Assert.DoesNotContain(result.Items, i => i.Kind == PlannedItemKind.Ad);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TimeWindow_SkipsOutsideChannelHours()
|
||||
{
|
||||
var element = Series(3, 20, out _);
|
||||
// Стык идёт в 18:20 UTC, канал в UTC+3 — 21:20 по времени канала, окно 06:00–20:00 закрыто.
|
||||
var window = new PlanningTimeWindow(new TimeOnly(6, 0), new TimeOnly(20, 0));
|
||||
var result = Run(
|
||||
Slot(
|
||||
element,
|
||||
3,
|
||||
between: new PlanningJunction(Guid.NewGuid(), [Ads(1, window: window)])
|
||||
),
|
||||
utcOffsetMinutes: 180
|
||||
);
|
||||
|
||||
Assert.DoesNotContain(result.Items, i => i.Kind == PlannedItemKind.Ad);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Choice_PlacesExactlyOneOfTheFork()
|
||||
{
|
||||
var element = Series(2, 20, out _);
|
||||
var first = Ads(1, choiceKey: "fork");
|
||||
var second = new PlanningJunctionElement(
|
||||
Guid.NewGuid(),
|
||||
JunctionElementKind.Promo,
|
||||
[Unit(0.5)],
|
||||
JunctionAmountMode.Count,
|
||||
1,
|
||||
IsRequired: false,
|
||||
ChoiceKey: "fork"
|
||||
);
|
||||
|
||||
var byFirst = Run(
|
||||
Slot(element, 2, between: new PlanningJunction(Guid.NewGuid(), [first, second])),
|
||||
new FirstAlways()
|
||||
);
|
||||
var byLast = Run(
|
||||
Slot(element, 2, between: new PlanningJunction(Guid.NewGuid(), [first, second])),
|
||||
new LastAlways()
|
||||
);
|
||||
|
||||
Assert.Single(byFirst.Items, i => i.Kind == PlannedItemKind.Ad);
|
||||
Assert.DoesNotContain(byFirst.Items, i => i.Kind == PlannedItemKind.Promo);
|
||||
Assert.Single(byLast.Items, i => i.Kind == PlannedItemKind.Promo);
|
||||
Assert.DoesNotContain(byLast.Items, i => i.Kind == PlannedItemKind.Ad);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MaxTotal_CapsJunctionLength()
|
||||
{
|
||||
var element = Series(2, 20, out _);
|
||||
var junction = new PlanningJunction(
|
||||
Guid.NewGuid(),
|
||||
[Ads(6, minutesEach: 1)],
|
||||
MaxTotal: TimeSpan.FromMinutes(2)
|
||||
);
|
||||
|
||||
var result = Run(Slot(element, 2, between: junction));
|
||||
|
||||
Assert.Equal(2, result.Items.Count(i => i.Kind == PlannedItemKind.Ad));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Bumper_ReservesDurationWithEmptyAssetAndShowPair()
|
||||
{
|
||||
var element = Series(2, 20, out var showId);
|
||||
var variantId = Guid.NewGuid();
|
||||
var bumper = new PlanningJunctionElement(
|
||||
Guid.NewGuid(),
|
||||
JunctionElementKind.Bumper,
|
||||
[],
|
||||
JunctionAmountMode.Count,
|
||||
1,
|
||||
IsRequired: true,
|
||||
BumperTemplateId: Guid.NewGuid(),
|
||||
BumperVariantId: variantId,
|
||||
BumperDuration: TimeSpan.FromSeconds(10)
|
||||
);
|
||||
|
||||
@@ -177,15 +302,17 @@ public class JunctionFillerTests
|
||||
// Ассет подставит рендер после сборки ленты — до неё пара соседей неизвестна.
|
||||
Assert.Equal(Guid.Empty, placed.MediaAssetId);
|
||||
Assert.Equal(showId, placed.ToShowId);
|
||||
Assert.Equal(variantId, placed.BumperVariantId);
|
||||
Assert.Equal(TimeSpan.FromSeconds(10), placed.EndsAtUtc - placed.StartsAtUtc);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RequiredElement_GoesBeforeOptional()
|
||||
public void OrderFollowsPosition_NotRequiredFlag()
|
||||
{
|
||||
var element = Series(2, 20, out _);
|
||||
var optional = Ads(1, minutesEach: 1);
|
||||
var required = new PlanningJunctionElement(
|
||||
Guid.NewGuid(),
|
||||
JunctionElementKind.Promo,
|
||||
[Unit(0.5)],
|
||||
JunctionAmountMode.Count,
|
||||
@@ -201,7 +328,34 @@ public class JunctionFillerTests
|
||||
.Items.Where(i => i.Kind is PlannedItemKind.Ad or PlannedItemKind.Promo)
|
||||
.OrderBy(i => i.StartsAtUtc)
|
||||
.ToList();
|
||||
Assert.Equal(PlannedItemKind.Promo, junctionItems[0].Kind);
|
||||
// Обязательность решает, кого выбросить при нехватке времени, а не кто идёт первым.
|
||||
Assert.Equal(PlannedItemKind.Ad, junctionItems[0].Kind);
|
||||
Assert.Equal(PlannedItemKind.Promo, junctionItems[1].Kind);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RequiredSurvives_WhenOptionalDoesNotFit()
|
||||
{
|
||||
var element = Series(2, 20, out _);
|
||||
var optional = Ads(4, minutesEach: 1);
|
||||
var required = new PlanningJunctionElement(
|
||||
Guid.NewGuid(),
|
||||
JunctionElementKind.Promo,
|
||||
[Unit(1)],
|
||||
JunctionAmountMode.Count,
|
||||
1,
|
||||
IsRequired: true
|
||||
);
|
||||
var junction = new PlanningJunction(
|
||||
Guid.NewGuid(),
|
||||
[optional, required],
|
||||
MaxTotal: TimeSpan.FromMinutes(2)
|
||||
);
|
||||
|
||||
var result = Run(Slot(element, 2, between: junction));
|
||||
|
||||
Assert.Single(result.Items, i => i.Kind == PlannedItemKind.Promo);
|
||||
Assert.Single(result.Items, i => i.Kind == PlannedItemKind.Ad);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -245,5 +399,6 @@ public class JunctionFillerTests
|
||||
result.Items.Where(i => i.Kind == PlannedItemKind.Ad),
|
||||
item => Assert.True(item.EndsAtUtc <= anchor.TargetStartUtc)
|
||||
);
|
||||
Assert.Single(result.Items, i => i.Kind == PlannedItemKind.Ad);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -251,7 +251,7 @@ public sealed class GridScheduleGeneratorIntegrationTests(PostgresFixture fixtur
|
||||
var filler = ReadyAsset(db, $"filler-{suffix}.mkv", TimeSpan.FromMinutes(1));
|
||||
|
||||
var channel = Channel.Create($"Канал {suffix}", $"ch-{suffix}", Now.AddDays(-7));
|
||||
channel.UpdateSettings(channel.Name, isEnabled: true, bumpersEnabled: false, filler.Id);
|
||||
channel.UpdateSettings(channel.Name, isEnabled: true, filler.Id);
|
||||
|
||||
var template = ScheduleTemplate.Create(channel.Id, "Сетка");
|
||||
var layer = template.AddLayer("Базовый", 10);
|
||||
|
||||
@@ -19,7 +19,7 @@ namespace TeleWave.Integration.Tests;
|
||||
public sealed class TemplateOperationsIntegrationTests(PostgresFixture fixture)
|
||||
{
|
||||
[SkippableFact]
|
||||
public async Task Copy_MovesLayersSlotsAndJunctions_AndReplacesTargetGrid()
|
||||
public async Task Copy_MovesLayersAndSlots_AndReplacesTargetGrid()
|
||||
{
|
||||
Skip.IfNot(fixture.Available, "Docker недоступен");
|
||||
|
||||
@@ -36,7 +36,6 @@ public sealed class TemplateOperationsIntegrationTests(PostgresFixture fixture)
|
||||
|
||||
Assert.Equal(1, result.Value.Layers);
|
||||
Assert.Equal(1, result.Value.Slots);
|
||||
Assert.Equal(1, result.Value.Junctions);
|
||||
|
||||
await using var verify = fixture.CreateContext();
|
||||
var target = verify.Channels.Single(c => c.Id == targetId);
|
||||
@@ -54,13 +53,15 @@ public sealed class TemplateOperationsIntegrationTests(PostgresFixture fixture)
|
||||
Assert.Equal(groupId, slot.GroupId);
|
||||
Assert.Single(verify.Groups.Where(g => g.Id == groupId));
|
||||
|
||||
// Стык переехал своей копией, и слот ссылается на неё, а не на стык чужого канала.
|
||||
var junction = verify.JunctionTemplates.Single(j => j.ChannelId == targetId);
|
||||
Assert.Equal(junction.Id, slot.JunctionAfterId);
|
||||
Assert.NotEqual(
|
||||
verify.JunctionTemplates.Single(j => j.ChannelId == sourceId).Id,
|
||||
slot.JunctionAfterId
|
||||
);
|
||||
// Стыки общие — копия ссылается на тот же стык, а не заводит свой.
|
||||
var sourceSlot = verify
|
||||
.ScheduleTemplates.Include(t => t.Layers)
|
||||
.ThenInclude(l => l.Slots)
|
||||
.Single(t => t.ChannelId == sourceId)
|
||||
.Layers.SelectMany(l => l.Slots)
|
||||
.Single();
|
||||
Assert.Equal(sourceSlot.JunctionAfterId, slot.JunctionAfterId);
|
||||
Assert.NotNull(slot.JunctionAfterId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -148,9 +149,22 @@ public sealed class TemplateOperationsIntegrationTests(PostgresFixture fixture)
|
||||
var source = Channel.Create($"Источник {suffix}", $"src-{suffix}", DateTimeOffset.UtcNow);
|
||||
var target = Channel.Create($"Приёмник {suffix}", $"dst-{suffix}", DateTimeOffset.UtcNow);
|
||||
|
||||
var junction = JunctionTemplate.Create(source.Id, "Прайм");
|
||||
var junction = JunctionTemplate.Create($"Прайм {suffix}");
|
||||
var ad = junction.AddElement(JunctionElementKind.Ad);
|
||||
ad.Update(JunctionElementKind.Ad, group.Id, null, JunctionAmountMode.Count, 2, true, null);
|
||||
ad.Update(
|
||||
new JunctionElementSettings(
|
||||
JunctionElementKind.Ad,
|
||||
group.Id,
|
||||
null,
|
||||
null,
|
||||
JunctionAmountMode.Count,
|
||||
2,
|
||||
true,
|
||||
null,
|
||||
JunctionElement.DefaultChoiceWeight,
|
||||
null
|
||||
)
|
||||
);
|
||||
db.JunctionTemplates.Add(junction);
|
||||
|
||||
var sourceTemplate = ScheduleTemplate.Create(source.Id, "Сетка источника");
|
||||
|
||||
Reference in New Issue
Block a user