diff --git a/backend/tests/TeleWave.Application.Tests/Admin/DelegatingHandlersTests.cs b/backend/tests/TeleWave.Application.Tests/Admin/DelegatingHandlersTests.cs new file mode 100644 index 0000000..f1ea3e2 --- /dev/null +++ b/backend/tests/TeleWave.Application.Tests/Admin/DelegatingHandlersTests.cs @@ -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(); + private readonly IIdentityService _identity = Substitute.For(); + private readonly ICurrentUser _currentUser = Substitute.For(); + + [Fact] + public async Task Roles_HandlersDelegateToService() + { + var id = Guid.NewGuid(); + var dto = new RoleDto(id, "editor", false); + _roles.CreateRoleAsync("editor", Arg.Any()).Returns(Result.Success(dto)); + _roles + .UpdateRoleAsync(id, "editor", Arg.Any()) + .Returns(Result.Success(dto)); + _roles.DeleteRoleAsync(id, Arg.Any()).Returns(Result.Success()); + _roles.ListRolesAsync(Arg.Any()).Returns(new List { 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()).Returns(Result.Success()); + _identity + .ResetPasswordAsync(id, "Password1", Arg.Any()) + .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()); + } + + [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(), Arg.Any()); + } + + [Fact] + public async Task DeleteUser_OtherUser_Delegates() + { + _currentUser.UserId.Returns(Guid.NewGuid()); + var target = Guid.NewGuid(); + _identity.DeleteUserAsync(target, Arg.Any()).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()) + .Returns(Result.Success()); + + var result = await new ChangePasswordCommandHandler(_identity, _currentUser).Handle( + new ChangePasswordCommand("old", "New12345"), + default + ); + + Assert.True(result.IsSuccess); + } +} diff --git a/backend/tests/TeleWave.Application.Tests/Broadcast/BumperVariantHandlersTests.cs b/backend/tests/TeleWave.Application.Tests/Broadcast/BumperVariantHandlersTests.cs new file mode 100644 index 0000000..8b25b0c --- /dev/null +++ b/backend/tests/TeleWave.Application.Tests/Broadcast/BumperVariantHandlersTests.cs @@ -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); + } +} diff --git a/backend/tests/TeleWave.Application.Tests/Broadcast/ChannelHandlersTests.cs b/backend/tests/TeleWave.Application.Tests/Broadcast/ChannelHandlersTests.cs new file mode 100644 index 0000000..263f876 --- /dev/null +++ b/backend/tests/TeleWave.Application.Tests/Broadcast/ChannelHandlersTests.cs @@ -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); + } +} diff --git a/backend/tests/TeleWave.Application.Tests/Broadcast/QueryHandlersTests.cs b/backend/tests/TeleWave.Application.Tests/Broadcast/QueryHandlersTests.cs new file mode 100644 index 0000000..e22d4a7 --- /dev/null +++ b/backend/tests/TeleWave.Application.Tests/Broadcast/QueryHandlersTests.cs @@ -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(); + 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); + } + } +} diff --git a/backend/tests/TeleWave.Application.Tests/Library/ShowHandlersTests.cs b/backend/tests/TeleWave.Application.Tests/Library/ShowHandlersTests.cs new file mode 100644 index 0000000..b3bb145 --- /dev/null +++ b/backend/tests/TeleWave.Application.Tests/Library/ShowHandlersTests.cs @@ -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); + } +} diff --git a/backend/tests/TeleWave.Application.Tests/Support/TestDb.cs b/backend/tests/TeleWave.Application.Tests/Support/TestDb.cs new file mode 100644 index 0000000..252731a --- /dev/null +++ b/backend/tests/TeleWave.Application.Tests/Support/TestDb.cs @@ -0,0 +1,17 @@ +using Microsoft.EntityFrameworkCore; +using TeleWave.Infrastructure.Persistence; + +namespace TeleWave.Application.Tests.Support; + +/// +/// Общий InMemory-стенд поверх реального (с настоящими EF-конфигурациями). +/// Каждый экземпляр — своя изолированная БД; отдаёт новый контекст на ту же БД, +/// чтобы можно было перечитать сохранённое свежим контекстом. +/// +internal sealed class TestDb +{ + private readonly string _name = $"tw-{Guid.NewGuid():N}"; + + public AppDbContext New() => + new(new DbContextOptionsBuilder().UseInMemoryDatabase(_name).Options); +} diff --git a/backend/tests/TeleWave.Application.Tests/TeleWave.Application.Tests.csproj b/backend/tests/TeleWave.Application.Tests/TeleWave.Application.Tests.csproj index 2e6a52f..fb22ea2 100644 --- a/backend/tests/TeleWave.Application.Tests/TeleWave.Application.Tests.csproj +++ b/backend/tests/TeleWave.Application.Tests/TeleWave.Application.Tests.csproj @@ -25,5 +25,6 @@ + diff --git a/backend/tests/TeleWave.Application.Tests/Validators/ValidatorTests.cs b/backend/tests/TeleWave.Application.Tests/Validators/ValidatorTests.cs new file mode 100644 index 0000000..42a5b88 --- /dev/null +++ b/backend/tests/TeleWave.Application.Tests/Validators/ValidatorTests.cs @@ -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); + } +} diff --git a/backend/tests/TeleWave.Domain.Tests/Broadcast/BumperTemplateTests.cs b/backend/tests/TeleWave.Domain.Tests/Broadcast/BumperTemplateTests.cs new file mode 100644 index 0000000..eb01dd2 --- /dev/null +++ b/backend/tests/TeleWave.Domain.Tests/Broadcast/BumperTemplateTests.cs @@ -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); + } +} diff --git a/backend/tests/TeleWave.Domain.Tests/Broadcast/BumperTextVariantTests.cs b/backend/tests/TeleWave.Domain.Tests/Broadcast/BumperTextVariantTests.cs new file mode 100644 index 0000000..04deadc --- /dev/null +++ b/backend/tests/TeleWave.Domain.Tests/Broadcast/BumperTextVariantTests.cs @@ -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)); + } +} diff --git a/backend/tests/TeleWave.Domain.Tests/Broadcast/ChannelShowTests.cs b/backend/tests/TeleWave.Domain.Tests/Broadcast/ChannelShowTests.cs new file mode 100644 index 0000000..bc3da73 --- /dev/null +++ b/backend/tests/TeleWave.Domain.Tests/Broadcast/ChannelShowTests.cs @@ -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); + } +} diff --git a/backend/tests/TeleWave.Domain.Tests/Broadcast/ChannelTests.cs b/backend/tests/TeleWave.Domain.Tests/Broadcast/ChannelTests.cs new file mode 100644 index 0000000..05840d3 --- /dev/null +++ b/backend/tests/TeleWave.Domain.Tests/Broadcast/ChannelTests.cs @@ -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); + } +} diff --git a/backend/tests/TeleWave.Domain.Tests/Broadcast/DomainRecordFactoryTests.cs b/backend/tests/TeleWave.Domain.Tests/Broadcast/DomainRecordFactoryTests.cs new file mode 100644 index 0000000..f271744 --- /dev/null +++ b/backend/tests/TeleWave.Domain.Tests/Broadcast/DomainRecordFactoryTests.cs @@ -0,0 +1,50 @@ +using TeleWave.Domain.Broadcast; +using TeleWave.Domain.Settings; +using Xunit; + +namespace TeleWave.Domain.Tests.Broadcast; + +/// Мелкие фабрики/сеттеры простых сущностей, которые нигде не покрыты напрямую. +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); + } +} diff --git a/backend/tests/TeleWave.Domain.Tests/Broadcast/ProgrammingOverrideTests.cs b/backend/tests/TeleWave.Domain.Tests/Broadcast/ProgrammingOverrideTests.cs new file mode 100644 index 0000000..bfa6bfc --- /dev/null +++ b/backend/tests/TeleWave.Domain.Tests/Broadcast/ProgrammingOverrideTests.cs @@ -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))); // другой день недели + } +} diff --git a/backend/tests/TeleWave.Domain.Tests/Broadcast/ScheduleEntryTests.cs b/backend/tests/TeleWave.Domain.Tests/Broadcast/ScheduleEntryTests.cs new file mode 100644 index 0000000..1d072b9 --- /dev/null +++ b/backend/tests/TeleWave.Domain.Tests/Broadcast/ScheduleEntryTests.cs @@ -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); + } +} diff --git a/backend/tests/TeleWave.Domain.Tests/Images/ImageTests.cs b/backend/tests/TeleWave.Domain.Tests/Images/ImageTests.cs new file mode 100644 index 0000000..15e581f --- /dev/null +++ b/backend/tests/TeleWave.Domain.Tests/Images/ImageTests.cs @@ -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); + } +} diff --git a/backend/tests/TeleWave.Domain.Tests/Library/ShowEpisodeTests.cs b/backend/tests/TeleWave.Domain.Tests/Library/ShowEpisodeTests.cs new file mode 100644 index 0000000..f3e11be --- /dev/null +++ b/backend/tests/TeleWave.Domain.Tests/Library/ShowEpisodeTests.cs @@ -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); + } +} diff --git a/backend/tests/TeleWave.Domain.Tests/Library/ShowTests.cs b/backend/tests/TeleWave.Domain.Tests/Library/ShowTests.cs new file mode 100644 index 0000000..556d97e --- /dev/null +++ b/backend/tests/TeleWave.Domain.Tests/Library/ShowTests.cs @@ -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); + } +}