Add project reference for TeleWave.Infrastructure: include TeleWave.Infrastructure.csproj in TeleWave.Application.Tests project to support additional testing capabilities.
This commit is contained in:
@@ -0,0 +1,157 @@
|
||||
using NSubstitute;
|
||||
using TeleWave.Application.Admin.Roles;
|
||||
using TeleWave.Application.Admin.Roles.CreateRole;
|
||||
using TeleWave.Application.Admin.Roles.DeleteRole;
|
||||
using TeleWave.Application.Admin.Roles.ListRoles;
|
||||
using TeleWave.Application.Admin.Roles.UpdateRole;
|
||||
using TeleWave.Application.Admin.Users;
|
||||
using TeleWave.Application.Admin.Users.DeleteUser;
|
||||
using TeleWave.Application.Admin.Users.ResetPassword;
|
||||
using TeleWave.Application.Admin.Users.UnblockUser;
|
||||
using TeleWave.Application.Auth;
|
||||
using TeleWave.Application.Auth.ChangePassword;
|
||||
using TeleWave.Application.Common.Interfaces;
|
||||
using TeleWave.Application.Common.Models;
|
||||
using Xunit;
|
||||
|
||||
namespace TeleWave.Application.Tests.Admin;
|
||||
|
||||
public class DelegatingHandlersTests
|
||||
{
|
||||
private readonly IRoleService _roles = Substitute.For<IRoleService>();
|
||||
private readonly IIdentityService _identity = Substitute.For<IIdentityService>();
|
||||
private readonly ICurrentUser _currentUser = Substitute.For<ICurrentUser>();
|
||||
|
||||
[Fact]
|
||||
public async Task Roles_HandlersDelegateToService()
|
||||
{
|
||||
var id = Guid.NewGuid();
|
||||
var dto = new RoleDto(id, "editor", false);
|
||||
_roles.CreateRoleAsync("editor", Arg.Any<CancellationToken>()).Returns(Result.Success(dto));
|
||||
_roles
|
||||
.UpdateRoleAsync(id, "editor", Arg.Any<CancellationToken>())
|
||||
.Returns(Result.Success(dto));
|
||||
_roles.DeleteRoleAsync(id, Arg.Any<CancellationToken>()).Returns(Result.Success());
|
||||
_roles.ListRolesAsync(Arg.Any<CancellationToken>()).Returns(new List<RoleDto> { dto });
|
||||
|
||||
Assert.True(
|
||||
(
|
||||
await new CreateRoleCommandHandler(_roles).Handle(
|
||||
new CreateRoleCommand("editor"),
|
||||
default
|
||||
)
|
||||
).IsSuccess
|
||||
);
|
||||
Assert.True(
|
||||
(
|
||||
await new UpdateRoleCommandHandler(_roles).Handle(
|
||||
new UpdateRoleCommand(id, "editor"),
|
||||
default
|
||||
)
|
||||
).IsSuccess
|
||||
);
|
||||
Assert.True(
|
||||
(
|
||||
await new DeleteRoleCommandHandler(_roles).Handle(
|
||||
new DeleteRoleCommand(id),
|
||||
default
|
||||
)
|
||||
).IsSuccess
|
||||
);
|
||||
Assert.Single(
|
||||
await new ListRolesQueryHandler(_roles).Handle(new ListRolesQuery(), default)
|
||||
);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UnblockUser_And_ResetPassword_Delegate()
|
||||
{
|
||||
var id = Guid.NewGuid();
|
||||
_identity.UnblockUserAsync(id, Arg.Any<CancellationToken>()).Returns(Result.Success());
|
||||
_identity
|
||||
.ResetPasswordAsync(id, "Password1", Arg.Any<CancellationToken>())
|
||||
.Returns(Result.Success());
|
||||
|
||||
Assert.True(
|
||||
(
|
||||
await new UnblockUserCommandHandler(_identity).Handle(
|
||||
new UnblockUserCommand(id),
|
||||
default
|
||||
)
|
||||
).IsSuccess
|
||||
);
|
||||
Assert.True(
|
||||
(
|
||||
await new ResetUserPasswordCommandHandler(_identity).Handle(
|
||||
new ResetUserPasswordCommand(id, "Password1"),
|
||||
default
|
||||
)
|
||||
).IsSuccess
|
||||
);
|
||||
await _identity
|
||||
.Received(1)
|
||||
.ResetPasswordAsync(id, "Password1", Arg.Any<CancellationToken>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DeleteUser_CannotDeleteSelf()
|
||||
{
|
||||
var me = Guid.NewGuid();
|
||||
_currentUser.UserId.Returns(me);
|
||||
|
||||
var result = await new DeleteUserCommandHandler(_identity, _currentUser).Handle(
|
||||
new DeleteUserCommand(me),
|
||||
default
|
||||
);
|
||||
|
||||
Assert.Equal(UserErrors.CannotDeleteSelf, result.Error);
|
||||
await _identity
|
||||
.DidNotReceive()
|
||||
.DeleteUserAsync(Arg.Any<Guid>(), Arg.Any<CancellationToken>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DeleteUser_OtherUser_Delegates()
|
||||
{
|
||||
_currentUser.UserId.Returns(Guid.NewGuid());
|
||||
var target = Guid.NewGuid();
|
||||
_identity.DeleteUserAsync(target, Arg.Any<CancellationToken>()).Returns(Result.Success());
|
||||
|
||||
var result = await new DeleteUserCommandHandler(_identity, _currentUser).Handle(
|
||||
new DeleteUserCommand(target),
|
||||
default
|
||||
);
|
||||
|
||||
Assert.True(result.IsSuccess);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ChangePassword_Unauthorized_WhenNoCurrentUser()
|
||||
{
|
||||
_currentUser.UserId.Returns((Guid?)null);
|
||||
|
||||
var result = await new ChangePasswordCommandHandler(_identity, _currentUser).Handle(
|
||||
new ChangePasswordCommand("old", "New12345"),
|
||||
default
|
||||
);
|
||||
|
||||
Assert.Equal(AuthErrors.Unauthorized, result.Error);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ChangePassword_Delegates_WhenAuthenticated()
|
||||
{
|
||||
var me = Guid.NewGuid();
|
||||
_currentUser.UserId.Returns(me);
|
||||
_identity
|
||||
.ChangePasswordAsync(me, "old", "New12345", Arg.Any<CancellationToken>())
|
||||
.Returns(Result.Success());
|
||||
|
||||
var result = await new ChangePasswordCommandHandler(_identity, _currentUser).Handle(
|
||||
new ChangePasswordCommand("old", "New12345"),
|
||||
default
|
||||
);
|
||||
|
||||
Assert.True(result.IsSuccess);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using TeleWave.Application.Broadcast;
|
||||
using TeleWave.Application.Broadcast.Bumpers;
|
||||
using TeleWave.Application.Tests.Support;
|
||||
using TeleWave.Domain.Broadcast;
|
||||
using Xunit;
|
||||
|
||||
namespace TeleWave.Application.Tests.Broadcast;
|
||||
|
||||
public class BumperVariantHandlersTests
|
||||
{
|
||||
private static readonly DateTimeOffset T0 = new(2026, 1, 1, 0, 0, 0, TimeSpan.Zero);
|
||||
|
||||
[Fact]
|
||||
public async Task AddVariant_AutoNames_WhenBlank()
|
||||
{
|
||||
var fixture = new TestDb();
|
||||
var channel = Channel.Create("c", "c", T0);
|
||||
var template = channel.BumperTemplates[0];
|
||||
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 AddBumperTextVariantCommandHandler(db).Handle(
|
||||
new AddBumperTextVariantCommand(channel.Id, template.Id, " "),
|
||||
CancellationToken.None
|
||||
);
|
||||
Assert.True(result.IsSuccess);
|
||||
await db.SaveChangesAsync(CancellationToken.None);
|
||||
|
||||
await using var verify = fixture.New();
|
||||
var stored = await verify
|
||||
.Channels.Include(c => c.BumperTemplates)
|
||||
.ThenInclude(t => t.Variants)
|
||||
.FirstAsync(c => c.Id == channel.Id);
|
||||
var variants = stored.BumperTemplates[0].Variants;
|
||||
Assert.Equal(2, variants.Count);
|
||||
Assert.Contains(variants, v => v.Name == "Текст 2");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AddVariant_UnknownTemplate_ReturnsNotFound()
|
||||
{
|
||||
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 AddBumperTextVariantCommandHandler(db).Handle(
|
||||
new AddBumperTextVariantCommand(channel.Id, Guid.NewGuid(), "x"),
|
||||
CancellationToken.None
|
||||
);
|
||||
Assert.Equal(ChannelErrors.BumperTemplateNotFound, result.Error);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UpdateVariant_ChangesFields()
|
||||
{
|
||||
var fixture = new TestDb();
|
||||
var channel = Channel.Create("c", "c", T0);
|
||||
var template = channel.BumperTemplates[0];
|
||||
var variant = template.Variants[0];
|
||||
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 UpdateBumperTextVariantCommandHandler(db).Handle(
|
||||
new UpdateBumperTextVariantCommand(
|
||||
channel.Id,
|
||||
template.Id,
|
||||
variant.Id,
|
||||
"Custom",
|
||||
BumperTextKind.Free,
|
||||
"NOW",
|
||||
"NEXT",
|
||||
"line1",
|
||||
"line2",
|
||||
BumperTrigger.Both,
|
||||
7
|
||||
),
|
||||
CancellationToken.None
|
||||
);
|
||||
Assert.True(result.IsSuccess);
|
||||
await db.SaveChangesAsync(CancellationToken.None);
|
||||
|
||||
await using var verify = fixture.New();
|
||||
var stored = await verify
|
||||
.Channels.Include(c => c.BumperTemplates)
|
||||
.ThenInclude(t => t.Variants)
|
||||
.FirstAsync(c => c.Id == channel.Id);
|
||||
var v = stored.BumperTemplates[0].Variants[0];
|
||||
Assert.Equal("Custom", v.Name);
|
||||
Assert.Equal(BumperTextKind.Free, v.Kind);
|
||||
Assert.Equal(7, v.Weight);
|
||||
Assert.Equal(BumperTrigger.Both, v.Trigger);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RemoveVariant_CannotRemoveLast_ButRemovesExtra()
|
||||
{
|
||||
var fixture = new TestDb();
|
||||
var channel = Channel.Create("c", "c", T0);
|
||||
var template = channel.BumperTemplates[0];
|
||||
var only = template.Variants[0];
|
||||
var extra = template.AddVariant("Text 2");
|
||||
await using (var seed = fixture.New())
|
||||
{
|
||||
seed.Channels.Add(channel);
|
||||
await seed.SaveChangesAsync(CancellationToken.None);
|
||||
}
|
||||
|
||||
await using var db = fixture.New();
|
||||
var handler = new RemoveBumperTextVariantCommandHandler(db);
|
||||
|
||||
var okRemove = await handler.Handle(
|
||||
new RemoveBumperTextVariantCommand(channel.Id, template.Id, extra.Id),
|
||||
CancellationToken.None
|
||||
);
|
||||
Assert.True(okRemove.IsSuccess);
|
||||
await db.SaveChangesAsync(CancellationToken.None);
|
||||
|
||||
await using var db2 = fixture.New();
|
||||
var lastGuard = await new RemoveBumperTextVariantCommandHandler(db2).Handle(
|
||||
new RemoveBumperTextVariantCommand(channel.Id, template.Id, only.Id),
|
||||
CancellationToken.None
|
||||
);
|
||||
Assert.Equal(ChannelErrors.CannotRemoveLastBumperTextVariant, lastGuard.Error);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,324 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using TeleWave.Application.Broadcast;
|
||||
using TeleWave.Application.Broadcast.AddChannelAd;
|
||||
using TeleWave.Application.Broadcast.AddChannelShow;
|
||||
using TeleWave.Application.Broadcast.CreateChannel;
|
||||
using TeleWave.Application.Broadcast.CreateOverride;
|
||||
using TeleWave.Application.Broadcast.DeleteOverride;
|
||||
using TeleWave.Application.Broadcast.RemoveChannelShow;
|
||||
using TeleWave.Application.Broadcast.UpdateChannelSettings;
|
||||
using TeleWave.Application.Broadcast.UpdateChannelShow;
|
||||
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 AddChannelShow_HappyPath_AndErrors()
|
||||
{
|
||||
var fixture = new TestDb();
|
||||
var channel = Channel.Create("c", "c", T0);
|
||||
var show = Show.Create("S", ShowKind.Series);
|
||||
await using (var seed = fixture.New())
|
||||
{
|
||||
seed.Channels.Add(channel);
|
||||
seed.Shows.Add(show);
|
||||
await seed.SaveChangesAsync(CancellationToken.None);
|
||||
}
|
||||
|
||||
await using var db = fixture.New();
|
||||
var handler = new AddChannelShowCommandHandler(db);
|
||||
|
||||
// неизвестный канал
|
||||
var noChannel = await handler.Handle(
|
||||
new AddChannelShowCommand(Guid.NewGuid(), show.Id, 1, BlockMode.Count, 1),
|
||||
CancellationToken.None
|
||||
);
|
||||
Assert.Equal(ChannelErrors.NotFound, noChannel.Error);
|
||||
|
||||
// неизвестное шоу
|
||||
var noShow = await handler.Handle(
|
||||
new AddChannelShowCommand(channel.Id, Guid.NewGuid(), 1, BlockMode.Count, 1),
|
||||
CancellationToken.None
|
||||
);
|
||||
Assert.Equal(ChannelErrors.ShowNotFound, noShow.Error);
|
||||
|
||||
// ок
|
||||
var ok = await handler.Handle(
|
||||
new AddChannelShowCommand(channel.Id, show.Id, 2, BlockMode.Count, 3),
|
||||
CancellationToken.None
|
||||
);
|
||||
Assert.True(ok.IsSuccess);
|
||||
await db.SaveChangesAsync(CancellationToken.None);
|
||||
|
||||
// повторно — уже добавлено
|
||||
await using var db2 = fixture.New();
|
||||
var dup = await new AddChannelShowCommandHandler(db2).Handle(
|
||||
new AddChannelShowCommand(channel.Id, show.Id, 1, BlockMode.Count, 1),
|
||||
CancellationToken.None
|
||||
);
|
||||
Assert.Equal(ChannelErrors.ShowAlreadyAdded, dup.Error);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RemoveChannelShow_RemovesOrReportsNotFound()
|
||||
{
|
||||
var fixture = new TestDb();
|
||||
var channel = Channel.Create("c", "c", T0);
|
||||
var cs = channel.AddShow(Guid.NewGuid(), 1, BlockMode.Count, 1);
|
||||
await using (var seed = fixture.New())
|
||||
{
|
||||
seed.Channels.Add(channel);
|
||||
await seed.SaveChangesAsync(CancellationToken.None);
|
||||
}
|
||||
|
||||
await using var db = fixture.New();
|
||||
var handler = new RemoveChannelShowCommandHandler(db);
|
||||
|
||||
var missing = await handler.Handle(
|
||||
new RemoveChannelShowCommand(channel.Id, Guid.NewGuid()),
|
||||
CancellationToken.None
|
||||
);
|
||||
Assert.Equal(ChannelErrors.ChannelShowNotFound, missing.Error);
|
||||
|
||||
var ok = await handler.Handle(
|
||||
new RemoveChannelShowCommand(channel.Id, cs.Id),
|
||||
CancellationToken.None
|
||||
);
|
||||
Assert.True(ok.IsSuccess);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AddChannelAd_HappyPath_AndAssetNotFound()
|
||||
{
|
||||
var fixture = new TestDb();
|
||||
var channel = Channel.Create("c", "c", T0);
|
||||
var asset = MediaAsset.Register("clip.mp4", ".mp4", MediaSource.Upload);
|
||||
await using (var seed = fixture.New())
|
||||
{
|
||||
seed.Channels.Add(channel);
|
||||
seed.MediaAssets.Add(asset);
|
||||
await seed.SaveChangesAsync(CancellationToken.None);
|
||||
}
|
||||
|
||||
await using var db = fixture.New();
|
||||
var handler = new AddChannelAdCommandHandler(db);
|
||||
|
||||
var noAsset = await handler.Handle(
|
||||
new AddChannelAdCommand(channel.Id, Guid.NewGuid()),
|
||||
CancellationToken.None
|
||||
);
|
||||
Assert.Equal(ChannelErrors.AssetNotFound, noAsset.Error);
|
||||
|
||||
var ok = await handler.Handle(
|
||||
new AddChannelAdCommand(channel.Id, asset.Id),
|
||||
CancellationToken.None
|
||||
);
|
||||
Assert.True(ok.IsSuccess);
|
||||
}
|
||||
|
||||
[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,
|
||||
AdInsertion.BetweenBlocks,
|
||||
1,
|
||||
true,
|
||||
new BumperSettingsInput(
|
||||
BumperFont.Sans,
|
||||
5,
|
||||
BumperSelection.WeightedRandom,
|
||||
0.5,
|
||||
0.2
|
||||
),
|
||||
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(0.5, stored!.BumperShowChangeChance);
|
||||
Assert.Equal(0.2, stored.BumperEpisodeChangeChance);
|
||||
Assert.Equal(BumperSelection.WeightedRandom, stored.BumperSelection);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UpdateChannelShow_SetsPreferredHours()
|
||||
{
|
||||
var fixture = new TestDb();
|
||||
var channel = Channel.Create("c", "c", T0);
|
||||
var cs = channel.AddShow(Guid.NewGuid(), 1, BlockMode.Count, 1);
|
||||
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 UpdateChannelShowCommandHandler(db).Handle(
|
||||
new UpdateChannelShowCommand(
|
||||
channel.Id,
|
||||
cs.Id,
|
||||
3,
|
||||
BlockMode.Count,
|
||||
2,
|
||||
true,
|
||||
4,
|
||||
[new HourWindowInput(18, 23)]
|
||||
),
|
||||
CancellationToken.None
|
||||
);
|
||||
Assert.True(result.IsSuccess);
|
||||
await db.SaveChangesAsync(CancellationToken.None);
|
||||
|
||||
await using var verify = fixture.New();
|
||||
var stored = await verify
|
||||
.Channels.Include(c => c.Shows)
|
||||
.ThenInclude(s => s.PreferredHours)
|
||||
.FirstAsync(c => c.Id == channel.Id);
|
||||
var show = Assert.Single(stored.Shows);
|
||||
Assert.Equal(4, show.PreferredWeightMultiplier);
|
||||
Assert.Single(show.PreferredHours);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CreateOverride_OneTime_Weekly_AndValidation()
|
||||
{
|
||||
var fixture = new TestDb();
|
||||
var channel = Channel.Create("c", "c", T0);
|
||||
var show = Show.Create("S", ShowKind.Series);
|
||||
await using (var seed = fixture.New())
|
||||
{
|
||||
seed.Channels.Add(channel);
|
||||
seed.Shows.Add(show);
|
||||
await seed.SaveChangesAsync(CancellationToken.None);
|
||||
}
|
||||
|
||||
await using var db = fixture.New();
|
||||
var handler = new CreateProgrammingOverrideCommandHandler(db);
|
||||
OverrideShowInput[] shows = [new OverrideShowInput(show.Id, 1)];
|
||||
|
||||
// Разовое окно — ок.
|
||||
var oneTime = await handler.Handle(
|
||||
new CreateProgrammingOverrideCommand(
|
||||
channel.Id,
|
||||
OverrideMode.Exclusive,
|
||||
OverrideRecurrence.OneTime,
|
||||
T0,
|
||||
T0.AddHours(2),
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
shows
|
||||
),
|
||||
CancellationToken.None
|
||||
);
|
||||
Assert.True(oneTime.IsSuccess);
|
||||
|
||||
// Еженедельное — ок.
|
||||
var weekly = await handler.Handle(
|
||||
new CreateProgrammingOverrideCommand(
|
||||
channel.Id,
|
||||
OverrideMode.Boost,
|
||||
OverrideRecurrence.Weekly,
|
||||
null,
|
||||
null,
|
||||
6,
|
||||
600,
|
||||
720,
|
||||
shows
|
||||
),
|
||||
CancellationToken.None
|
||||
);
|
||||
Assert.True(weekly.IsSuccess);
|
||||
|
||||
// Невалидное окно (end <= start).
|
||||
var bad = await handler.Handle(
|
||||
new CreateProgrammingOverrideCommand(
|
||||
channel.Id,
|
||||
OverrideMode.Exclusive,
|
||||
OverrideRecurrence.OneTime,
|
||||
T0.AddHours(2),
|
||||
T0,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
shows
|
||||
),
|
||||
CancellationToken.None
|
||||
);
|
||||
Assert.Equal(ChannelErrors.InvalidOverrideWindow, bad.Error);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DeleteOverride_RemovesOrReportsNotFound()
|
||||
{
|
||||
var fixture = new TestDb();
|
||||
var channel = Channel.Create("c", "c", T0);
|
||||
var ovr = channel.AddOverride(OverrideMode.Exclusive, T0, T0.AddHours(1));
|
||||
await using (var seed = fixture.New())
|
||||
{
|
||||
seed.Channels.Add(channel);
|
||||
await seed.SaveChangesAsync(CancellationToken.None);
|
||||
}
|
||||
|
||||
await using var db = fixture.New();
|
||||
var handler = new DeleteProgrammingOverrideCommandHandler(db);
|
||||
|
||||
var missing = await handler.Handle(
|
||||
new DeleteProgrammingOverrideCommand(channel.Id, Guid.NewGuid()),
|
||||
CancellationToken.None
|
||||
);
|
||||
Assert.Equal(ChannelErrors.OverrideNotFound, missing.Error);
|
||||
|
||||
var ok = await handler.Handle(
|
||||
new DeleteProgrammingOverrideCommand(channel.Id, ovr.Id),
|
||||
CancellationToken.None
|
||||
);
|
||||
Assert.True(ok.IsSuccess);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,286 @@
|
||||
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.Broadcast.RemoveChannelAd;
|
||||
using TeleWave.Application.Common.Interfaces;
|
||||
using TeleWave.Application.Library.DeleteShow;
|
||||
using TeleWave.Application.Library.GetShow;
|
||||
using TeleWave.Application.Media.GetMedia;
|
||||
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);
|
||||
|
||||
[Fact]
|
||||
public async Task GetChannel_ReturnsDto_WithBumperAndOverride()
|
||||
{
|
||||
var fixture = new TestDb();
|
||||
var channel = Channel.Create("News", "news", T0);
|
||||
var show = Show.Create("S", ShowKind.Series);
|
||||
channel.AddShow(show.Id, 1, BlockMode.Count, 1);
|
||||
channel.AddWeeklyOverride(OverrideMode.Exclusive, 6, 600, 720).AddShow(show.Id, 1);
|
||||
await using (var seed = fixture.New())
|
||||
{
|
||||
seed.Shows.Add(show);
|
||||
seed.Channels.Add(channel);
|
||||
await seed.SaveChangesAsync(CancellationToken.None);
|
||||
}
|
||||
|
||||
await using var db = fixture.New();
|
||||
var result = await new GetChannelQueryHandler(db).Handle(
|
||||
new GetChannelQuery(channel.Id),
|
||||
CancellationToken.None
|
||||
);
|
||||
|
||||
Assert.True(result.IsSuccess);
|
||||
Assert.Equal("News", result.Value.Name);
|
||||
Assert.Single(result.Value.BumperTemplates);
|
||||
Assert.Single(result.Value.Overrides);
|
||||
Assert.Equal(OverrideRecurrence.Weekly, result.Value.Overrides[0].Recurrence);
|
||||
}
|
||||
|
||||
[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 GetMedia_ReturnsDtoOrNotFound()
|
||||
{
|
||||
var fixture = new TestDb();
|
||||
var asset = MediaAsset.Register("clip.mp4", ".mp4", MediaSource.Upload);
|
||||
await using (var seed = fixture.New())
|
||||
{
|
||||
seed.MediaAssets.Add(asset);
|
||||
await seed.SaveChangesAsync(CancellationToken.None);
|
||||
}
|
||||
|
||||
await using var db = fixture.New();
|
||||
var ok = await new GetMediaAssetQueryHandler(db).Handle(
|
||||
new GetMediaAssetQuery(asset.Id),
|
||||
CancellationToken.None
|
||||
);
|
||||
Assert.True(ok.IsSuccess);
|
||||
|
||||
var missing = await new GetMediaAssetQueryHandler(db).Handle(
|
||||
new GetMediaAssetQuery(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).Handle(
|
||||
new DeleteShowCommand(Guid.NewGuid()),
|
||||
CancellationToken.None
|
||||
);
|
||||
Assert.False(missing.IsSuccess);
|
||||
|
||||
var ok = await new DeleteShowCommandHandler(db).Handle(
|
||||
new DeleteShowCommand(show.Id),
|
||||
CancellationToken.None
|
||||
);
|
||||
Assert.True(ok.IsSuccess);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RemoveChannelAd_RemovesOrNotFound()
|
||||
{
|
||||
var fixture = new TestDb();
|
||||
var channel = Channel.Create("c", "c", T0);
|
||||
var ad = channel.AddAd(Guid.NewGuid());
|
||||
await using (var seed = fixture.New())
|
||||
{
|
||||
seed.Channels.Add(channel);
|
||||
await seed.SaveChangesAsync(CancellationToken.None);
|
||||
}
|
||||
|
||||
await using var db = fixture.New();
|
||||
var handler = new RemoveChannelAdCommandHandler(db);
|
||||
|
||||
Assert.Equal(
|
||||
ChannelErrors.AdNotFound,
|
||||
(
|
||||
await handler.Handle(
|
||||
new RemoveChannelAdCommand(channel.Id, Guid.NewGuid()),
|
||||
CancellationToken.None
|
||||
)
|
||||
).Error
|
||||
);
|
||||
Assert.True(
|
||||
(
|
||||
await handler.Handle(
|
||||
new RemoveChannelAdCommand(channel.Id, ad.Id),
|
||||
CancellationToken.None
|
||||
)
|
||||
).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);
|
||||
}
|
||||
storage.Received(1).DeleteTemplate(templateId);
|
||||
|
||||
// дефолтный блок удалить нельзя
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using TeleWave.Application.Library;
|
||||
using TeleWave.Application.Library.AddEpisode;
|
||||
using TeleWave.Application.Library.CreateShow;
|
||||
using TeleWave.Application.Library.RemoveEpisode;
|
||||
using TeleWave.Application.Library.RenameShow;
|
||||
using TeleWave.Application.Library.SetShowOriginalName;
|
||||
using TeleWave.Application.Tests.Support;
|
||||
using TeleWave.Domain.Library;
|
||||
using TeleWave.Domain.Media;
|
||||
using Xunit;
|
||||
|
||||
namespace TeleWave.Application.Tests.Library;
|
||||
|
||||
public class ShowHandlersTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task CreateShow_PersistsShow()
|
||||
{
|
||||
var fixture = new TestDb();
|
||||
await using var db = fixture.New();
|
||||
|
||||
var result = await new CreateShowCommandHandler(db).Handle(
|
||||
new CreateShowCommand("Симпсоны", ShowKind.Series, "desc", "The Simpsons"),
|
||||
CancellationToken.None
|
||||
);
|
||||
Assert.True(result.IsSuccess);
|
||||
await db.SaveChangesAsync(CancellationToken.None);
|
||||
|
||||
await using var verify = fixture.New();
|
||||
var stored = await verify.Shows.FindAsync(result.Value);
|
||||
Assert.Equal("Симпсоны", stored!.Name);
|
||||
Assert.Equal("The Simpsons", stored.OriginalName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RenameShow_ChangesName_OrNotFound()
|
||||
{
|
||||
var fixture = new TestDb();
|
||||
var show = Show.Create("Old", 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 RenameShowCommandHandler(db).Handle(
|
||||
new RenameShowCommand(Guid.NewGuid(), "New"),
|
||||
CancellationToken.None
|
||||
);
|
||||
Assert.Equal(ShowErrors.NotFound, missing.Error);
|
||||
|
||||
var ok = await new RenameShowCommandHandler(db).Handle(
|
||||
new RenameShowCommand(show.Id, " New "),
|
||||
CancellationToken.None
|
||||
);
|
||||
Assert.True(ok.IsSuccess);
|
||||
await db.SaveChangesAsync(CancellationToken.None);
|
||||
|
||||
await using var verify = fixture.New();
|
||||
Assert.Equal("New", (await verify.Shows.FindAsync(show.Id))!.Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SetOriginalName_TrimsAndClears()
|
||||
{
|
||||
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 SetShowOriginalNameCommandHandler(db).Handle(
|
||||
new SetShowOriginalNameCommand(show.Id, " Orig "),
|
||||
CancellationToken.None
|
||||
);
|
||||
Assert.True(ok.IsSuccess);
|
||||
await db.SaveChangesAsync(CancellationToken.None);
|
||||
|
||||
await using var verify = fixture.New();
|
||||
Assert.Equal("Orig", (await verify.Shows.FindAsync(show.Id))!.OriginalName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AddEpisode_ParsesNumbers_AndGuardsSingle()
|
||||
{
|
||||
var fixture = new TestDb();
|
||||
var series = Show.Create("Series", ShowKind.Series);
|
||||
var single = Show.Create("Movie", ShowKind.Single);
|
||||
single.AddEpisode(Guid.NewGuid()); // уже занята единственная серия
|
||||
var asset = MediaAsset.Register("The.Show.S03E07.WEBDL.mkv", ".mkv", MediaSource.Upload);
|
||||
await using (var seed = fixture.New())
|
||||
{
|
||||
seed.Shows.Add(series);
|
||||
seed.Shows.Add(single);
|
||||
seed.MediaAssets.Add(asset);
|
||||
await seed.SaveChangesAsync(CancellationToken.None);
|
||||
}
|
||||
|
||||
await using var db = fixture.New();
|
||||
var handler = new AddEpisodeCommandHandler(db);
|
||||
|
||||
// неизвестный ассет
|
||||
var noAsset = await handler.Handle(
|
||||
new AddEpisodeCommand(series.Id, Guid.NewGuid()),
|
||||
CancellationToken.None
|
||||
);
|
||||
Assert.Equal(ShowErrors.AssetNotFound, noAsset.Error);
|
||||
|
||||
// single уже имеет серию
|
||||
var singleGuard = await handler.Handle(
|
||||
new AddEpisodeCommand(single.Id, asset.Id),
|
||||
CancellationToken.None
|
||||
);
|
||||
Assert.Equal(ShowErrors.SingleAlreadyHasEpisode, singleGuard.Error);
|
||||
|
||||
// ок + распознавание S03E07
|
||||
var ok = await handler.Handle(
|
||||
new AddEpisodeCommand(series.Id, asset.Id),
|
||||
CancellationToken.None
|
||||
);
|
||||
Assert.True(ok.IsSuccess);
|
||||
await db.SaveChangesAsync(CancellationToken.None);
|
||||
|
||||
await using var verify = fixture.New();
|
||||
var stored = await verify.Shows.Include(s => s.Episodes).FirstAsync(s => s.Id == series.Id);
|
||||
var episode = Assert.Single(stored.Episodes);
|
||||
Assert.Equal(3, episode.Season);
|
||||
Assert.Equal(7, episode.Episode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RemoveEpisode_RemovesOrReportsNotFound()
|
||||
{
|
||||
var fixture = new TestDb();
|
||||
var show = Show.Create("Series", ShowKind.Series);
|
||||
var ep = show.AddEpisode(Guid.NewGuid());
|
||||
await using (var seed = fixture.New())
|
||||
{
|
||||
seed.Shows.Add(show);
|
||||
await seed.SaveChangesAsync(CancellationToken.None);
|
||||
}
|
||||
|
||||
await using var db = fixture.New();
|
||||
var handler = new RemoveEpisodeCommandHandler(db);
|
||||
|
||||
var missing = await handler.Handle(
|
||||
new RemoveEpisodeCommand(show.Id, Guid.NewGuid()),
|
||||
CancellationToken.None
|
||||
);
|
||||
Assert.Equal(ShowErrors.EpisodeNotFound, missing.Error);
|
||||
|
||||
var ok = await handler.Handle(
|
||||
new RemoveEpisodeCommand(show.Id, ep.Id),
|
||||
CancellationToken.None
|
||||
);
|
||||
Assert.True(ok.IsSuccess);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using TeleWave.Infrastructure.Persistence;
|
||||
|
||||
namespace TeleWave.Application.Tests.Support;
|
||||
|
||||
/// <summary>
|
||||
/// Общий InMemory-стенд поверх реального <see cref="AppDbContext"/> (с настоящими EF-конфигурациями).
|
||||
/// Каждый экземпляр — своя изолированная БД; <see cref="New"/> отдаёт новый контекст на ту же БД,
|
||||
/// чтобы можно было перечитать сохранённое свежим контекстом.
|
||||
/// </summary>
|
||||
internal sealed class TestDb
|
||||
{
|
||||
private readonly string _name = $"tw-{Guid.NewGuid():N}";
|
||||
|
||||
public AppDbContext New() =>
|
||||
new(new DbContextOptionsBuilder<AppDbContext>().UseInMemoryDatabase(_name).Options);
|
||||
}
|
||||
@@ -25,5 +25,6 @@
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\TeleWave.Application\TeleWave.Application.csproj" />
|
||||
<ProjectReference Include="..\..\src\TeleWave.Infrastructure\TeleWave.Infrastructure.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
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.CreateOverride;
|
||||
using TeleWave.Application.Broadcast.UpdateChannelSettings;
|
||||
using TeleWave.Application.Broadcast.UpdateChannelShow;
|
||||
using TeleWave.Domain.Broadcast;
|
||||
using Xunit;
|
||||
|
||||
namespace TeleWave.Application.Tests.Validators;
|
||||
|
||||
public class ValidatorTests
|
||||
{
|
||||
[Fact]
|
||||
public void UpdateChannelShow_RejectsBadWeightMultiplierAndWindows()
|
||||
{
|
||||
var v = new UpdateChannelShowCommandValidator();
|
||||
|
||||
var good = new UpdateChannelShowCommand(
|
||||
Guid.NewGuid(),
|
||||
Guid.NewGuid(),
|
||||
5,
|
||||
BlockMode.Count,
|
||||
3,
|
||||
true,
|
||||
4,
|
||||
[new HourWindowInput(18, 23)]
|
||||
);
|
||||
Assert.True(v.Validate(good).IsValid);
|
||||
|
||||
var badMultiplier = good with { PreferredWeightMultiplier = 0 };
|
||||
Assert.False(v.Validate(badMultiplier).IsValid);
|
||||
|
||||
var badWindow = good with { PreferredHours = [new HourWindowInput(23, 20)] };
|
||||
Assert.False(v.Validate(badWindow).IsValid);
|
||||
|
||||
var badWeight = good with { Weight = 0 };
|
||||
Assert.False(v.Validate(badWeight).IsValid);
|
||||
}
|
||||
|
||||
[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,
|
||||
10,
|
||||
BumperSelection.Rotation,
|
||||
0.5,
|
||||
0.5
|
||||
);
|
||||
|
||||
var good = new UpdateChannelSettingsCommand(
|
||||
Guid.NewGuid(),
|
||||
"Name",
|
||||
true,
|
||||
AdInsertion.BetweenBlocks,
|
||||
2,
|
||||
true,
|
||||
bumper,
|
||||
null
|
||||
);
|
||||
Assert.True(v.Validate(good).IsValid);
|
||||
|
||||
Assert.False(v.Validate(good with { Name = "" }).IsValid);
|
||||
Assert.False(v.Validate(good with { AdsPerBreak = 99 }).IsValid);
|
||||
Assert.False(
|
||||
v.Validate(good with { Bumper = bumper with { ShowChangeChance = 2 } }).IsValid
|
||||
);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateOverride_RequiresShows_WithValidWeights()
|
||||
{
|
||||
var v = new CreateProgrammingOverrideCommandValidator();
|
||||
|
||||
var good = new CreateProgrammingOverrideCommand(
|
||||
Guid.NewGuid(),
|
||||
OverrideMode.Boost,
|
||||
OverrideRecurrence.OneTime,
|
||||
DateTimeOffset.UtcNow,
|
||||
DateTimeOffset.UtcNow.AddHours(1),
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
[new OverrideShowInput(Guid.NewGuid(), 3)]
|
||||
);
|
||||
Assert.True(v.Validate(good).IsValid);
|
||||
|
||||
Assert.False(v.Validate(good with { Shows = [] }).IsValid);
|
||||
Assert.False(
|
||||
v.Validate(good with { Shows = [new OverrideShowInput(Guid.NewGuid(), 0)] }).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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
using TeleWave.Domain.Broadcast;
|
||||
using Xunit;
|
||||
|
||||
namespace TeleWave.Domain.Tests.Broadcast;
|
||||
|
||||
public class BumperTemplateTests
|
||||
{
|
||||
private static BumperTemplate NewTemplate() =>
|
||||
Channel.Create("c", "c", DateTimeOffset.UnixEpoch).AddBumperTemplate("Block");
|
||||
|
||||
[Fact]
|
||||
public void Create_HasDefaultsAndOneVariant()
|
||||
{
|
||||
var t = NewTemplate();
|
||||
|
||||
Assert.Equal(BumperTemplate.DefaultBackgroundColor, t.BackgroundColor);
|
||||
Assert.Null(t.AudioExtension);
|
||||
Assert.Null(t.BackgroundImageId);
|
||||
Assert.Equal(0, t.Revision);
|
||||
var v = Assert.Single(t.Variants);
|
||||
Assert.Equal(BumperTrigger.OnShowChange, v.Trigger);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AddVariant_AppendsWithNextPosition()
|
||||
{
|
||||
var t = NewTemplate();
|
||||
|
||||
var v1 = t.AddVariant("Text 2");
|
||||
|
||||
Assert.Equal(1, v1.Position);
|
||||
Assert.Equal(2, t.Variants.Count);
|
||||
Assert.Equal(v1, t.FindVariant(v1.Id));
|
||||
Assert.Null(t.FindVariant(Guid.NewGuid()));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RemoveVariant_CannotRemoveLast()
|
||||
{
|
||||
var t = NewTemplate();
|
||||
var first = t.Variants[0];
|
||||
|
||||
Assert.False(t.RemoveVariant(first.Id)); // единственный — нельзя
|
||||
|
||||
var second = t.AddVariant("Text 2");
|
||||
Assert.True(t.RemoveVariant(second.Id));
|
||||
Assert.False(t.RemoveVariant(Guid.NewGuid()));
|
||||
Assert.Single(t.Variants);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UpdateStyle_ChangesColorsAndName()
|
||||
{
|
||||
var t = NewTemplate();
|
||||
|
||||
t.UpdateStyle("New", "0x111111", "0x222222", "0x333333", "black");
|
||||
|
||||
Assert.Equal("New", t.Name);
|
||||
Assert.Equal("0x111111", t.BackgroundColor);
|
||||
Assert.Equal("0x333333", t.AccentColor);
|
||||
Assert.Equal("black", t.TextColor);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SetAudio_BumpsRevision_AndStoresDuration()
|
||||
{
|
||||
var t = NewTemplate();
|
||||
|
||||
t.SetAudio(".mp3", 12.5);
|
||||
|
||||
Assert.Equal(".mp3", t.AudioExtension);
|
||||
Assert.Equal(12.5, t.AudioDurationSeconds);
|
||||
Assert.Equal(1, t.Revision);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SetAudio_NonPositiveDuration_StoresNull()
|
||||
{
|
||||
var t = NewTemplate();
|
||||
t.SetAudio(".wav", 0);
|
||||
Assert.Null(t.AudioDurationSeconds);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ClearAudio_ResetsAndBumpsRevisionOnceWhenPresent()
|
||||
{
|
||||
var t = NewTemplate();
|
||||
t.SetAudio(".mp3", 5); // rev 1
|
||||
|
||||
t.ClearAudio(); // rev 2
|
||||
Assert.Null(t.AudioExtension);
|
||||
Assert.Equal(2, t.Revision);
|
||||
|
||||
t.ClearAudio(); // уже пусто — ревизия не двигается
|
||||
Assert.Equal(2, t.Revision);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BackgroundImage_SetAndClear_BumpRevision()
|
||||
{
|
||||
var t = NewTemplate();
|
||||
var img = Guid.NewGuid();
|
||||
|
||||
t.SetBackgroundImage(img); // rev 1
|
||||
Assert.Equal(img, t.BackgroundImageId);
|
||||
Assert.Equal(1, t.Revision);
|
||||
|
||||
t.ClearBackgroundImage(); // rev 2
|
||||
Assert.Null(t.BackgroundImageId);
|
||||
Assert.Equal(2, t.Revision);
|
||||
|
||||
t.ClearBackgroundImage(); // уже пусто
|
||||
Assert.Equal(2, t.Revision);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
using TeleWave.Domain.Broadcast;
|
||||
using Xunit;
|
||||
|
||||
namespace TeleWave.Domain.Tests.Broadcast;
|
||||
|
||||
public class BumperTextVariantTests
|
||||
{
|
||||
private static BumperTextVariant NewVariant() =>
|
||||
Channel
|
||||
.Create("c", "c", DateTimeOffset.UnixEpoch)
|
||||
.AddBumperTemplate("b")
|
||||
.AddVariant("Text");
|
||||
|
||||
[Fact]
|
||||
public void Create_HasDefaultLabelsAndWeight()
|
||||
{
|
||||
var v = NewVariant();
|
||||
|
||||
Assert.Equal(BumperTextKind.NowNext, v.Kind);
|
||||
Assert.Equal(BumperTextVariant.DefaultNowLabel, v.NowLabel);
|
||||
Assert.Equal(BumperTextVariant.DefaultNextLabel, v.NextLabel);
|
||||
Assert.Equal(BumperTextVariant.DefaultWeight, v.Weight);
|
||||
Assert.Equal(string.Empty, v.Line1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_SetsFields_AndClampsNegativeWeightToZero()
|
||||
{
|
||||
var v = NewVariant();
|
||||
|
||||
v.Update("Name", BumperTextKind.Free, "NOW", "NEXT", "l1", "l2", BumperTrigger.Both, -5);
|
||||
|
||||
Assert.Equal("Name", v.Name);
|
||||
Assert.Equal(BumperTextKind.Free, v.Kind);
|
||||
Assert.Equal("NOW", v.NowLabel);
|
||||
Assert.Equal("l1", v.Line1);
|
||||
Assert.Equal(BumperTrigger.Both, v.Trigger);
|
||||
Assert.Equal(0, v.Weight);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(BumperTrigger.OnShowChange, true, true)]
|
||||
[InlineData(BumperTrigger.OnShowChange, false, false)]
|
||||
[InlineData(BumperTrigger.BetweenEpisodes, true, false)]
|
||||
[InlineData(BumperTrigger.BetweenEpisodes, false, true)]
|
||||
[InlineData(BumperTrigger.Both, true, true)]
|
||||
[InlineData(BumperTrigger.Both, false, true)]
|
||||
public void Matches_FollowsTrigger(BumperTrigger trigger, bool isShowChange, bool expected)
|
||||
{
|
||||
var v = NewVariant();
|
||||
v.Update("n", BumperTextKind.NowNext, "a", "b", "", "", trigger, 1);
|
||||
|
||||
Assert.Equal(expected, v.Matches(isShowChange));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
using TeleWave.Domain.Broadcast;
|
||||
using Xunit;
|
||||
|
||||
namespace TeleWave.Domain.Tests.Broadcast;
|
||||
|
||||
public class ChannelShowTests
|
||||
{
|
||||
private static ChannelShow NewShow() =>
|
||||
Channel
|
||||
.Create("c", "c", DateTimeOffset.UnixEpoch)
|
||||
.AddShow(Guid.NewGuid(), 1, BlockMode.Count, 1);
|
||||
|
||||
[Fact]
|
||||
public void Create_HasDefaultPreferredMultiplier_AndNoWindows()
|
||||
{
|
||||
var cs = NewShow();
|
||||
|
||||
Assert.Equal(ChannelShow.DefaultPreferredWeightMultiplier, cs.PreferredWeightMultiplier);
|
||||
Assert.Empty(cs.PreferredHours);
|
||||
Assert.True(cs.IsEnabled);
|
||||
Assert.Equal(0, cs.NextEpisodeIndex);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_ChangesCoreFields()
|
||||
{
|
||||
var cs = NewShow();
|
||||
|
||||
cs.Update(9, BlockMode.Duration, 45, false);
|
||||
|
||||
Assert.Equal(9, cs.Weight);
|
||||
Assert.Equal(BlockMode.Duration, cs.BlockMode);
|
||||
Assert.Equal(45, cs.BlockValue);
|
||||
Assert.False(cs.IsEnabled);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SetPreferredHours_ClampsMultiplier_AndNormalizesWindows()
|
||||
{
|
||||
var cs = NewShow();
|
||||
|
||||
cs.SetPreferredHours(
|
||||
0, // < 1 → должно стать 1
|
||||
[
|
||||
(18, 23), // валидное
|
||||
(10, 10), // start == end → отбрасывается
|
||||
(5, 25), // end > 24 → отбрасывается
|
||||
(-1, 3), // start < 0 → отбрасывается
|
||||
(18, 23), // дубликат → схлопывается
|
||||
]
|
||||
);
|
||||
|
||||
Assert.Equal(1, cs.PreferredWeightMultiplier);
|
||||
var window = Assert.Single(cs.PreferredHours);
|
||||
Assert.Equal(18, window.StartHour);
|
||||
Assert.Equal(23, window.EndHour);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SetPreferredHours_ReplacesPreviousSet()
|
||||
{
|
||||
var cs = NewShow();
|
||||
cs.SetPreferredHours(3, [(1, 2)]);
|
||||
cs.SetPreferredHours(2, [(8, 9), (20, 22)]);
|
||||
|
||||
Assert.Equal(2, cs.PreferredWeightMultiplier);
|
||||
Assert.Equal(2, cs.PreferredHours.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsPreferredAt_ChecksWindows()
|
||||
{
|
||||
var cs = NewShow();
|
||||
cs.SetPreferredHours(3, [(18, 23)]);
|
||||
|
||||
Assert.True(cs.IsPreferredAt(18));
|
||||
Assert.True(cs.IsPreferredAt(22));
|
||||
Assert.False(cs.IsPreferredAt(23)); // конец исключается
|
||||
Assert.False(cs.IsPreferredAt(9));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SetNextEpisodeIndex_MovesCursor()
|
||||
{
|
||||
var cs = NewShow();
|
||||
cs.SetNextEpisodeIndex(5);
|
||||
Assert.Equal(5, cs.NextEpisodeIndex);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
using TeleWave.Domain.Broadcast;
|
||||
using Xunit;
|
||||
|
||||
namespace TeleWave.Domain.Tests.Broadcast;
|
||||
|
||||
public class ChannelTests
|
||||
{
|
||||
private static readonly DateTimeOffset Epoch = new(2026, 1, 1, 0, 0, 0, TimeSpan.Zero);
|
||||
|
||||
private static Channel NewChannel() => Channel.Create("News", "news", Epoch);
|
||||
|
||||
[Fact]
|
||||
public void Create_SetsDefaults_AndSeedsDefaultBumperTemplate()
|
||||
{
|
||||
var channel = NewChannel();
|
||||
|
||||
Assert.NotEqual(Guid.Empty, channel.Id);
|
||||
Assert.Equal("News", channel.Name);
|
||||
Assert.Equal("news", channel.Slug);
|
||||
Assert.True(channel.IsEnabled);
|
||||
Assert.Equal(Epoch, channel.EpochUtc);
|
||||
Assert.Equal(AdInsertion.BetweenBlocks, channel.AdInsertion);
|
||||
Assert.False(channel.BumpersEnabled);
|
||||
Assert.Equal(BumperSelection.Rotation, channel.BumperSelection);
|
||||
Assert.Equal(1.0, channel.BumperShowChangeChance);
|
||||
Assert.Equal(1.0, channel.BumperEpisodeChangeChance);
|
||||
|
||||
var template = Assert.Single(channel.BumperTemplates);
|
||||
Assert.True(template.IsDefault);
|
||||
Assert.Equal(0, template.Position);
|
||||
Assert.Single(template.Variants); // дефолтный подблок
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UpdateSettings_ChangesFields()
|
||||
{
|
||||
var channel = NewChannel();
|
||||
var filler = Guid.NewGuid();
|
||||
|
||||
channel.UpdateSettings("N2", false, AdInsertion.BetweenEpisodes, 4, true, filler);
|
||||
|
||||
Assert.Equal("N2", channel.Name);
|
||||
Assert.False(channel.IsEnabled);
|
||||
Assert.Equal(AdInsertion.BetweenEpisodes, channel.AdInsertion);
|
||||
Assert.Equal(4, channel.AdsPerBreak);
|
||||
Assert.True(channel.BumpersEnabled);
|
||||
Assert.Equal(filler, channel.FillerAssetId);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(-1, 2.0, -5.0, 1.0, 0.0)]
|
||||
[InlineData(30, 0.3, 0.9, 0.3, 0.9)]
|
||||
public void UpdateBumperSettings_ClampsChancesAndInterval(
|
||||
int interval,
|
||||
double show,
|
||||
double episode,
|
||||
double expectedShow,
|
||||
double expectedEpisode
|
||||
)
|
||||
{
|
||||
var channel = NewChannel();
|
||||
|
||||
channel.UpdateBumperSettings(
|
||||
BumperFont.Serif,
|
||||
interval,
|
||||
BumperSelection.WeightedRandom,
|
||||
show,
|
||||
episode
|
||||
);
|
||||
|
||||
Assert.Equal(BumperFont.Serif, channel.BumperFont);
|
||||
Assert.Equal(Math.Max(0, interval), channel.BumperMinIntervalMinutes);
|
||||
Assert.Equal(BumperSelection.WeightedRandom, channel.BumperSelection);
|
||||
Assert.Equal(expectedShow, channel.BumperShowChangeChance);
|
||||
Assert.Equal(expectedEpisode, channel.BumperEpisodeChangeChance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AddBumperTemplate_AppendsWithNextPosition()
|
||||
{
|
||||
var channel = NewChannel();
|
||||
|
||||
var t1 = channel.AddBumperTemplate("Block 2");
|
||||
var t2 = channel.AddBumperTemplate("Block 3");
|
||||
|
||||
Assert.Equal(1, t1.Position);
|
||||
Assert.Equal(2, t2.Position);
|
||||
Assert.False(t1.IsDefault);
|
||||
Assert.Equal(3, channel.BumperTemplates.Count);
|
||||
Assert.Equal(t1, channel.FindBumperTemplate(t1.Id));
|
||||
Assert.Null(channel.FindBumperTemplate(Guid.NewGuid()));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RemoveBumperTemplate_CannotRemoveDefault_CanRemoveOthers()
|
||||
{
|
||||
var channel = NewChannel();
|
||||
var def = channel.BumperTemplates[0];
|
||||
var extra = channel.AddBumperTemplate("Block 2");
|
||||
|
||||
Assert.False(channel.RemoveBumperTemplate(def.Id));
|
||||
Assert.True(channel.RemoveBumperTemplate(extra.Id));
|
||||
Assert.False(channel.RemoveBumperTemplate(Guid.NewGuid()));
|
||||
Assert.Single(channel.BumperTemplates);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Shows_AddFindRemoveHas()
|
||||
{
|
||||
var channel = NewChannel();
|
||||
var showId = Guid.NewGuid();
|
||||
|
||||
var cs = channel.AddShow(showId, 5, BlockMode.Count, 3);
|
||||
|
||||
Assert.Equal(showId, cs.ShowId);
|
||||
Assert.Equal(5, cs.Weight);
|
||||
Assert.True(channel.HasShow(showId));
|
||||
Assert.Equal(cs, channel.FindShow(cs.Id));
|
||||
Assert.True(channel.RemoveShow(cs.Id));
|
||||
Assert.False(channel.RemoveShow(cs.Id));
|
||||
Assert.False(channel.HasShow(showId));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Ads_AppendWithPosition_AndRemove()
|
||||
{
|
||||
var channel = NewChannel();
|
||||
var a1 = channel.AddAd(Guid.NewGuid());
|
||||
var a2 = channel.AddAd(Guid.NewGuid());
|
||||
|
||||
Assert.Equal(0, a1.Position);
|
||||
Assert.Equal(1, a2.Position);
|
||||
Assert.True(channel.HasAd(a1.MediaAssetId));
|
||||
Assert.True(channel.RemoveAd(a1.Id));
|
||||
Assert.False(channel.RemoveAd(a1.Id));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AddOverride_OneTime_AndWeekly()
|
||||
{
|
||||
var channel = NewChannel();
|
||||
|
||||
var one = channel.AddOverride(OverrideMode.Exclusive, Epoch, Epoch.AddHours(2));
|
||||
Assert.Equal(OverrideRecurrence.OneTime, one.Recurrence);
|
||||
Assert.Equal(Epoch, one.StartsAtUtc);
|
||||
|
||||
var weekly = channel.AddWeeklyOverride(OverrideMode.Boost, 6, 600, 720);
|
||||
Assert.Equal(OverrideRecurrence.Weekly, weekly.Recurrence);
|
||||
Assert.Equal(6, weekly.DayOfWeek);
|
||||
Assert.Equal(600, weekly.StartMinute);
|
||||
|
||||
Assert.Equal(2, channel.Overrides.Count);
|
||||
Assert.True(channel.RemoveOverride(one.Id));
|
||||
Assert.False(channel.RemoveOverride(Guid.NewGuid()));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Cursors_AreMovedBySetters()
|
||||
{
|
||||
var channel = NewChannel();
|
||||
channel.SetNextAdIndex(7);
|
||||
channel.SetNextBumperIndex(3);
|
||||
|
||||
Assert.Equal(7, channel.NextAdIndex);
|
||||
Assert.Equal(3, channel.NextBumperIndex);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
using TeleWave.Domain.Broadcast;
|
||||
using TeleWave.Domain.Settings;
|
||||
using Xunit;
|
||||
|
||||
namespace TeleWave.Domain.Tests.Broadcast;
|
||||
|
||||
/// <summary>Мелкие фабрики/сеттеры простых сущностей, которые нигде не покрыты напрямую.</summary>
|
||||
public class DomainRecordFactoryTests
|
||||
{
|
||||
[Fact]
|
||||
public void ChannelShowHour_Contains_HalfOpenWindow()
|
||||
{
|
||||
var cs = Channel
|
||||
.Create("c", "c", DateTimeOffset.UnixEpoch)
|
||||
.AddShow(Guid.NewGuid(), 1, BlockMode.Count, 1);
|
||||
cs.SetPreferredHours(2, [(9, 12)]);
|
||||
var hour = Assert.Single(cs.PreferredHours);
|
||||
|
||||
Assert.False(hour.Contains(8));
|
||||
Assert.True(hour.Contains(9));
|
||||
Assert.True(hour.Contains(11));
|
||||
Assert.False(hour.Contains(12));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BumperAsset_Create_StoresPair()
|
||||
{
|
||||
var from = Guid.NewGuid();
|
||||
var to = Guid.NewGuid();
|
||||
var asset = Guid.NewGuid();
|
||||
|
||||
var b = BumperAsset.Create(from, to, "sig", asset);
|
||||
|
||||
Assert.Equal(from, b.FromShowId);
|
||||
Assert.Equal(to, b.ToShowId);
|
||||
Assert.Equal("sig", b.Signature);
|
||||
Assert.Equal(asset, b.MediaAssetId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AppSetting_CreateAndSetValue()
|
||||
{
|
||||
var s = AppSetting.Create("site.title", "TeleWave");
|
||||
Assert.Equal("site.title", s.Key);
|
||||
Assert.Equal("TeleWave", s.Value);
|
||||
|
||||
s.SetValue("New");
|
||||
Assert.Equal("New", s.Value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
using TeleWave.Domain.Broadcast;
|
||||
using Xunit;
|
||||
|
||||
namespace TeleWave.Domain.Tests.Broadcast;
|
||||
|
||||
public class ProgrammingOverrideTests
|
||||
{
|
||||
private static readonly DateTimeOffset Epoch = new(2026, 1, 1, 0, 0, 0, TimeSpan.Zero);
|
||||
|
||||
private static Channel NewChannel() => Channel.Create("c", "c", Epoch);
|
||||
|
||||
[Fact]
|
||||
public void OneTime_Covers_OnlyInsideWindow()
|
||||
{
|
||||
var ovr = NewChannel()
|
||||
.AddOverride(OverrideMode.Exclusive, Epoch.AddHours(1), Epoch.AddHours(3));
|
||||
|
||||
Assert.False(ovr.Covers(Epoch.AddMinutes(59)));
|
||||
Assert.True(ovr.Covers(Epoch.AddHours(1)));
|
||||
Assert.True(ovr.Covers(Epoch.AddHours(2)));
|
||||
Assert.False(ovr.Covers(Epoch.AddHours(3))); // конец исключается
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AddShow_AttachesWithWeight()
|
||||
{
|
||||
var ovr = NewChannel().AddOverride(OverrideMode.Boost, Epoch, Epoch.AddHours(1));
|
||||
var showId = Guid.NewGuid();
|
||||
|
||||
var os = ovr.AddShow(showId, 4);
|
||||
|
||||
Assert.Equal(showId, os.ShowId);
|
||||
Assert.Equal(4, os.Weight);
|
||||
Assert.Single(ovr.Shows);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Weekly_Covers_MatchingDayAndMinuteWindow()
|
||||
{
|
||||
// Epoch = 2026-01-01 — четверг (DayOfWeek=4).
|
||||
var day = (int)Epoch.UtcDateTime.DayOfWeek;
|
||||
var ovr = NewChannel().AddWeeklyOverride(OverrideMode.Exclusive, day, 600, 720); // 10:00–12:00
|
||||
|
||||
Assert.True(ovr.Covers(Epoch.AddMinutes(600))); // 10:00 того же дня
|
||||
Assert.True(ovr.Covers(Epoch.AddMinutes(719))); // 11:59
|
||||
Assert.False(ovr.Covers(Epoch.AddMinutes(720))); // 12:00 — конец исключается
|
||||
Assert.False(ovr.Covers(Epoch.AddMinutes(599))); // 09:59
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Weekly_Covers_NextWeekSameDay()
|
||||
{
|
||||
var day = (int)Epoch.UtcDateTime.DayOfWeek;
|
||||
var ovr = NewChannel().AddWeeklyOverride(OverrideMode.Exclusive, day, 0, 1440);
|
||||
|
||||
Assert.True(ovr.Covers(Epoch.AddDays(7).AddHours(5)));
|
||||
Assert.False(ovr.Covers(Epoch.AddDays(1).AddHours(5))); // другой день недели
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
using TeleWave.Domain.Broadcast;
|
||||
using Xunit;
|
||||
|
||||
namespace TeleWave.Domain.Tests.Broadcast;
|
||||
|
||||
public class ScheduleEntryTests
|
||||
{
|
||||
private static readonly DateTimeOffset Start = new(2026, 1, 1, 0, 0, 0, TimeSpan.Zero);
|
||||
|
||||
[Fact]
|
||||
public void Program_SetsKindAndEpg()
|
||||
{
|
||||
var channel = Guid.NewGuid();
|
||||
var asset = Guid.NewGuid();
|
||||
var show = Guid.NewGuid();
|
||||
|
||||
var e = ScheduleEntry.Program(channel, asset, Start, Start.AddMinutes(20), show, 3);
|
||||
|
||||
Assert.Equal(ScheduleEntryKind.Program, e.Kind);
|
||||
Assert.Equal(show, e.ShowId);
|
||||
Assert.Equal(3, e.EpisodeIndex);
|
||||
Assert.Null(e.BumperVariantId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Ad_HasNoShow()
|
||||
{
|
||||
var e = ScheduleEntry.Ad(Guid.NewGuid(), Guid.NewGuid(), Start, Start.AddMinutes(1));
|
||||
|
||||
Assert.Equal(ScheduleEntryKind.Ad, e.Kind);
|
||||
Assert.Null(e.ShowId);
|
||||
Assert.Null(e.EpisodeIndex);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Bumper_CarriesShowAndVariant()
|
||||
{
|
||||
var toShow = Guid.NewGuid();
|
||||
var variant = Guid.NewGuid();
|
||||
|
||||
var e = ScheduleEntry.Bumper(
|
||||
Guid.NewGuid(),
|
||||
Guid.NewGuid(),
|
||||
Start,
|
||||
Start.AddSeconds(8),
|
||||
toShow,
|
||||
variant
|
||||
);
|
||||
|
||||
Assert.Equal(ScheduleEntryKind.Bumper, e.Kind);
|
||||
Assert.Equal(toShow, e.ShowId);
|
||||
Assert.Equal(variant, e.BumperVariantId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
using TeleWave.Domain.Images;
|
||||
using Xunit;
|
||||
|
||||
namespace TeleWave.Domain.Tests.Images;
|
||||
|
||||
public class ImageTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData("jpg", ".jpg")]
|
||||
[InlineData(".PNG", ".png")]
|
||||
[InlineData(" .WebP ", ".webp")]
|
||||
public void Create_NormalizesExtension(string input, string expected)
|
||||
{
|
||||
var img = Image.Create(ImageCategory.ShowPoster, input, "poster.jpg");
|
||||
|
||||
Assert.Equal(expected, img.FileExtension);
|
||||
Assert.Equal(ImageCategory.ShowPoster, img.Category);
|
||||
Assert.Equal("poster.jpg", img.OriginalFileName);
|
||||
Assert.NotEqual(Guid.Empty, img.Id);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Create_BlankFileName_BecomesNull()
|
||||
{
|
||||
var img = Image.Create(ImageCategory.Library, ".jpg", " ");
|
||||
Assert.Null(img.OriginalFileName);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
using TeleWave.Domain.Library;
|
||||
using Xunit;
|
||||
|
||||
namespace TeleWave.Domain.Tests.Library;
|
||||
|
||||
public class ShowEpisodeTests
|
||||
{
|
||||
private static ShowEpisode NewEpisode() =>
|
||||
Show.Create("A", ShowKind.Series).AddEpisode(Guid.NewGuid());
|
||||
|
||||
[Fact]
|
||||
public void SetNumbers_StoresSeasonEpisode()
|
||||
{
|
||||
var e = NewEpisode();
|
||||
e.SetNumbers(3, 7);
|
||||
|
||||
Assert.Equal(3, e.Season);
|
||||
Assert.Equal(7, e.Episode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ApplyMetadata_SetsFields_StillOnlyWhenProvided()
|
||||
{
|
||||
var e = NewEpisode();
|
||||
var still = Guid.NewGuid();
|
||||
var air = new DateOnly(2020, 5, 1);
|
||||
|
||||
e.ApplyMetadata("Title", "Overview", still, air);
|
||||
Assert.Equal("Title", e.Title);
|
||||
Assert.Equal("Overview", e.Overview);
|
||||
Assert.Equal(still, e.StillImageId);
|
||||
Assert.Equal(air, e.AirDate);
|
||||
|
||||
// Повторно без кадра — существующий не затирается.
|
||||
e.ApplyMetadata("T2", "O2", null, null);
|
||||
Assert.Equal(still, e.StillImageId);
|
||||
Assert.Equal("T2", e.Title);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ClearMetadata_ResetsAll()
|
||||
{
|
||||
var e = NewEpisode();
|
||||
e.ApplyMetadata("T", "O", Guid.NewGuid(), new DateOnly(2020, 1, 1));
|
||||
|
||||
e.ClearMetadata();
|
||||
|
||||
Assert.Null(e.Title);
|
||||
Assert.Null(e.Overview);
|
||||
Assert.Null(e.StillImageId);
|
||||
Assert.Null(e.AirDate);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
using TeleWave.Domain.Library;
|
||||
using Xunit;
|
||||
|
||||
namespace TeleWave.Domain.Tests.Library;
|
||||
|
||||
public class ShowTests
|
||||
{
|
||||
[Fact]
|
||||
public void Create_NormalizesOriginalName()
|
||||
{
|
||||
var s = Show.Create("Симпсоны", ShowKind.Series, "desc", " The Simpsons ");
|
||||
|
||||
Assert.Equal("Симпсоны", s.Name);
|
||||
Assert.Equal("The Simpsons", s.OriginalName);
|
||||
Assert.Equal(ShowKind.Series, s.Kind);
|
||||
Assert.Equal("desc", s.Description);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Create_BlankOriginalName_BecomesNull()
|
||||
{
|
||||
var s = Show.Create("X", ShowKind.Single, originalName: " ");
|
||||
Assert.Null(s.OriginalName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RenameAndSetName_ChangeDisplayName()
|
||||
{
|
||||
var s = Show.Create("A", ShowKind.Series);
|
||||
s.Rename("B", "d");
|
||||
Assert.Equal("B", s.Name);
|
||||
Assert.Equal("d", s.Description);
|
||||
|
||||
s.SetName("C");
|
||||
Assert.Equal("C", s.Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SetOriginalName_TrimsOrClears()
|
||||
{
|
||||
var s = Show.Create("A", ShowKind.Series);
|
||||
s.SetOriginalName(" orig ");
|
||||
Assert.Equal("orig", s.OriginalName);
|
||||
s.SetOriginalName("");
|
||||
Assert.Null(s.OriginalName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AddEpisode_AppendsInOrder()
|
||||
{
|
||||
var s = Show.Create("A", ShowKind.Series);
|
||||
var e0 = s.AddEpisode(Guid.NewGuid());
|
||||
var e1 = s.AddEpisode(Guid.NewGuid());
|
||||
|
||||
Assert.Equal(0, e0.Position);
|
||||
Assert.Equal(1, e1.Position);
|
||||
Assert.Equal(2, s.Episodes.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RemoveEpisode_RemovesById()
|
||||
{
|
||||
var s = Show.Create("A", ShowKind.Series);
|
||||
var e = s.AddEpisode(Guid.NewGuid());
|
||||
|
||||
Assert.True(s.RemoveEpisode(e.Id));
|
||||
Assert.False(s.RemoveEpisode(e.Id));
|
||||
Assert.Empty(s.Episodes);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanAddEpisode_SingleAllowsOnlyOne()
|
||||
{
|
||||
var single = Show.Create("Movie", ShowKind.Single);
|
||||
Assert.True(single.CanAddEpisode);
|
||||
single.AddEpisode(Guid.NewGuid());
|
||||
Assert.False(single.CanAddEpisode);
|
||||
|
||||
var series = Show.Create("Series", ShowKind.Series);
|
||||
series.AddEpisode(Guid.NewGuid());
|
||||
Assert.True(series.CanAddEpisode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ApplyMetadata_SetsProviderYearPoster_KeepsDescriptionIfBlank()
|
||||
{
|
||||
var s = Show.Create("A", ShowKind.Series, description: "old");
|
||||
var poster = Guid.NewGuid();
|
||||
|
||||
s.ApplyMetadata("tmdb", "42", description: null, year: 1999, posterImageId: poster);
|
||||
|
||||
Assert.Equal("tmdb", s.MetadataProvider);
|
||||
Assert.Equal("42", s.MetadataExternalId);
|
||||
Assert.Equal(1999, s.Year);
|
||||
Assert.Equal(poster, s.PosterImageId);
|
||||
Assert.Equal("old", s.Description); // пустое описание не затирает
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UpdateMetadataManual_MarksManualAndClearsExternal()
|
||||
{
|
||||
var s = Show.Create("A", ShowKind.Series);
|
||||
s.ApplyMetadata("tmdb", "1", "d", 2000, Guid.NewGuid());
|
||||
|
||||
s.UpdateMetadataManual("new", 2001);
|
||||
|
||||
Assert.Equal("manual", s.MetadataProvider);
|
||||
Assert.Null(s.MetadataExternalId);
|
||||
Assert.Equal("new", s.Description);
|
||||
Assert.Equal(2001, s.Year);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SetPosterImage_AndClearMetadata()
|
||||
{
|
||||
var s = Show.Create("A", ShowKind.Series);
|
||||
s.ApplyMetadata("tmdb", "1", "d", 2000, Guid.NewGuid());
|
||||
s.SetPosterImage(Guid.NewGuid());
|
||||
|
||||
s.ClearMetadata();
|
||||
|
||||
Assert.Null(s.MetadataProvider);
|
||||
Assert.Null(s.MetadataExternalId);
|
||||
Assert.Null(s.Year);
|
||||
Assert.Null(s.PosterImageId);
|
||||
Assert.Null(s.Description);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user