Files
TeleWave/backend/tests/TeleWave.Application.Tests/Programming/ValidateTemplateTests.cs
T
Leonid Pershin 1a7f73a5bd
ci / build-backend (push) Successful in 2m4s
ci / build-frontend (push) Successful in 53s
ci / tests (push) Successful in 1m59s
ci / sonar (push) Successful in 6m6s
Implement bulk tagging and enriching of shows with new API endpoints and frontend integration
Added new API endpoints for bulk tagging and enriching shows, allowing for mass updates of genres and audience ratings. Implemented backend logic to handle bulk operations and updated the Dependency Injection configuration to include necessary services. Enhanced the frontend with new components for selecting shows and applying bulk actions, improving the user experience for managing multiple shows simultaneously. Localization updates were made to support these new features in both English and Russian.
2026-07-27 05:18:18 +03:00

305 lines
12 KiB
C#

using TeleWave.Application.Broadcast;
using TeleWave.Application.Programming.Templates;
using TeleWave.Application.Programming.Templates.Validate;
using TeleWave.Application.Tests.Support;
using TeleWave.Domain.Broadcast;
using TeleWave.Domain.Library;
using TeleWave.Domain.Programming;
using Xunit;
namespace TeleWave.Application.Tests.Programming;
/// <summary>
/// Проверки сетки до генерации: пустые группы, пересечения слотов, недостижимое остывание,
/// конфликт с детским временем и непокрытые интервалы суток.
/// </summary>
public class ValidateTemplateTests
{
private static readonly DateTimeOffset T0 = new(2026, 1, 1, 0, 0, 0, TimeSpan.Zero);
/// <summary>Слот на сутки целиком — им закрываем сетку, когда дыры не тема теста.</summary>
private static Slot FullDaySlot(GridLayer layer, Guid? groupId, TimeOnly dayStart)
{
var slot = layer.AddSlot("Круглосуточно", dayStart, 24 * 60);
slot.UpdateContent(
new SlotContent(
slot.Title,
SlotKind.Content,
groupId,
new SlotStrategy(SlotStrategyType.Sequential).ToJson(),
null,
SlotBlockMode.FillSlot,
1,
OverflowPolicy.ContinueNext
)
);
return slot;
}
private static void Fill(
Slot slot,
Guid? groupId,
SlotStrategy? strategy = null,
SlotKind kind = SlotKind.Content
) =>
slot.UpdateContent(
new SlotContent(
slot.Title,
kind,
groupId,
(strategy ?? new SlotStrategy(SlotStrategyType.Sequential)).ToJson(),
null,
SlotBlockMode.FillSlot,
1,
OverflowPolicy.ContinueNext
)
);
/// <summary>Наполняет группу позициями: проверке важно их число, а не что за ними стоит.</summary>
private static void FillGroup(Group group, int count)
{
for (var i = 0; i < count; i++)
group.AddElement(GroupElementKind.Show, Guid.NewGuid());
}
private static async Task<IReadOnlyList<TemplateIssueDto>> ValidateAsync(
TestDb fixture,
Guid channelId
)
{
await using var db = fixture.New();
var result = await new ValidateTemplateQueryHandler(db, GroupServices.Dynamic(db)).Handle(
new ValidateTemplateQuery(channelId),
CancellationToken.None
);
Assert.True(result.IsSuccess);
return result.Value;
}
[Fact]
public async Task Validate_UnknownChannel_ReturnsNotFound()
{
var fixture = new TestDb();
await using var db = fixture.New();
var result = await new ValidateTemplateQueryHandler(db, GroupServices.Dynamic(db)).Handle(
new ValidateTemplateQuery(Guid.NewGuid()),
CancellationToken.None
);
Assert.Equal(ChannelErrors.NotFound, result.Error);
}
[Fact]
public async Task Validate_ChannelWithoutGrid_ReturnsTemplateNotFound()
{
var fixture = new TestDb();
var channel = Channel.Create("Без сетки", "nogrid", 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 ValidateTemplateQueryHandler(db, GroupServices.Dynamic(db)).Handle(
new ValidateTemplateQuery(channel.Id),
CancellationToken.None
);
Assert.Equal(ChannelErrors.TemplateNotFound, result.Error);
}
[Fact]
public async Task Validate_EmptyGrid_ReportsGapForEveryDay()
{
var fixture = new TestDb();
var channel = Channel.Create("Первый", "one", T0);
var template = ScheduleTemplate.Create(channel.Id, "Сетка");
await using (var seed = fixture.New())
{
seed.Channels.Add(channel);
seed.ScheduleTemplates.Add(template);
await seed.SaveChangesAsync(CancellationToken.None);
}
var issues = await ValidateAsync(fixture, channel.Id);
// Семь суток без единого слота — семь дыр «весь день».
var gaps = issues.Where(i => i.Kind == TemplateIssueKind.GridGap).ToList();
Assert.Equal(7, gaps.Count);
Assert.All(gaps, g => Assert.Contains("весь день", g.Details));
Assert.All(gaps, g => Assert.Equal(TemplateIssueSeverity.Error, g.Severity));
}
[Fact]
public async Task Validate_SlotWithoutGroup_ReportsGroupMissing()
{
var fixture = new TestDb();
var channel = Channel.Create("Первый", "one", T0);
var template = ScheduleTemplate.Create(channel.Id, "Сетка");
FullDaySlot(template.Background!, null, channel.DayStartTime);
await using (var seed = fixture.New())
{
seed.Channels.Add(channel);
seed.ScheduleTemplates.Add(template);
await seed.SaveChangesAsync(CancellationToken.None);
}
var issues = await ValidateAsync(fixture, channel.Id);
Assert.Contains(issues, i => i.Kind == TemplateIssueKind.GroupMissing);
// Сутки закрыты слотом — дыр быть не должно.
Assert.DoesNotContain(issues, i => i.Kind == TemplateIssueKind.GridGap);
}
[Fact]
public async Task Validate_EmptyGroup_ReportsGroupEmpty()
{
var fixture = new TestDb();
var channel = Channel.Create("Первый", "one", T0);
var group = Group.Create("Пустая");
var template = ScheduleTemplate.Create(channel.Id, "Сетка");
FullDaySlot(template.Background!, group.Id, channel.DayStartTime);
await using (var seed = fixture.New())
{
seed.Channels.Add(channel);
seed.Groups.Add(group);
seed.ScheduleTemplates.Add(template);
await seed.SaveChangesAsync(CancellationToken.None);
}
var issues = await ValidateAsync(fixture, channel.Id);
var issue = Assert.Single(issues, i => i.Kind == TemplateIssueKind.GroupEmpty);
Assert.Contains("Пустая", issue.Details);
}
[Fact]
public async Task Validate_SmallGroupWithCooldown_ReportsBothWarnings()
{
var fixture = new TestDb();
var channel = Channel.Create("Первый", "one", T0);
var group = Group.Create("Маленькая");
// Позиции настоящие: проверка считает состав, а не кэш статистики, — у динамической группы
// кэш отстаёт от библиотеки, и полагаться на него нельзя.
FillGroup(group, 3);
var template = ScheduleTemplate.Create(channel.Id, "Сетка");
var slot = FullDaySlot(template.Background!, group.Id, channel.DayStartTime);
// Ежедневный слот — 7 выходов в неделю на 3 позиции, да ещё с двухнедельным остыванием.
Fill(
slot,
group.Id,
new SlotStrategy(SlotStrategyType.RandomWithCooldown, CooldownDays: 14)
);
await using (var seed = fixture.New())
{
seed.Channels.Add(channel);
seed.Groups.Add(group);
seed.ScheduleTemplates.Add(template);
await seed.SaveChangesAsync(CancellationToken.None);
}
var issues = await ValidateAsync(fixture, channel.Id);
Assert.Contains(issues, i => i.Kind == TemplateIssueKind.GroupTooSmall);
Assert.Contains(issues, i => i.Kind == TemplateIssueKind.CooldownUnreachable);
Assert.All(
issues.Where(i => i.Kind != TemplateIssueKind.GridGap),
i => Assert.Equal(TemplateIssueSeverity.Warning, i.Severity)
);
}
[Fact]
public async Task Validate_OverlappingSlotsInOneLayer_AreReported()
{
var fixture = new TestDb();
var channel = Channel.Create("Первый", "one", T0);
var group = Group.Create("Кино");
FillGroup(group, 50);
var template = ScheduleTemplate.Create(channel.Id, "Сетка");
FullDaySlot(template.Background!, group.Id, channel.DayStartTime);
var layer = template.AddLayer("Прайм", 10);
var first = layer.AddSlot("Кино", new TimeOnly(20, 0), 120);
Fill(first, group.Id);
var second = layer.AddSlot("Сериал", new TimeOnly(21, 0), 60);
Fill(second, group.Id);
await using (var seed = fixture.New())
{
seed.Channels.Add(channel);
seed.Groups.Add(group);
seed.ScheduleTemplates.Add(template);
await seed.SaveChangesAsync(CancellationToken.None);
}
var issues = await ValidateAsync(fixture, channel.Id);
// Внутри слоя приоритетов нет: второй слот молча пропал бы при генерации.
var overlap = Assert.Single(issues, i => i.Kind == TemplateIssueKind.SlotOverlap);
Assert.Equal(second.Id, overlap.SlotId);
Assert.Equal(layer.Id, overlap.LayerId);
}
[Fact]
public async Task Validate_AdultContentInKidsWindow_ReportsAudienceConflict()
{
var fixture = new TestDb();
var channel = Channel.Create("Первый", "one", T0);
var mild = Show.Create("Мультик", ShowKind.Single);
mild.SetAudience(ShowAudience.G);
var harsh = Show.Create("Ужастик", ShowKind.Single);
harsh.SetAudience(ShowAudience.R);
// Строжайший рейтинг группы приходит через коллекцию: франшиза идёт целиком.
var collection = Collection.Create("Франшиза");
collection.AddShow(harsh.Id);
var group = Group.Create("Вечернее");
group.AddElement(GroupElementKind.Show, mild.Id);
group.AddElement(GroupElementKind.Collection, collection.Id);
FillGroup(group, 2);
var template = ScheduleTemplate.Create(channel.Id, "Сетка");
var slot = FullDaySlot(template.Background!, group.Id, channel.DayStartTime);
slot.UpdateTiming(
null,
new TimeOnly(10, 0),
24 * 60,
Daypart.Day,
isAnchor: false,
5,
null
);
template.SetRules(
new PlanningRules(
MaxAudienceByTime:
[
new AudienceWindow(new TimeOnly(6, 0), new TimeOnly(21, 0), ShowAudience.Pg),
]
).ToJson()
);
await using (var seed = fixture.New())
{
seed.Channels.Add(channel);
seed.Groups.Add(group);
seed.Collections.Add(collection);
seed.Shows.AddRange(mild, harsh);
seed.ScheduleTemplates.Add(template);
await seed.SaveChangesAsync(CancellationToken.None);
}
var issues = await ValidateAsync(fixture, channel.Id);
var conflict = Assert.Single(issues, i => i.Kind == TemplateIssueKind.AudienceConflict);
Assert.Contains("Вечернее", conflict.Details);
}
}