Files
TeleWave/backend/tests/TeleWave.Application.Tests/Programming/ValidateTemplateTests.cs
T
Leonid Pershin 790d01b587
ci / build-backend (push) Successful in 2m34s
ci / build-frontend (push) Successful in 41s
ci / tests (push) Successful in 2m55s
ci / sonar (push) Successful in 5m21s
Update README.md with additional SonarCloud badges and improve CI workflow for coverage reporting
Enhanced the README.md file by adding new SonarCloud badges for coverage, bugs, code smells, security rating, and maintainability rating. Updated the CI workflow to remove coverage collection from the test step, as it is now handled by SonarCloud, streamlining the process and ensuring accurate badge representation.
2026-07-27 03:19:18 +03:00

296 lines
11 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
)
);
private static async Task<IReadOnlyList<TemplateIssueDto>> ValidateAsync(
TestDb fixture,
Guid channelId
)
{
await using var db = fixture.New();
var result = await new ValidateTemplateQueryHandler(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).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).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("Маленькая");
group.UpdateStats(3, 3, TimeSpan.FromHours(3), T0);
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("Кино");
group.UpdateStats(50, 50, TimeSpan.FromHours(50), T0);
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);
group.UpdateStats(2, 2, TimeSpan.FromHours(2), T0);
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);
}
}