Normalize line endings to LF via .gitattributes
Репозиторий хранил фронтенд в CRLF, а часть бэкенда — вперемешку, хотя CI и Docker-сборка работают под Linux. Прибиваем LF атрибутом `* text=auto eol=lf` и разово нормализуем дерево, чтобы форматтеры не переписывали файлы целиком на каждом прогоне. Коммит чисто механический: изменений содержимого нет, только концы строк. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
9d1c6d2fc3
commit
0442056367
@@ -1,183 +1,183 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using TeleWave.Application.Programming.Templates;
|
||||
using TeleWave.Application.Programming.Templates.CopyTemplate;
|
||||
using TeleWave.Application.Programming.Templates.CreateTemplate;
|
||||
using TeleWave.Application.Programming.Templates.Validate;
|
||||
using TeleWave.Domain.Broadcast;
|
||||
using TeleWave.Domain.Programming;
|
||||
using TeleWave.Infrastructure.Persistence;
|
||||
using Xunit;
|
||||
|
||||
namespace TeleWave.Integration.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Операции над шаблоном против настоящей БД: копирование на другой канал и проверки по правилам.
|
||||
/// Обе штуки — сплошные запросы EF и перекладывание графов, в юнит-тестах их не поймать.
|
||||
/// </summary>
|
||||
[Collection("postgres")]
|
||||
public sealed class TemplateOperationsIntegrationTests(PostgresFixture fixture)
|
||||
{
|
||||
[SkippableFact]
|
||||
public async Task Copy_MovesLayersSlotsAndJunctions_AndReplacesTargetGrid()
|
||||
{
|
||||
Skip.IfNot(fixture.Available, "Docker недоступен");
|
||||
|
||||
await using var seedDb = fixture.CreateContext();
|
||||
var (sourceId, targetId, groupId) = await SeedPairAsync(seedDb);
|
||||
|
||||
await using var db = fixture.CreateContext();
|
||||
var result = await new CopyTemplateCommandHandler(db).Handle(
|
||||
new CopyTemplateCommand(sourceId, targetId),
|
||||
default
|
||||
);
|
||||
Assert.True(result.IsSuccess);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
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);
|
||||
var copied = verify
|
||||
.ScheduleTemplates.Include(t => t.Layers)
|
||||
.ThenInclude(l => l.Slots)
|
||||
.Single(t => t.ChannelId == targetId);
|
||||
|
||||
// У приёмника ровно один шаблон, и канал смотрит именно на него.
|
||||
Assert.Equal(copied.Id, target.TemplateId);
|
||||
Assert.Single(verify.ScheduleTemplates.Where(t => t.ChannelId == targetId));
|
||||
|
||||
var slot = copied.Layers.SelectMany(l => l.Slots).Single();
|
||||
// Группы общие — ссылка переносится как есть, а не копией группы.
|
||||
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
|
||||
);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Copy_ToItself_IsRejectedByValidator()
|
||||
{
|
||||
var validator = new CopyTemplateCommandValidator();
|
||||
var id = Guid.NewGuid();
|
||||
|
||||
Assert.False(validator.Validate(new CopyTemplateCommand(id, id)).IsValid);
|
||||
Assert.True(validator.Validate(new CopyTemplateCommand(id, Guid.NewGuid())).IsValid);
|
||||
}
|
||||
|
||||
[SkippableFact]
|
||||
public async Task Validate_ReportsEmptyGroupAndGridGap()
|
||||
{
|
||||
Skip.IfNot(fixture.Available, "Docker недоступен");
|
||||
|
||||
await using var seedDb = fixture.CreateContext();
|
||||
var (sourceId, _, _) = await SeedPairAsync(seedDb);
|
||||
|
||||
await using var db = fixture.CreateContext();
|
||||
var result = await new ValidateTemplateQueryHandler(db).Handle(
|
||||
new ValidateTemplateQuery(sourceId),
|
||||
default
|
||||
);
|
||||
|
||||
Assert.True(result.IsSuccess);
|
||||
// Группа в сиде пустая, а слот занимает лишь два часа суток — обе проверки должны сработать.
|
||||
Assert.Contains(result.Value, i => i.Kind == TemplateIssueKind.GroupEmpty);
|
||||
Assert.Contains(result.Value, i => i.Kind == TemplateIssueKind.GridGap);
|
||||
}
|
||||
|
||||
[SkippableFact]
|
||||
public async Task CreateTemplate_ForChannelWithoutGrid_LinksItAndIsIdempotent()
|
||||
{
|
||||
Skip.IfNot(fixture.Available, "Docker недоступен");
|
||||
|
||||
// Канал из старой ротации: сетки нет, ссылки на неё тоже.
|
||||
await using var seedDb = fixture.CreateContext();
|
||||
var suffix = Guid.NewGuid().ToString("N")[..8];
|
||||
var channel = Channel.Create(
|
||||
$"Без сетки {suffix}",
|
||||
$"nogrid-{suffix}",
|
||||
DateTimeOffset.UtcNow
|
||||
);
|
||||
seedDb.Channels.Add(channel);
|
||||
await seedDb.SaveChangesAsync();
|
||||
|
||||
await using var db = fixture.CreateContext();
|
||||
var created = await new CreateChannelTemplateCommandHandler(db).Handle(
|
||||
new CreateChannelTemplateCommand(channel.Id),
|
||||
default
|
||||
);
|
||||
Assert.True(created.IsSuccess);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
await using var again = fixture.CreateContext();
|
||||
var second = await new CreateChannelTemplateCommandHandler(again).Handle(
|
||||
new CreateChannelTemplateCommand(channel.Id),
|
||||
default
|
||||
);
|
||||
await again.SaveChangesAsync();
|
||||
|
||||
// Повторный вызов возвращает ту же сетку: одна на канал, второй не появляется.
|
||||
Assert.True(second.IsSuccess);
|
||||
Assert.Equal(created.Value, second.Value);
|
||||
|
||||
await using var verify = fixture.CreateContext();
|
||||
var template = verify.ScheduleTemplates.Single(t => t.ChannelId == channel.Id);
|
||||
Assert.Equal(template.Id, verify.Channels.Single(c => c.Id == channel.Id).TemplateId);
|
||||
// Фоновый слой заводится сразу — без него первую же дыру в сетке нечем закрыть.
|
||||
Assert.Single(verify.GridLayers.Where(l => l.TemplateId == template.Id && l.IsBackground));
|
||||
}
|
||||
|
||||
/// <summary>Два канала: у источника слой со слотом, группой и стыком; у приёмника — пустая сетка.</summary>
|
||||
private static async Task<(Guid Source, Guid Target, Guid GroupId)> SeedPairAsync(
|
||||
AppDbContext db
|
||||
)
|
||||
{
|
||||
var suffix = Guid.NewGuid().ToString("N")[..8];
|
||||
|
||||
var group = Group.Create($"Группа {suffix}");
|
||||
db.Groups.Add(group);
|
||||
|
||||
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 ad = junction.AddElement(JunctionElementKind.Ad);
|
||||
ad.Update(JunctionElementKind.Ad, group.Id, null, JunctionAmountMode.Count, 2, true, null);
|
||||
db.JunctionTemplates.Add(junction);
|
||||
|
||||
var sourceTemplate = ScheduleTemplate.Create(source.Id, "Сетка источника");
|
||||
var layer = sourceTemplate.AddLayer("Прайм", 10);
|
||||
var slot = layer.AddSlot("Вечернее кино", new TimeOnly(20, 0), 120);
|
||||
slot.UpdateContent(
|
||||
new SlotContent(
|
||||
slot.Title,
|
||||
SlotKind.Content,
|
||||
group.Id,
|
||||
new SlotStrategy(SlotStrategyType.Sequential).ToJson(),
|
||||
null,
|
||||
SlotBlockMode.FillSlot,
|
||||
1,
|
||||
OverflowPolicy.ContinueNext,
|
||||
JunctionAfterId: junction.Id
|
||||
)
|
||||
);
|
||||
sourceTemplate.SetDefaultJunction(junction.Id);
|
||||
source.SetTemplate(sourceTemplate.Id);
|
||||
|
||||
var targetTemplate = ScheduleTemplate.Create(target.Id, "Сетка приёмника");
|
||||
target.SetTemplate(targetTemplate.Id);
|
||||
|
||||
db.Channels.AddRange(source, target);
|
||||
db.ScheduleTemplates.AddRange(sourceTemplate, targetTemplate);
|
||||
|
||||
await db.SaveChangesAsync();
|
||||
return (source.Id, target.Id, group.Id);
|
||||
}
|
||||
}
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using TeleWave.Application.Programming.Templates;
|
||||
using TeleWave.Application.Programming.Templates.CopyTemplate;
|
||||
using TeleWave.Application.Programming.Templates.CreateTemplate;
|
||||
using TeleWave.Application.Programming.Templates.Validate;
|
||||
using TeleWave.Domain.Broadcast;
|
||||
using TeleWave.Domain.Programming;
|
||||
using TeleWave.Infrastructure.Persistence;
|
||||
using Xunit;
|
||||
|
||||
namespace TeleWave.Integration.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Операции над шаблоном против настоящей БД: копирование на другой канал и проверки по правилам.
|
||||
/// Обе штуки — сплошные запросы EF и перекладывание графов, в юнит-тестах их не поймать.
|
||||
/// </summary>
|
||||
[Collection("postgres")]
|
||||
public sealed class TemplateOperationsIntegrationTests(PostgresFixture fixture)
|
||||
{
|
||||
[SkippableFact]
|
||||
public async Task Copy_MovesLayersSlotsAndJunctions_AndReplacesTargetGrid()
|
||||
{
|
||||
Skip.IfNot(fixture.Available, "Docker недоступен");
|
||||
|
||||
await using var seedDb = fixture.CreateContext();
|
||||
var (sourceId, targetId, groupId) = await SeedPairAsync(seedDb);
|
||||
|
||||
await using var db = fixture.CreateContext();
|
||||
var result = await new CopyTemplateCommandHandler(db).Handle(
|
||||
new CopyTemplateCommand(sourceId, targetId),
|
||||
default
|
||||
);
|
||||
Assert.True(result.IsSuccess);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
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);
|
||||
var copied = verify
|
||||
.ScheduleTemplates.Include(t => t.Layers)
|
||||
.ThenInclude(l => l.Slots)
|
||||
.Single(t => t.ChannelId == targetId);
|
||||
|
||||
// У приёмника ровно один шаблон, и канал смотрит именно на него.
|
||||
Assert.Equal(copied.Id, target.TemplateId);
|
||||
Assert.Single(verify.ScheduleTemplates.Where(t => t.ChannelId == targetId));
|
||||
|
||||
var slot = copied.Layers.SelectMany(l => l.Slots).Single();
|
||||
// Группы общие — ссылка переносится как есть, а не копией группы.
|
||||
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
|
||||
);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Copy_ToItself_IsRejectedByValidator()
|
||||
{
|
||||
var validator = new CopyTemplateCommandValidator();
|
||||
var id = Guid.NewGuid();
|
||||
|
||||
Assert.False(validator.Validate(new CopyTemplateCommand(id, id)).IsValid);
|
||||
Assert.True(validator.Validate(new CopyTemplateCommand(id, Guid.NewGuid())).IsValid);
|
||||
}
|
||||
|
||||
[SkippableFact]
|
||||
public async Task Validate_ReportsEmptyGroupAndGridGap()
|
||||
{
|
||||
Skip.IfNot(fixture.Available, "Docker недоступен");
|
||||
|
||||
await using var seedDb = fixture.CreateContext();
|
||||
var (sourceId, _, _) = await SeedPairAsync(seedDb);
|
||||
|
||||
await using var db = fixture.CreateContext();
|
||||
var result = await new ValidateTemplateQueryHandler(db).Handle(
|
||||
new ValidateTemplateQuery(sourceId),
|
||||
default
|
||||
);
|
||||
|
||||
Assert.True(result.IsSuccess);
|
||||
// Группа в сиде пустая, а слот занимает лишь два часа суток — обе проверки должны сработать.
|
||||
Assert.Contains(result.Value, i => i.Kind == TemplateIssueKind.GroupEmpty);
|
||||
Assert.Contains(result.Value, i => i.Kind == TemplateIssueKind.GridGap);
|
||||
}
|
||||
|
||||
[SkippableFact]
|
||||
public async Task CreateTemplate_ForChannelWithoutGrid_LinksItAndIsIdempotent()
|
||||
{
|
||||
Skip.IfNot(fixture.Available, "Docker недоступен");
|
||||
|
||||
// Канал из старой ротации: сетки нет, ссылки на неё тоже.
|
||||
await using var seedDb = fixture.CreateContext();
|
||||
var suffix = Guid.NewGuid().ToString("N")[..8];
|
||||
var channel = Channel.Create(
|
||||
$"Без сетки {suffix}",
|
||||
$"nogrid-{suffix}",
|
||||
DateTimeOffset.UtcNow
|
||||
);
|
||||
seedDb.Channels.Add(channel);
|
||||
await seedDb.SaveChangesAsync();
|
||||
|
||||
await using var db = fixture.CreateContext();
|
||||
var created = await new CreateChannelTemplateCommandHandler(db).Handle(
|
||||
new CreateChannelTemplateCommand(channel.Id),
|
||||
default
|
||||
);
|
||||
Assert.True(created.IsSuccess);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
await using var again = fixture.CreateContext();
|
||||
var second = await new CreateChannelTemplateCommandHandler(again).Handle(
|
||||
new CreateChannelTemplateCommand(channel.Id),
|
||||
default
|
||||
);
|
||||
await again.SaveChangesAsync();
|
||||
|
||||
// Повторный вызов возвращает ту же сетку: одна на канал, второй не появляется.
|
||||
Assert.True(second.IsSuccess);
|
||||
Assert.Equal(created.Value, second.Value);
|
||||
|
||||
await using var verify = fixture.CreateContext();
|
||||
var template = verify.ScheduleTemplates.Single(t => t.ChannelId == channel.Id);
|
||||
Assert.Equal(template.Id, verify.Channels.Single(c => c.Id == channel.Id).TemplateId);
|
||||
// Фоновый слой заводится сразу — без него первую же дыру в сетке нечем закрыть.
|
||||
Assert.Single(verify.GridLayers.Where(l => l.TemplateId == template.Id && l.IsBackground));
|
||||
}
|
||||
|
||||
/// <summary>Два канала: у источника слой со слотом, группой и стыком; у приёмника — пустая сетка.</summary>
|
||||
private static async Task<(Guid Source, Guid Target, Guid GroupId)> SeedPairAsync(
|
||||
AppDbContext db
|
||||
)
|
||||
{
|
||||
var suffix = Guid.NewGuid().ToString("N")[..8];
|
||||
|
||||
var group = Group.Create($"Группа {suffix}");
|
||||
db.Groups.Add(group);
|
||||
|
||||
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 ad = junction.AddElement(JunctionElementKind.Ad);
|
||||
ad.Update(JunctionElementKind.Ad, group.Id, null, JunctionAmountMode.Count, 2, true, null);
|
||||
db.JunctionTemplates.Add(junction);
|
||||
|
||||
var sourceTemplate = ScheduleTemplate.Create(source.Id, "Сетка источника");
|
||||
var layer = sourceTemplate.AddLayer("Прайм", 10);
|
||||
var slot = layer.AddSlot("Вечернее кино", new TimeOnly(20, 0), 120);
|
||||
slot.UpdateContent(
|
||||
new SlotContent(
|
||||
slot.Title,
|
||||
SlotKind.Content,
|
||||
group.Id,
|
||||
new SlotStrategy(SlotStrategyType.Sequential).ToJson(),
|
||||
null,
|
||||
SlotBlockMode.FillSlot,
|
||||
1,
|
||||
OverflowPolicy.ContinueNext,
|
||||
JunctionAfterId: junction.Id
|
||||
)
|
||||
);
|
||||
sourceTemplate.SetDefaultJunction(junction.Id);
|
||||
source.SetTemplate(sourceTemplate.Id);
|
||||
|
||||
var targetTemplate = ScheduleTemplate.Create(target.Id, "Сетка приёмника");
|
||||
target.SetTemplate(targetTemplate.Id);
|
||||
|
||||
db.Channels.AddRange(source, target);
|
||||
db.ScheduleTemplates.AddRange(sourceTemplate, targetTemplate);
|
||||
|
||||
await db.SaveChangesAsync();
|
||||
return (source.Id, target.Id, group.Id);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user