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,69 +1,69 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using TeleWave.Application.Broadcast;
|
||||
using TeleWave.Application.Broadcast.CreateChannel;
|
||||
using TeleWave.Application.Broadcast.UpdateChannelSettings;
|
||||
using TeleWave.Application.Tests.Support;
|
||||
using TeleWave.Domain.Broadcast;
|
||||
using TeleWave.Domain.Library;
|
||||
using TeleWave.Domain.Media;
|
||||
using Xunit;
|
||||
|
||||
namespace TeleWave.Application.Tests.Broadcast;
|
||||
|
||||
public class ChannelHandlersTests
|
||||
{
|
||||
private static readonly DateTimeOffset T0 = new(2026, 1, 1, 0, 0, 0, TimeSpan.Zero);
|
||||
|
||||
[Fact]
|
||||
public async Task CreateChannel_Succeeds_AndRejectsDuplicateSlug()
|
||||
{
|
||||
var fixture = new TestDb();
|
||||
await using var db = fixture.New();
|
||||
|
||||
var ok = await new CreateChannelCommandHandler(db).Handle(
|
||||
new CreateChannelCommand("News", "news"),
|
||||
CancellationToken.None
|
||||
);
|
||||
Assert.True(ok.IsSuccess);
|
||||
await db.SaveChangesAsync(CancellationToken.None);
|
||||
|
||||
var dup = await new CreateChannelCommandHandler(db).Handle(
|
||||
new CreateChannelCommand("Other", "news"),
|
||||
CancellationToken.None
|
||||
);
|
||||
Assert.False(dup.IsSuccess);
|
||||
Assert.Equal(ChannelErrors.DuplicateSlug, dup.Error);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UpdateChannelSettings_UpdatesBumperChances()
|
||||
{
|
||||
var fixture = new TestDb();
|
||||
var channel = Channel.Create("c", "c", 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 UpdateChannelSettingsCommandHandler(db).Handle(
|
||||
new UpdateChannelSettingsCommand(
|
||||
channel.Id,
|
||||
"c",
|
||||
true,
|
||||
true,
|
||||
new BumperSettingsInput(BumperFont.Sans, BumperSelection.WeightedRandom),
|
||||
null
|
||||
),
|
||||
CancellationToken.None
|
||||
);
|
||||
Assert.True(result.IsSuccess);
|
||||
await db.SaveChangesAsync(CancellationToken.None);
|
||||
|
||||
await using var verify = fixture.New();
|
||||
var stored = await verify.Channels.FindAsync(channel.Id);
|
||||
Assert.Equal(BumperFont.Sans, stored!.BumperFont);
|
||||
Assert.Equal(BumperSelection.WeightedRandom, stored.BumperSelection);
|
||||
}
|
||||
}
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using TeleWave.Application.Broadcast;
|
||||
using TeleWave.Application.Broadcast.CreateChannel;
|
||||
using TeleWave.Application.Broadcast.UpdateChannelSettings;
|
||||
using TeleWave.Application.Tests.Support;
|
||||
using TeleWave.Domain.Broadcast;
|
||||
using TeleWave.Domain.Library;
|
||||
using TeleWave.Domain.Media;
|
||||
using Xunit;
|
||||
|
||||
namespace TeleWave.Application.Tests.Broadcast;
|
||||
|
||||
public class ChannelHandlersTests
|
||||
{
|
||||
private static readonly DateTimeOffset T0 = new(2026, 1, 1, 0, 0, 0, TimeSpan.Zero);
|
||||
|
||||
[Fact]
|
||||
public async Task CreateChannel_Succeeds_AndRejectsDuplicateSlug()
|
||||
{
|
||||
var fixture = new TestDb();
|
||||
await using var db = fixture.New();
|
||||
|
||||
var ok = await new CreateChannelCommandHandler(db).Handle(
|
||||
new CreateChannelCommand("News", "news"),
|
||||
CancellationToken.None
|
||||
);
|
||||
Assert.True(ok.IsSuccess);
|
||||
await db.SaveChangesAsync(CancellationToken.None);
|
||||
|
||||
var dup = await new CreateChannelCommandHandler(db).Handle(
|
||||
new CreateChannelCommand("Other", "news"),
|
||||
CancellationToken.None
|
||||
);
|
||||
Assert.False(dup.IsSuccess);
|
||||
Assert.Equal(ChannelErrors.DuplicateSlug, dup.Error);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UpdateChannelSettings_UpdatesBumperChances()
|
||||
{
|
||||
var fixture = new TestDb();
|
||||
var channel = Channel.Create("c", "c", 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 UpdateChannelSettingsCommandHandler(db).Handle(
|
||||
new UpdateChannelSettingsCommand(
|
||||
channel.Id,
|
||||
"c",
|
||||
true,
|
||||
true,
|
||||
new BumperSettingsInput(BumperFont.Sans, BumperSelection.WeightedRandom),
|
||||
null
|
||||
),
|
||||
CancellationToken.None
|
||||
);
|
||||
Assert.True(result.IsSuccess);
|
||||
await db.SaveChangesAsync(CancellationToken.None);
|
||||
|
||||
await using var verify = fixture.New();
|
||||
var stored = await verify.Channels.FindAsync(channel.Id);
|
||||
Assert.Equal(BumperFont.Sans, stored!.BumperFont);
|
||||
Assert.Equal(BumperSelection.WeightedRandom, stored.BumperSelection);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,201 +1,201 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using NSubstitute;
|
||||
using TeleWave.Application.Broadcast;
|
||||
using TeleWave.Application.Broadcast.Bumpers;
|
||||
using TeleWave.Application.Broadcast.GetChannel;
|
||||
using TeleWave.Application.Broadcast.GetSchedule;
|
||||
using TeleWave.Application.Broadcast.ListChannels;
|
||||
using TeleWave.Application.Common.Interfaces;
|
||||
using TeleWave.Application.Library.DeleteShow;
|
||||
using TeleWave.Application.Library.GetShow;
|
||||
using TeleWave.Application.Programming.Groups;
|
||||
using TeleWave.Application.Tests.Support;
|
||||
using TeleWave.Domain.Broadcast;
|
||||
using TeleWave.Domain.Library;
|
||||
using TeleWave.Domain.Media;
|
||||
using Xunit;
|
||||
|
||||
namespace TeleWave.Application.Tests.Broadcast;
|
||||
|
||||
public class QueryHandlersTests
|
||||
{
|
||||
private static readonly DateTimeOffset T0 = new(2026, 1, 1, 0, 0, 0, TimeSpan.Zero);
|
||||
|
||||
private static GroupMembershipCleaner GroupCleaner(IAppDbContext db) =>
|
||||
new(db, new GroupStatsService(db, new GroupElementResolver(db)));
|
||||
|
||||
[Fact]
|
||||
public async Task GetChannel_UnknownId_ReturnsNotFound()
|
||||
{
|
||||
var fixture = new TestDb();
|
||||
await using var db = fixture.New();
|
||||
var result = await new GetChannelQueryHandler(db).Handle(
|
||||
new GetChannelQuery(Guid.NewGuid()),
|
||||
CancellationToken.None
|
||||
);
|
||||
Assert.False(result.IsSuccess);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ListChannels_ReturnsSummaries()
|
||||
{
|
||||
var fixture = new TestDb();
|
||||
await using (var seed = fixture.New())
|
||||
{
|
||||
seed.Channels.Add(Channel.Create("A", "a", T0));
|
||||
seed.Channels.Add(Channel.Create("B", "b", T0));
|
||||
await seed.SaveChangesAsync(CancellationToken.None);
|
||||
}
|
||||
|
||||
await using var db = fixture.New();
|
||||
var result = await new ListChannelsQueryHandler(db).Handle(
|
||||
new ListChannelsQuery(),
|
||||
CancellationToken.None
|
||||
);
|
||||
Assert.Equal(2, result.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetChannelSchedule_ReturnsEntriesWithBumperLabel()
|
||||
{
|
||||
var fixture = new TestDb();
|
||||
var channel = Channel.Create("c", "c", T0);
|
||||
var show = Show.Create("Show A", ShowKind.Series);
|
||||
var variant = channel.BumperTemplates[0].Variants[0];
|
||||
var asset = MediaAsset.Register("Show.A.S01E01.mkv", ".mkv", MediaSource.Upload);
|
||||
var program = ScheduleEntry.Program(
|
||||
channel.Id,
|
||||
asset.Id,
|
||||
T0,
|
||||
T0.AddMinutes(20),
|
||||
show.Id,
|
||||
0
|
||||
);
|
||||
var bumper = ScheduleEntry.Bumper(
|
||||
channel.Id,
|
||||
Guid.NewGuid(),
|
||||
T0.AddMinutes(20),
|
||||
T0.AddMinutes(20).AddSeconds(8),
|
||||
show.Id,
|
||||
variant.Id
|
||||
);
|
||||
await using (var seed = fixture.New())
|
||||
{
|
||||
seed.Shows.Add(show);
|
||||
seed.Channels.Add(channel);
|
||||
seed.MediaAssets.Add(asset);
|
||||
seed.ScheduleEntries.Add(program);
|
||||
seed.ScheduleEntries.Add(bumper);
|
||||
await seed.SaveChangesAsync(CancellationToken.None);
|
||||
}
|
||||
|
||||
await using var db = fixture.New();
|
||||
var result = await new GetChannelScheduleQueryHandler(db).Handle(
|
||||
new GetChannelScheduleQuery(channel.Id, T0.AddMinutes(-5), T0.AddHours(1)),
|
||||
CancellationToken.None
|
||||
);
|
||||
|
||||
Assert.True(result.IsSuccess);
|
||||
Assert.Equal(2, result.Value.Count);
|
||||
var bumperDto = result.Value.Single(e => e.Kind == ScheduleEntryKind.Bumper);
|
||||
Assert.NotNull(bumperDto.BumperName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetShow_ReturnsDtoOrNotFound()
|
||||
{
|
||||
var fixture = new TestDb();
|
||||
var show = Show.Create("A", ShowKind.Series);
|
||||
await using (var seed = fixture.New())
|
||||
{
|
||||
seed.Shows.Add(show);
|
||||
await seed.SaveChangesAsync(CancellationToken.None);
|
||||
}
|
||||
|
||||
await using var db = fixture.New();
|
||||
var ok = await new GetShowQueryHandler(db).Handle(
|
||||
new GetShowQuery(show.Id),
|
||||
CancellationToken.None
|
||||
);
|
||||
Assert.True(ok.IsSuccess);
|
||||
|
||||
var missing = await new GetShowQueryHandler(db).Handle(
|
||||
new GetShowQuery(Guid.NewGuid()),
|
||||
CancellationToken.None
|
||||
);
|
||||
Assert.False(missing.IsSuccess);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DeleteShow_RemovesOrNotFound()
|
||||
{
|
||||
var fixture = new TestDb();
|
||||
var show = Show.Create("A", ShowKind.Series);
|
||||
await using (var seed = fixture.New())
|
||||
{
|
||||
seed.Shows.Add(show);
|
||||
await seed.SaveChangesAsync(CancellationToken.None);
|
||||
}
|
||||
|
||||
await using var db = fixture.New();
|
||||
var missing = await new DeleteShowCommandHandler(db, GroupCleaner(db)).Handle(
|
||||
new DeleteShowCommand(Guid.NewGuid()),
|
||||
CancellationToken.None
|
||||
);
|
||||
Assert.False(missing.IsSuccess);
|
||||
|
||||
var ok = await new DeleteShowCommandHandler(db, GroupCleaner(db)).Handle(
|
||||
new DeleteShowCommand(show.Id),
|
||||
CancellationToken.None
|
||||
);
|
||||
Assert.True(ok.IsSuccess);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AddAndRemoveBumperTemplate_WorkThroughStorage()
|
||||
{
|
||||
var fixture = new TestDb();
|
||||
var channel = Channel.Create("c", "c", T0);
|
||||
await using (var seed = fixture.New())
|
||||
{
|
||||
seed.Channels.Add(channel);
|
||||
await seed.SaveChangesAsync(CancellationToken.None);
|
||||
}
|
||||
|
||||
Guid templateId;
|
||||
await using (var db = fixture.New())
|
||||
{
|
||||
var added = await new AddBumperTemplateCommandHandler(db).Handle(
|
||||
new AddBumperTemplateCommand(channel.Id, ""),
|
||||
CancellationToken.None
|
||||
);
|
||||
Assert.True(added.IsSuccess);
|
||||
templateId = added.Value;
|
||||
await db.SaveChangesAsync(CancellationToken.None);
|
||||
}
|
||||
|
||||
var storage = Substitute.For<IBumperTemplateStorage>();
|
||||
await using (var db = fixture.New())
|
||||
{
|
||||
var removed = await new RemoveBumperTemplateCommandHandler(db, storage).Handle(
|
||||
new RemoveBumperTemplateCommand(channel.Id, templateId),
|
||||
CancellationToken.None
|
||||
);
|
||||
Assert.True(removed.IsSuccess);
|
||||
}
|
||||
await storage.Received(1).DeleteTemplateAsync(templateId, Arg.Any<CancellationToken>());
|
||||
|
||||
// дефолтный блок удалить нельзя
|
||||
await using (var db = fixture.New())
|
||||
{
|
||||
var def = await db
|
||||
.Channels.Include(c => c.BumperTemplates)
|
||||
.FirstAsync(c => c.Id == channel.Id);
|
||||
var result = await new RemoveBumperTemplateCommandHandler(db, storage).Handle(
|
||||
new RemoveBumperTemplateCommand(channel.Id, def.BumperTemplates[0].Id),
|
||||
CancellationToken.None
|
||||
);
|
||||
Assert.Equal(ChannelErrors.CannotRemoveDefaultBumperTemplate, result.Error);
|
||||
}
|
||||
}
|
||||
}
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using NSubstitute;
|
||||
using TeleWave.Application.Broadcast;
|
||||
using TeleWave.Application.Broadcast.Bumpers;
|
||||
using TeleWave.Application.Broadcast.GetChannel;
|
||||
using TeleWave.Application.Broadcast.GetSchedule;
|
||||
using TeleWave.Application.Broadcast.ListChannels;
|
||||
using TeleWave.Application.Common.Interfaces;
|
||||
using TeleWave.Application.Library.DeleteShow;
|
||||
using TeleWave.Application.Library.GetShow;
|
||||
using TeleWave.Application.Programming.Groups;
|
||||
using TeleWave.Application.Tests.Support;
|
||||
using TeleWave.Domain.Broadcast;
|
||||
using TeleWave.Domain.Library;
|
||||
using TeleWave.Domain.Media;
|
||||
using Xunit;
|
||||
|
||||
namespace TeleWave.Application.Tests.Broadcast;
|
||||
|
||||
public class QueryHandlersTests
|
||||
{
|
||||
private static readonly DateTimeOffset T0 = new(2026, 1, 1, 0, 0, 0, TimeSpan.Zero);
|
||||
|
||||
private static GroupMembershipCleaner GroupCleaner(IAppDbContext db) =>
|
||||
new(db, new GroupStatsService(db, new GroupElementResolver(db)));
|
||||
|
||||
[Fact]
|
||||
public async Task GetChannel_UnknownId_ReturnsNotFound()
|
||||
{
|
||||
var fixture = new TestDb();
|
||||
await using var db = fixture.New();
|
||||
var result = await new GetChannelQueryHandler(db).Handle(
|
||||
new GetChannelQuery(Guid.NewGuid()),
|
||||
CancellationToken.None
|
||||
);
|
||||
Assert.False(result.IsSuccess);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ListChannels_ReturnsSummaries()
|
||||
{
|
||||
var fixture = new TestDb();
|
||||
await using (var seed = fixture.New())
|
||||
{
|
||||
seed.Channels.Add(Channel.Create("A", "a", T0));
|
||||
seed.Channels.Add(Channel.Create("B", "b", T0));
|
||||
await seed.SaveChangesAsync(CancellationToken.None);
|
||||
}
|
||||
|
||||
await using var db = fixture.New();
|
||||
var result = await new ListChannelsQueryHandler(db).Handle(
|
||||
new ListChannelsQuery(),
|
||||
CancellationToken.None
|
||||
);
|
||||
Assert.Equal(2, result.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetChannelSchedule_ReturnsEntriesWithBumperLabel()
|
||||
{
|
||||
var fixture = new TestDb();
|
||||
var channel = Channel.Create("c", "c", T0);
|
||||
var show = Show.Create("Show A", ShowKind.Series);
|
||||
var variant = channel.BumperTemplates[0].Variants[0];
|
||||
var asset = MediaAsset.Register("Show.A.S01E01.mkv", ".mkv", MediaSource.Upload);
|
||||
var program = ScheduleEntry.Program(
|
||||
channel.Id,
|
||||
asset.Id,
|
||||
T0,
|
||||
T0.AddMinutes(20),
|
||||
show.Id,
|
||||
0
|
||||
);
|
||||
var bumper = ScheduleEntry.Bumper(
|
||||
channel.Id,
|
||||
Guid.NewGuid(),
|
||||
T0.AddMinutes(20),
|
||||
T0.AddMinutes(20).AddSeconds(8),
|
||||
show.Id,
|
||||
variant.Id
|
||||
);
|
||||
await using (var seed = fixture.New())
|
||||
{
|
||||
seed.Shows.Add(show);
|
||||
seed.Channels.Add(channel);
|
||||
seed.MediaAssets.Add(asset);
|
||||
seed.ScheduleEntries.Add(program);
|
||||
seed.ScheduleEntries.Add(bumper);
|
||||
await seed.SaveChangesAsync(CancellationToken.None);
|
||||
}
|
||||
|
||||
await using var db = fixture.New();
|
||||
var result = await new GetChannelScheduleQueryHandler(db).Handle(
|
||||
new GetChannelScheduleQuery(channel.Id, T0.AddMinutes(-5), T0.AddHours(1)),
|
||||
CancellationToken.None
|
||||
);
|
||||
|
||||
Assert.True(result.IsSuccess);
|
||||
Assert.Equal(2, result.Value.Count);
|
||||
var bumperDto = result.Value.Single(e => e.Kind == ScheduleEntryKind.Bumper);
|
||||
Assert.NotNull(bumperDto.BumperName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetShow_ReturnsDtoOrNotFound()
|
||||
{
|
||||
var fixture = new TestDb();
|
||||
var show = Show.Create("A", ShowKind.Series);
|
||||
await using (var seed = fixture.New())
|
||||
{
|
||||
seed.Shows.Add(show);
|
||||
await seed.SaveChangesAsync(CancellationToken.None);
|
||||
}
|
||||
|
||||
await using var db = fixture.New();
|
||||
var ok = await new GetShowQueryHandler(db).Handle(
|
||||
new GetShowQuery(show.Id),
|
||||
CancellationToken.None
|
||||
);
|
||||
Assert.True(ok.IsSuccess);
|
||||
|
||||
var missing = await new GetShowQueryHandler(db).Handle(
|
||||
new GetShowQuery(Guid.NewGuid()),
|
||||
CancellationToken.None
|
||||
);
|
||||
Assert.False(missing.IsSuccess);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DeleteShow_RemovesOrNotFound()
|
||||
{
|
||||
var fixture = new TestDb();
|
||||
var show = Show.Create("A", ShowKind.Series);
|
||||
await using (var seed = fixture.New())
|
||||
{
|
||||
seed.Shows.Add(show);
|
||||
await seed.SaveChangesAsync(CancellationToken.None);
|
||||
}
|
||||
|
||||
await using var db = fixture.New();
|
||||
var missing = await new DeleteShowCommandHandler(db, GroupCleaner(db)).Handle(
|
||||
new DeleteShowCommand(Guid.NewGuid()),
|
||||
CancellationToken.None
|
||||
);
|
||||
Assert.False(missing.IsSuccess);
|
||||
|
||||
var ok = await new DeleteShowCommandHandler(db, GroupCleaner(db)).Handle(
|
||||
new DeleteShowCommand(show.Id),
|
||||
CancellationToken.None
|
||||
);
|
||||
Assert.True(ok.IsSuccess);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AddAndRemoveBumperTemplate_WorkThroughStorage()
|
||||
{
|
||||
var fixture = new TestDb();
|
||||
var channel = Channel.Create("c", "c", T0);
|
||||
await using (var seed = fixture.New())
|
||||
{
|
||||
seed.Channels.Add(channel);
|
||||
await seed.SaveChangesAsync(CancellationToken.None);
|
||||
}
|
||||
|
||||
Guid templateId;
|
||||
await using (var db = fixture.New())
|
||||
{
|
||||
var added = await new AddBumperTemplateCommandHandler(db).Handle(
|
||||
new AddBumperTemplateCommand(channel.Id, ""),
|
||||
CancellationToken.None
|
||||
);
|
||||
Assert.True(added.IsSuccess);
|
||||
templateId = added.Value;
|
||||
await db.SaveChangesAsync(CancellationToken.None);
|
||||
}
|
||||
|
||||
var storage = Substitute.For<IBumperTemplateStorage>();
|
||||
await using (var db = fixture.New())
|
||||
{
|
||||
var removed = await new RemoveBumperTemplateCommandHandler(db, storage).Handle(
|
||||
new RemoveBumperTemplateCommand(channel.Id, templateId),
|
||||
CancellationToken.None
|
||||
);
|
||||
Assert.True(removed.IsSuccess);
|
||||
}
|
||||
await storage.Received(1).DeleteTemplateAsync(templateId, Arg.Any<CancellationToken>());
|
||||
|
||||
// дефолтный блок удалить нельзя
|
||||
await using (var db = fixture.New())
|
||||
{
|
||||
var def = await db
|
||||
.Channels.Include(c => c.BumperTemplates)
|
||||
.FirstAsync(c => c.Id == channel.Id);
|
||||
var result = await new RemoveBumperTemplateCommandHandler(db, storage).Handle(
|
||||
new RemoveBumperTemplateCommand(channel.Id, def.BumperTemplates[0].Id),
|
||||
CancellationToken.None
|
||||
);
|
||||
Assert.Equal(ChannelErrors.CannotRemoveDefaultBumperTemplate, result.Error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,150 +1,150 @@
|
||||
using TeleWave.Application.Programming.Planning;
|
||||
using TeleWave.Application.Programming.Templates;
|
||||
using TeleWave.Domain.Programming;
|
||||
using Xunit;
|
||||
|
||||
namespace TeleWave.Application.Tests.Programming;
|
||||
|
||||
/// <summary>
|
||||
/// Применимость слоёв (см. 3.4) и разрешение перекрытий: какие слоты реально действуют в сутки.
|
||||
/// </summary>
|
||||
public class LayerApplicabilityTests
|
||||
{
|
||||
private static readonly TimeOnly DayStart = new(6, 0);
|
||||
|
||||
[Fact]
|
||||
public void Empty_CoversEveryDate()
|
||||
{
|
||||
var applicability = new LayerApplicability();
|
||||
|
||||
Assert.True(applicability.IsEmpty);
|
||||
Assert.True(applicability.Covers(new DateOnly(2026, 3, 17)));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(2026, 12, 25, true)] // внутри, до Нового года
|
||||
[InlineData(2026, 1, 3, true)] // внутри, после Нового года
|
||||
[InlineData(2026, 12, 20, true)] // ровно начало
|
||||
[InlineData(2026, 1, 8, true)] // ровно конец
|
||||
[InlineData(2026, 12, 19, false)] // за день до начала
|
||||
[InlineData(2026, 1, 9, false)] // на следующий день после конца
|
||||
[InlineData(2026, 6, 15, false)] // середина года
|
||||
public void AnnualRange_CrossingNewYear_IsInclusiveOnBothEnds(
|
||||
int year,
|
||||
int month,
|
||||
int day,
|
||||
bool expected
|
||||
)
|
||||
{
|
||||
// «20 декабря — 8 января» задаётся один раз и работает в любом году, поэтому сравнение идёт
|
||||
// по паре (месяц, день), а не по датам.
|
||||
var applicability = new LayerApplicability(AnnualRanges: [new AnnualRange(12, 20, 1, 8)]);
|
||||
|
||||
Assert.Equal(expected, applicability.Covers(new DateOnly(year, month, day)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AnnualRange_WithinOneYear_DoesNotWrap()
|
||||
{
|
||||
var applicability = new LayerApplicability(AnnualRanges: [new AnnualRange(6, 1, 8, 31)]);
|
||||
|
||||
Assert.True(applicability.Covers(new DateOnly(2026, 7, 4)));
|
||||
Assert.False(applicability.Covers(new DateOnly(2026, 1, 4)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Sections_AreCombinedWithOr()
|
||||
{
|
||||
// Понедельник ИЛИ конкретная дата: суббота из списка дат проходит, обычная суббота — нет.
|
||||
var applicability = new LayerApplicability(
|
||||
Weekdays: [1],
|
||||
SpecificDates: [new DateOnly(2026, 3, 21)]
|
||||
);
|
||||
|
||||
Assert.True(applicability.Covers(new DateOnly(2026, 3, 16))); // понедельник
|
||||
Assert.True(applicability.Covers(new DateOnly(2026, 3, 21))); // суббота из списка
|
||||
Assert.False(applicability.Covers(new DateOnly(2026, 3, 14))); // другая суббота
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Build_SkipsSlotsCoveredByHigherPriorityLayer()
|
||||
{
|
||||
// Слот младшего слоя пропускается целиком, а не обрезается: половина слота означала бы
|
||||
// половину настройки — своей группы и стратегии у половинки нет.
|
||||
var template = ScheduleTemplate.Create(Guid.NewGuid(), "Сетка");
|
||||
var top = template.AddLayer("Прайм", 100);
|
||||
var bottom = template.AddLayer("Обычный", 50);
|
||||
|
||||
AddSlot(top, "Кино", new TimeOnly(20, 0), 120);
|
||||
AddSlot(bottom, "Сериал", new TimeOnly(20, 30), 60);
|
||||
AddSlot(bottom, "Ночь", new TimeOnly(23, 0), 60);
|
||||
|
||||
var titles = Build(template).Select(s => s.Slot.Title).Distinct().ToList();
|
||||
|
||||
Assert.Contains("Кино", titles);
|
||||
Assert.Contains("Ночь", titles);
|
||||
Assert.DoesNotContain("Сериал", titles);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Build_DropsLayersThatDoNotApplyOnTheDate()
|
||||
{
|
||||
var template = ScheduleTemplate.Create(Guid.NewGuid(), "Сетка");
|
||||
var newYear = template.AddLayer("Новогодний", 100);
|
||||
newYear.Update(
|
||||
newYear.Name,
|
||||
newYear.Priority,
|
||||
new LayerApplicability(AnnualRanges: [new AnnualRange(12, 20, 1, 8)]).ToJson(),
|
||||
isEnabled: true
|
||||
);
|
||||
AddSlot(newYear, "Ирония судьбы", new TimeOnly(20, 0), 180);
|
||||
|
||||
var usual = template.AddLayer("Обычный", 50);
|
||||
AddSlot(usual, "Вечерний сериал", new TimeOnly(20, 0), 60);
|
||||
|
||||
var march = Build(template, new DateTimeOffset(2026, 3, 17, 12, 0, 0, TimeSpan.Zero))
|
||||
.Select(s => s.Slot.Title)
|
||||
.ToList();
|
||||
var december = Build(template, new DateTimeOffset(2026, 12, 25, 12, 0, 0, TimeSpan.Zero))
|
||||
.Select(s => s.Slot.Title)
|
||||
.ToList();
|
||||
|
||||
Assert.Contains("Вечерний сериал", march);
|
||||
Assert.DoesNotContain("Ирония судьбы", march);
|
||||
Assert.Contains("Ирония судьбы", december);
|
||||
Assert.DoesNotContain("Вечерний сериал", december);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Build_DisabledLayerIsIgnored()
|
||||
{
|
||||
var template = ScheduleTemplate.Create(Guid.NewGuid(), "Сетка");
|
||||
var layer = template.AddLayer("Выключённый", 100);
|
||||
AddSlot(layer, "Ничего", new TimeOnly(20, 0), 60);
|
||||
layer.Update(layer.Name, layer.Priority, null, isEnabled: false);
|
||||
|
||||
Assert.Empty(Build(template));
|
||||
}
|
||||
|
||||
private static void AddSlot(
|
||||
GridLayer layer,
|
||||
string title,
|
||||
TimeOnly start,
|
||||
int durationMinutes
|
||||
) => layer.AddSlot(title, start, durationMinutes);
|
||||
|
||||
private static IReadOnlyList<ScheduledSlot> Build(
|
||||
ScheduleTemplate template,
|
||||
DateTimeOffset? from = null
|
||||
)
|
||||
{
|
||||
var start = from ?? new DateTimeOffset(2026, 3, 17, 12, 0, 0, TimeSpan.Zero);
|
||||
return EffectiveGridBuilder.Build(
|
||||
template,
|
||||
utcOffsetMinutes: 180,
|
||||
DayStart,
|
||||
start,
|
||||
start.AddHours(18)
|
||||
);
|
||||
}
|
||||
}
|
||||
using TeleWave.Application.Programming.Planning;
|
||||
using TeleWave.Application.Programming.Templates;
|
||||
using TeleWave.Domain.Programming;
|
||||
using Xunit;
|
||||
|
||||
namespace TeleWave.Application.Tests.Programming;
|
||||
|
||||
/// <summary>
|
||||
/// Применимость слоёв (см. 3.4) и разрешение перекрытий: какие слоты реально действуют в сутки.
|
||||
/// </summary>
|
||||
public class LayerApplicabilityTests
|
||||
{
|
||||
private static readonly TimeOnly DayStart = new(6, 0);
|
||||
|
||||
[Fact]
|
||||
public void Empty_CoversEveryDate()
|
||||
{
|
||||
var applicability = new LayerApplicability();
|
||||
|
||||
Assert.True(applicability.IsEmpty);
|
||||
Assert.True(applicability.Covers(new DateOnly(2026, 3, 17)));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(2026, 12, 25, true)] // внутри, до Нового года
|
||||
[InlineData(2026, 1, 3, true)] // внутри, после Нового года
|
||||
[InlineData(2026, 12, 20, true)] // ровно начало
|
||||
[InlineData(2026, 1, 8, true)] // ровно конец
|
||||
[InlineData(2026, 12, 19, false)] // за день до начала
|
||||
[InlineData(2026, 1, 9, false)] // на следующий день после конца
|
||||
[InlineData(2026, 6, 15, false)] // середина года
|
||||
public void AnnualRange_CrossingNewYear_IsInclusiveOnBothEnds(
|
||||
int year,
|
||||
int month,
|
||||
int day,
|
||||
bool expected
|
||||
)
|
||||
{
|
||||
// «20 декабря — 8 января» задаётся один раз и работает в любом году, поэтому сравнение идёт
|
||||
// по паре (месяц, день), а не по датам.
|
||||
var applicability = new LayerApplicability(AnnualRanges: [new AnnualRange(12, 20, 1, 8)]);
|
||||
|
||||
Assert.Equal(expected, applicability.Covers(new DateOnly(year, month, day)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AnnualRange_WithinOneYear_DoesNotWrap()
|
||||
{
|
||||
var applicability = new LayerApplicability(AnnualRanges: [new AnnualRange(6, 1, 8, 31)]);
|
||||
|
||||
Assert.True(applicability.Covers(new DateOnly(2026, 7, 4)));
|
||||
Assert.False(applicability.Covers(new DateOnly(2026, 1, 4)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Sections_AreCombinedWithOr()
|
||||
{
|
||||
// Понедельник ИЛИ конкретная дата: суббота из списка дат проходит, обычная суббота — нет.
|
||||
var applicability = new LayerApplicability(
|
||||
Weekdays: [1],
|
||||
SpecificDates: [new DateOnly(2026, 3, 21)]
|
||||
);
|
||||
|
||||
Assert.True(applicability.Covers(new DateOnly(2026, 3, 16))); // понедельник
|
||||
Assert.True(applicability.Covers(new DateOnly(2026, 3, 21))); // суббота из списка
|
||||
Assert.False(applicability.Covers(new DateOnly(2026, 3, 14))); // другая суббота
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Build_SkipsSlotsCoveredByHigherPriorityLayer()
|
||||
{
|
||||
// Слот младшего слоя пропускается целиком, а не обрезается: половина слота означала бы
|
||||
// половину настройки — своей группы и стратегии у половинки нет.
|
||||
var template = ScheduleTemplate.Create(Guid.NewGuid(), "Сетка");
|
||||
var top = template.AddLayer("Прайм", 100);
|
||||
var bottom = template.AddLayer("Обычный", 50);
|
||||
|
||||
AddSlot(top, "Кино", new TimeOnly(20, 0), 120);
|
||||
AddSlot(bottom, "Сериал", new TimeOnly(20, 30), 60);
|
||||
AddSlot(bottom, "Ночь", new TimeOnly(23, 0), 60);
|
||||
|
||||
var titles = Build(template).Select(s => s.Slot.Title).Distinct().ToList();
|
||||
|
||||
Assert.Contains("Кино", titles);
|
||||
Assert.Contains("Ночь", titles);
|
||||
Assert.DoesNotContain("Сериал", titles);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Build_DropsLayersThatDoNotApplyOnTheDate()
|
||||
{
|
||||
var template = ScheduleTemplate.Create(Guid.NewGuid(), "Сетка");
|
||||
var newYear = template.AddLayer("Новогодний", 100);
|
||||
newYear.Update(
|
||||
newYear.Name,
|
||||
newYear.Priority,
|
||||
new LayerApplicability(AnnualRanges: [new AnnualRange(12, 20, 1, 8)]).ToJson(),
|
||||
isEnabled: true
|
||||
);
|
||||
AddSlot(newYear, "Ирония судьбы", new TimeOnly(20, 0), 180);
|
||||
|
||||
var usual = template.AddLayer("Обычный", 50);
|
||||
AddSlot(usual, "Вечерний сериал", new TimeOnly(20, 0), 60);
|
||||
|
||||
var march = Build(template, new DateTimeOffset(2026, 3, 17, 12, 0, 0, TimeSpan.Zero))
|
||||
.Select(s => s.Slot.Title)
|
||||
.ToList();
|
||||
var december = Build(template, new DateTimeOffset(2026, 12, 25, 12, 0, 0, TimeSpan.Zero))
|
||||
.Select(s => s.Slot.Title)
|
||||
.ToList();
|
||||
|
||||
Assert.Contains("Вечерний сериал", march);
|
||||
Assert.DoesNotContain("Ирония судьбы", march);
|
||||
Assert.Contains("Ирония судьбы", december);
|
||||
Assert.DoesNotContain("Вечерний сериал", december);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Build_DisabledLayerIsIgnored()
|
||||
{
|
||||
var template = ScheduleTemplate.Create(Guid.NewGuid(), "Сетка");
|
||||
var layer = template.AddLayer("Выключённый", 100);
|
||||
AddSlot(layer, "Ничего", new TimeOnly(20, 0), 60);
|
||||
layer.Update(layer.Name, layer.Priority, null, isEnabled: false);
|
||||
|
||||
Assert.Empty(Build(template));
|
||||
}
|
||||
|
||||
private static void AddSlot(
|
||||
GridLayer layer,
|
||||
string title,
|
||||
TimeOnly start,
|
||||
int durationMinutes
|
||||
) => layer.AddSlot(title, start, durationMinutes);
|
||||
|
||||
private static IReadOnlyList<ScheduledSlot> Build(
|
||||
ScheduleTemplate template,
|
||||
DateTimeOffset? from = null
|
||||
)
|
||||
{
|
||||
var start = from ?? new DateTimeOffset(2026, 3, 17, 12, 0, 0, TimeSpan.Zero);
|
||||
return EffectiveGridBuilder.Build(
|
||||
template,
|
||||
utcOffsetMinutes: 180,
|
||||
DayStart,
|
||||
start,
|
||||
start.AddHours(18)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,77 +1,77 @@
|
||||
using TeleWave.Application.Admin.Users.CreateUser;
|
||||
using TeleWave.Application.Admin.Users.ResetPassword;
|
||||
using TeleWave.Application.Broadcast;
|
||||
using TeleWave.Application.Broadcast.Bumpers;
|
||||
using TeleWave.Application.Broadcast.UpdateChannelSettings;
|
||||
using TeleWave.Domain.Broadcast;
|
||||
using Xunit;
|
||||
|
||||
namespace TeleWave.Application.Tests.Validators;
|
||||
|
||||
public class ValidatorTests
|
||||
{
|
||||
[Fact]
|
||||
public void UpdateBumperTextVariant_ChecksLengthsAndWeight()
|
||||
{
|
||||
var v = new UpdateBumperTextVariantCommandValidator();
|
||||
|
||||
var good = new UpdateBumperTextVariantCommand(
|
||||
Guid.NewGuid(),
|
||||
Guid.NewGuid(),
|
||||
Guid.NewGuid(),
|
||||
"Name",
|
||||
BumperTextKind.NowNext,
|
||||
"NOW",
|
||||
"NEXT",
|
||||
"l1",
|
||||
"l2",
|
||||
BumperTrigger.Both,
|
||||
3
|
||||
);
|
||||
Assert.True(v.Validate(good).IsValid);
|
||||
|
||||
Assert.False(v.Validate(good with { Name = "" }).IsValid);
|
||||
Assert.False(v.Validate(good with { Weight = -1 }).IsValid);
|
||||
Assert.False(v.Validate(good with { Line1 = new string('x', 121) }).IsValid);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UpdateChannelSettings_ChecksRanges()
|
||||
{
|
||||
var v = new UpdateChannelSettingsCommandValidator();
|
||||
var bumper = new BumperSettingsInput(BumperFont.Sans, BumperSelection.WeightedRandom);
|
||||
|
||||
var good = new UpdateChannelSettingsCommand(
|
||||
Guid.NewGuid(),
|
||||
"Name",
|
||||
true,
|
||||
true,
|
||||
bumper,
|
||||
null
|
||||
);
|
||||
Assert.True(v.Validate(good).IsValid);
|
||||
|
||||
Assert.False(v.Validate(good with { Name = "" }).IsValid);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ResetUserPassword_RequiresMinLength()
|
||||
{
|
||||
var v = new ResetUserPasswordCommandValidator();
|
||||
|
||||
Assert.True(v.Validate(new ResetUserPasswordCommand(Guid.NewGuid(), "Password1")).IsValid);
|
||||
Assert.False(v.Validate(new ResetUserPasswordCommand(Guid.NewGuid(), "short")).IsValid);
|
||||
Assert.False(v.Validate(new ResetUserPasswordCommand(Guid.Empty, "Password1")).IsValid);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateUser_ChecksNameAndPassword()
|
||||
{
|
||||
var v = new CreateUserCommandValidator();
|
||||
|
||||
Assert.True(v.Validate(new CreateUserCommand("bob", "Password1", Guid.NewGuid())).IsValid);
|
||||
Assert.False(v.Validate(new CreateUserCommand("ab", "Password1", Guid.NewGuid())).IsValid);
|
||||
Assert.False(v.Validate(new CreateUserCommand("bob", "short", Guid.NewGuid())).IsValid);
|
||||
Assert.False(v.Validate(new CreateUserCommand("bob", "Password1", Guid.Empty)).IsValid);
|
||||
}
|
||||
}
|
||||
using TeleWave.Application.Admin.Users.CreateUser;
|
||||
using TeleWave.Application.Admin.Users.ResetPassword;
|
||||
using TeleWave.Application.Broadcast;
|
||||
using TeleWave.Application.Broadcast.Bumpers;
|
||||
using TeleWave.Application.Broadcast.UpdateChannelSettings;
|
||||
using TeleWave.Domain.Broadcast;
|
||||
using Xunit;
|
||||
|
||||
namespace TeleWave.Application.Tests.Validators;
|
||||
|
||||
public class ValidatorTests
|
||||
{
|
||||
[Fact]
|
||||
public void UpdateBumperTextVariant_ChecksLengthsAndWeight()
|
||||
{
|
||||
var v = new UpdateBumperTextVariantCommandValidator();
|
||||
|
||||
var good = new UpdateBumperTextVariantCommand(
|
||||
Guid.NewGuid(),
|
||||
Guid.NewGuid(),
|
||||
Guid.NewGuid(),
|
||||
"Name",
|
||||
BumperTextKind.NowNext,
|
||||
"NOW",
|
||||
"NEXT",
|
||||
"l1",
|
||||
"l2",
|
||||
BumperTrigger.Both,
|
||||
3
|
||||
);
|
||||
Assert.True(v.Validate(good).IsValid);
|
||||
|
||||
Assert.False(v.Validate(good with { Name = "" }).IsValid);
|
||||
Assert.False(v.Validate(good with { Weight = -1 }).IsValid);
|
||||
Assert.False(v.Validate(good with { Line1 = new string('x', 121) }).IsValid);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UpdateChannelSettings_ChecksRanges()
|
||||
{
|
||||
var v = new UpdateChannelSettingsCommandValidator();
|
||||
var bumper = new BumperSettingsInput(BumperFont.Sans, BumperSelection.WeightedRandom);
|
||||
|
||||
var good = new UpdateChannelSettingsCommand(
|
||||
Guid.NewGuid(),
|
||||
"Name",
|
||||
true,
|
||||
true,
|
||||
bumper,
|
||||
null
|
||||
);
|
||||
Assert.True(v.Validate(good).IsValid);
|
||||
|
||||
Assert.False(v.Validate(good with { Name = "" }).IsValid);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ResetUserPassword_RequiresMinLength()
|
||||
{
|
||||
var v = new ResetUserPasswordCommandValidator();
|
||||
|
||||
Assert.True(v.Validate(new ResetUserPasswordCommand(Guid.NewGuid(), "Password1")).IsValid);
|
||||
Assert.False(v.Validate(new ResetUserPasswordCommand(Guid.NewGuid(), "short")).IsValid);
|
||||
Assert.False(v.Validate(new ResetUserPasswordCommand(Guid.Empty, "Password1")).IsValid);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateUser_ChecksNameAndPassword()
|
||||
{
|
||||
var v = new CreateUserCommandValidator();
|
||||
|
||||
Assert.True(v.Validate(new CreateUserCommand("bob", "Password1", Guid.NewGuid())).IsValid);
|
||||
Assert.False(v.Validate(new CreateUserCommand("ab", "Password1", Guid.NewGuid())).IsValid);
|
||||
Assert.False(v.Validate(new CreateUserCommand("bob", "short", Guid.NewGuid())).IsValid);
|
||||
Assert.False(v.Validate(new CreateUserCommand("bob", "Password1", Guid.Empty)).IsValid);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user