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:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user