Files
TeleWave/backend/tests/TeleWave.Application.Tests/Programming/GridPlanPreviewTests.cs
T
Leonid Pershin f7e7b5f7c3
ci / build-backend (push) Successful in 1m31s
ci / build-frontend (push) Successful in 1m5s
ci / tests (push) Successful in 3m54s
ci / sonar (push) Successful in 4m40s
Implement template export/import endpoints and enhance grid generation logic
Added new endpoints for exporting and importing grid configurations in TemplateEndpoints, allowing for better management of template data. Enhanced the GenerateGridCommandHandler to support seasonal layers in grid generation, improving scheduling accuracy during holiday periods. Updated related classes and records to accommodate these changes, ensuring a cohesive integration of new features. Improved documentation for clarity and maintainability.
2026-07-28 02:15:04 +03:00

240 lines
8.9 KiB
C#

using TeleWave.Application.Programming.Groups;
using TeleWave.Application.Programming.Templates;
using TeleWave.Application.Programming.Templates.Generate;
using TeleWave.Application.Tests.Support;
using TeleWave.Domain.Broadcast;
using TeleWave.Domain.Library;
using TeleWave.Domain.Media;
using TeleWave.Domain.Programming;
using Xunit;
namespace TeleWave.Application.Tests.Programming;
/// <summary>
/// Витрина автосборки: каталог профилей, предпросмотр плана и его перевод в DTO. Предпросмотр —
/// то, по чему админ принимает решение, поэтому проверяется отдельно от самой раскладки.
/// </summary>
public class GridPlanPreviewTests
{
private static readonly DateTimeOffset T0 = new(2026, 1, 1, 0, 0, 0, TimeSpan.Zero);
private static PlannedSlot Slot(
string title,
SlotKind kind,
SlotBlockMode blockMode,
int blockValue,
int? weekday = null
) =>
new(
GridPlanLayer.Main,
weekday,
new TimeOnly(20, 0),
120,
title,
Daypart.Prime,
kind,
kind == SlotKind.Content ? Guid.NewGuid() : null,
kind == SlotKind.Content ? title : null,
blockMode,
blockValue,
null,
null,
IsAnchor: false
);
[Fact]
public async Task Profiles_AreListed_WithReferenceAndDescription()
{
var result = await new ListGridProfilesQueryHandler().Handle(
new ListGridProfilesQuery(),
CancellationToken.None
);
Assert.True(result.IsSuccess);
// Каждый профиль должен объяснять себя: имя, на что похоже и для какой библиотеки годится.
Assert.Equal(GridProfiles.All.Count, result.Value.Count);
Assert.All(
result.Value,
profile =>
{
Assert.False(string.IsNullOrWhiteSpace(profile.Name));
Assert.False(string.IsNullOrWhiteSpace(profile.Reference));
Assert.False(string.IsNullOrWhiteSpace(profile.Description));
}
);
Assert.Contains(result.Value, p => p.Kind == GridProfileKind.Animation);
}
[Fact]
public void Mapper_DescribesEveryBlockKind()
{
var plan = new GridPlan(
GridProfiles.Get(GridProfileKind.Mixed),
GridGenerationMode.Rebuild,
[
Slot("Сериалы", SlotKind.Content, SlotBlockMode.Count, 3),
Slot("Кино", SlotKind.Content, SlotBlockMode.FillSlot, 1),
Slot("Полоса", SlotKind.Content, SlotBlockMode.Duration, 90, weekday: 6),
Slot("Повтор", SlotKind.Repeat, SlotBlockMode.FillSlot, 1),
Slot("Конец вещания", SlotKind.SignOff, SlotBlockMode.FillSlot, 1),
],
[new PlanNote(false, "замечание")],
SlotsToRemove: 4,
FallbackGroupId: Guid.NewGuid(),
FallbackGroupName: "Фон",
FreeMinutes: 10080,
CoveredMinutes: 5040
);
var dto = plan.ToDto();
Assert.Equal(GridProfileKind.Mixed, dto.Profile);
Assert.Equal(plan.Profile.Name, dto.ProfileName);
Assert.Equal(4, dto.SlotsToRemove);
Assert.Equal("Фон", dto.FallbackGroupName);
Assert.Equal(["замечание"], dto.Notes.Select(n => n.Text));
// Описание блока — то, что читает админ вместо режима и числа.
Assert.Equal(
["3 подряд", "до конца слота", "90 мин", "повтор", "конец вещания"],
dto.Slots.Select(s => s.Block)
);
// Время приезжает строкой «ЧЧ:ММ»: в предпросмотре оно только показывается.
Assert.All(dto.Slots, slot => Assert.Equal("20:00", slot.Start));
Assert.Equal(6, dto.Slots[2].Weekday);
Assert.Null(dto.Slots[0].Weekday);
// У неконтентных слотов группы нет — показывать в колонке нечего.
Assert.Null(dto.Slots[3].GroupName);
Assert.Equal("Сериалы", dto.Slots[0].GroupName);
}
[Fact]
public async Task Preview_ReturnsPlanForProfile()
{
var fixture = new TestDb();
var channelId = await SeedAsync(fixture);
await using var db = fixture.New();
var result = await new PreviewGeneratedGridQueryHandler(
new GridPlanner(
db,
new GroupCatalog(db, GroupServices.Dynamic(db), new GroupElementResolver(db))
)
).Handle(
new PreviewGeneratedGridQuery(
channelId,
GridProfileKind.Kids,
GridGenerationMode.Rebuild
),
CancellationToken.None
);
Assert.True(result.IsSuccess);
Assert.Equal(GridProfileKind.Kids, result.Value.Profile);
Assert.NotEmpty(result.Value.Slots);
// Пересборка сносит то, что было, и это видно до нажатия.
Assert.True(result.Value.SlotsToRemove >= 0);
Assert.Equal(7 * GridCoverage.MinutesInDay, result.Value.FreeMinutes);
// У детского профиля ночь — конец вещания, и в предпросмотре он подписан именно так.
Assert.Contains(result.Value.Slots, s => s.Block == "конец вещания");
}
[Fact]
public async Task Preview_FailsWhenChannelIsGone()
{
var fixture = new TestDb();
await using var db = fixture.New();
var result = await new PreviewGeneratedGridQueryHandler(
new GridPlanner(
db,
new GroupCatalog(db, GroupServices.Dynamic(db), new GroupElementResolver(db))
)
).Handle(
new PreviewGeneratedGridQuery(
Guid.NewGuid(),
GridProfileKind.Mixed,
GridGenerationMode.FillGaps
),
CancellationToken.None
);
Assert.False(result.IsSuccess);
}
[Fact]
public void Validator_RejectsUnknownProfileAndMode()
{
var validator = new GenerateGridCommandValidator();
Assert.True(
validator
.Validate(
new GenerateGridCommand(
Guid.NewGuid(),
GridProfileKind.Sitcom,
GridGenerationMode.Rebuild
)
)
.IsValid
);
// Значения enum приходят с клиента строкой — за пределами каталога они бессмысленны.
Assert.False(
validator
.Validate(
new GenerateGridCommand(
Guid.NewGuid(),
(GridProfileKind)42,
GridGenerationMode.FillGaps
)
)
.IsValid
);
}
/// <summary>Канал с одной группой мягкого рейтинга — чтобы детскому профилю было чем закрыться.</summary>
private static async Task<Guid> SeedAsync(TestDb fixture)
{
var show = Show.Create("Мультсериал", ShowKind.Series);
show.SetAudience(ShowAudience.G);
var assets = new List<MediaAsset>();
for (var i = 0; i < 20; i++)
{
var asset = MediaAsset.Register($"e{i}.mkv", ".mkv", MediaSource.Upload);
asset.MarkProcessing();
asset.MarkReady(
new MediaReadyInfo(
TimeSpan.FromMinutes(22),
6,
220,
1920,
1080,
"h264",
"aac",
$"assets/e{i}"
)
);
assets.Add(asset);
show.AddEpisode(asset.Id);
}
var group = Group.Create("Мультфильмы");
group.AddElement(GroupElementKind.Show, show.Id);
var channel = Channel.Create("Детский", "kids", T0);
var template = ScheduleTemplate.Create(channel.Id, "Сетка");
channel.SetTemplate(template.Id);
await using var seed = fixture.New();
seed.MediaAssets.AddRange(assets);
seed.Shows.Add(show);
seed.Groups.Add(group);
seed.Channels.Add(channel);
seed.ScheduleTemplates.Add(template);
await seed.SaveChangesAsync(CancellationToken.None);
return channel.Id;
}
}