Enhance code readability and maintainability by refactoring various components. Update formatting commands in CLAUDE.md for csharpier integration. Simplify method signatures in SegmentFiles, UploadLimits, and multiple endpoint classes for improved clarity. Adjust query handlers and command handlers to streamline parameter handling and enhance consistency across the application.
ci / build-backend (push) Successful in 2m8s
ci / build-frontend (push) Failing after 32s
ci / tests (push) Skipped
ci / sonar (push) Skipped

This commit is contained in:
Leonid Pershin
2026-07-27 01:20:55 +03:00
parent 5449c05b5b
commit 9d1c6d2fc3
81 changed files with 716 additions and 461 deletions
@@ -35,9 +35,6 @@ public class ChannelHandlersTests
Assert.Equal(ChannelErrors.DuplicateSlug, dup.Error);
}
[Fact]
public async Task UpdateChannelSettings_UpdatesBumperChances()
{
@@ -69,7 +66,4 @@ public class ChannelHandlersTests
Assert.Equal(BumperFont.Sans, stored!.BumperFont);
Assert.Equal(BumperSelection.WeightedRandom, stored.BumperSelection);
}
}
@@ -24,7 +24,6 @@ public class QueryHandlersTests
private static GroupMembershipCleaner GroupCleaner(IAppDbContext db) =>
new(db, new GroupStatsService(db, new GroupElementResolver(db)));
[Fact]
public async Task GetChannel_UnknownId_ReturnsNotFound()
{
@@ -152,7 +151,6 @@ public class QueryHandlersTests
Assert.True(ok.IsSuccess);
}
[Fact]
public async Task AddAndRemoveBumperTemplate_WorkThroughStorage()
{
@@ -16,7 +16,9 @@ public class ImportManualInboxValidatorTests
[InlineData(null, null)] // номера не заданы — сервер разберёт имя сам
public void AllowsRealWorldNumbers(int? season, int? episode)
{
Assert.True(new ImportManualInboxCommandValidator().Validate(Command(season, episode)).IsValid);
Assert.True(
new ImportManualInboxCommandValidator().Validate(Command(season, episode)).IsValid
);
}
[Theory]
@@ -26,7 +28,9 @@ public class ImportManualInboxValidatorTests
[InlineData(1, 1000)]
public void RejectsOutOfRange(int? season, int? episode)
{
Assert.False(new ImportManualInboxCommandValidator().Validate(Command(season, episode)).IsValid);
Assert.False(
new ImportManualInboxCommandValidator().Validate(Command(season, episode)).IsValid
);
}
[Fact]
@@ -22,7 +22,16 @@ public class MediaStatsTests
var a = Pending(name);
a.MarkProcessing();
a.MarkReady(
new MediaReadyInfo(TimeSpan.FromMinutes(20), 2, 600, 1920, 1080, "h264", "aac", "assets/x")
new MediaReadyInfo(
TimeSpan.FromMinutes(20),
2,
600,
1920,
1080,
"h264",
"aac",
"assets/x"
)
);
return a;
}
@@ -26,9 +26,7 @@ public class ApplyShowMetadataTests
var result = await ApplyAsync(arranged);
Assert.True(result);
await arranged
.Provider.Received(1)
.GetShowAsync("42", kind, Arg.Any<CancellationToken>());
await arranged.Provider.Received(1).GetShowAsync("42", kind, Arg.Any<CancellationToken>());
}
[Fact]
@@ -48,7 +46,10 @@ public class ApplyShowMetadataTests
// «Not Rated» — это отсутствие данных. Снять им проставленный рейтинг нельзя: обновление
// метаданных тихо открыло бы взрослому шоу дорогу в детское время.
var arranged = await ArrangeAsync(ShowKind.Single, ShowAudience.R);
Respond(arranged.Provider, new ShowMetadata("42", "A", 2000, null, null, null, "Not Rated"));
Respond(
arranged.Provider,
new ShowMetadata("42", "A", 2000, null, null, null, "Not Rated")
);
await ApplyAsync(arranged);
@@ -63,11 +64,7 @@ public class ApplyShowMetadataTests
Assert.False(await ApplyAsync(arranged));
await arranged
.Provider.DidNotReceive()
.GetShowAsync(
Arg.Any<string>(),
Arg.Any<ShowKind>(),
Arg.Any<CancellationToken>()
);
.GetShowAsync(Arg.Any<string>(), Arg.Any<ShowKind>(), Arg.Any<CancellationToken>());
}
private static void Respond(IMetadataProvider provider, ShowMetadata meta) =>
@@ -38,9 +38,7 @@ public class LayerApplicabilityTests
{
// «20 декабря — 8 января» задаётся один раз и работает в любом году, поэтому сравнение идёт
// по паре (месяц, день), а не по датам.
var applicability = new LayerApplicability(
AnnualRanges: [new AnnualRange(12, 20, 1, 8)]
);
var applicability = new LayerApplicability(AnnualRanges: [new AnnualRange(12, 20, 1, 8)]);
Assert.Equal(expected, applicability.Covers(new DateOnly(year, month, day)));
}
@@ -128,8 +126,12 @@ public class LayerApplicabilityTests
Assert.Empty(Build(template));
}
private static void AddSlot(GridLayer layer, string title, TimeOnly start, int durationMinutes) =>
layer.AddSlot(title, start, durationMinutes);
private static void AddSlot(
GridLayer layer,
string title,
TimeOnly start,
int durationMinutes
) => layer.AddSlot(title, start, durationMinutes);
private static IReadOnlyList<ScheduledSlot> Build(
ScheduleTemplate template,
@@ -15,9 +15,9 @@ public class PlanningRulesTests
[InlineData(2, 0, false)]
public void AudienceAt_DayWindow(int hour, int minute, bool inside)
{
var rules = new PlanningRules(
[new AudienceWindow(new TimeOnly(6, 0), new TimeOnly(23, 0), ShowAudience.Pg13)]
);
var rules = new PlanningRules([
new AudienceWindow(new TimeOnly(6, 0), new TimeOnly(23, 0), ShowAudience.Pg13),
]);
var result = rules.AudienceAt(new TimeOnly(hour, minute));
@@ -31,9 +31,9 @@ public class PlanningRulesTests
public void AudienceAt_WindowCrossingMidnight(int hour, int minute, bool inside)
{
// «С 23:00 до 06:00» — ночное окно, границы сравниваются в обратную сторону.
var rules = new PlanningRules(
[new AudienceWindow(new TimeOnly(23, 0), new TimeOnly(6, 0), ShowAudience.Nc17)]
);
var rules = new PlanningRules([
new AudienceWindow(new TimeOnly(23, 0), new TimeOnly(6, 0), ShowAudience.Nc17),
]);
var result = rules.AudienceAt(new TimeOnly(hour, minute));
@@ -44,12 +44,10 @@ public class PlanningRulesTests
public void AudienceAt_OverlappingWindows_TakesTheStrictest()
{
// Широкое окно, случайно наложенное поверх детского, не должно его отменять.
var rules = new PlanningRules(
[
new AudienceWindow(new TimeOnly(6, 0), new TimeOnly(23, 0), ShowAudience.R),
new AudienceWindow(new TimeOnly(7, 0), new TimeOnly(10, 0), ShowAudience.G),
]
);
var rules = new PlanningRules([
new AudienceWindow(new TimeOnly(6, 0), new TimeOnly(23, 0), ShowAudience.R),
new AudienceWindow(new TimeOnly(7, 0), new TimeOnly(10, 0), ShowAudience.G),
]);
Assert.Equal(ShowAudience.G, rules.AudienceAt(new TimeOnly(8, 0)));
Assert.Equal(ShowAudience.R, rules.AudienceAt(new TimeOnly(12, 0)));
@@ -90,9 +88,9 @@ public class PlanningRulesTests
// Рейтинг уезжает в jsonb шаблона и в API ровно тем написанием, каким приходит от источников.
// Round-trip этого не поймает: он одинаково зелёный и на «Pg13», а такое значение потом
// придётся переводить на каждой границе.
var json = new PlanningRules(
[new AudienceWindow(new TimeOnly(6, 0), new TimeOnly(23, 0), ShowAudience.Pg13)]
).ToJson();
var json = new PlanningRules([
new AudienceWindow(new TimeOnly(6, 0), new TimeOnly(23, 0), ShowAudience.Pg13),
]).ToJson();
Assert.Contains("\"PG-13\"", json, StringComparison.Ordinal);
}
@@ -10,7 +10,6 @@ namespace TeleWave.Application.Tests.Validators;
public class ValidatorTests
{
[Fact]
public void UpdateBumperTextVariant_ChecksLengthsAndWeight()
{
@@ -55,7 +54,6 @@ public class ValidatorTests
Assert.False(v.Validate(good with { Name = "" }).IsValid);
}
[Fact]
public void ResetUserPassword_RequiresMinLength()
{
@@ -67,7 +67,16 @@ public class MediaAssetTests
Assert.Null(asset.ProcessingDuration);
asset.MarkReady(
new MediaReadyInfo(TimeSpan.FromSeconds(120), 2, 60, 1920, 1080, "h264", "aac", "assets/abc")
new MediaReadyInfo(
TimeSpan.FromSeconds(120),
2,
60,
1920,
1080,
"h264",
"aac",
"assets/abc"
)
);
Assert.NotNull(asset.ProcessingDuration);
@@ -80,7 +89,16 @@ public class MediaAssetTests
var asset = MediaAsset.Register("a.mp4", ".mp4", MediaSource.Upload);
asset.MarkReady(
new MediaReadyInfo(TimeSpan.FromSeconds(120), 2, 60, 1920, 1080, "h264", "aac", "assets/abc")
new MediaReadyInfo(
TimeSpan.FromSeconds(120),
2,
60,
1920,
1080,
"h264",
"aac",
"assets/abc"
)
);
Assert.Null(asset.ProcessingDuration);
@@ -125,7 +125,11 @@ public class CandidateFilterTests
var teen = Element(ShowAudience.Pg13, position: 1);
var pick = ElementSelector.Select(
Slot([adult, teen], maxAudience: ShowAudience.Pg13, strategy: SlotStrategyKind.Sequential),
Slot(
[adult, teen],
maxAudience: ShowAudience.Pg13,
strategy: SlotStrategyKind.Sequential
),
T0,
new FirstAlways()
);
@@ -193,9 +197,16 @@ public class CandidateFilterTests
lastPlayed: T0.AddDays(-10),
recentPlays: [T0.AddDays(-10), T0.AddDays(-9), T0.AddDays(-8)]
);
var recent = Element(position: 1, lastPlayed: T0.AddHours(-1), recentPlays: [T0.AddHours(-1)]);
var recent = Element(
position: 1,
lastPlayed: T0.AddHours(-1),
recentPlays: [T0.AddHours(-1)]
);
var slot = Slot([overCap, recent], repeatLimit: new RepeatLimit(WindowDays: 30, Max: 2)) with
var slot = Slot(
[overCap, recent],
repeatLimit: new RepeatLimit(WindowDays: 30, Max: 2)
) with
{
Strategy = new PlanningStrategy(SlotStrategyKind.RandomWithCooldown, CooldownDays: 2),
};
@@ -1,10 +1,10 @@
using System.Globalization;
using System.Text;
using TeleWave.Domain.Broadcast.Scheduling;
using GridPlanner = TeleWave.Domain.Programming.Planning.SchedulePlanner;
using TeleWave.Domain.Programming;
using TeleWave.Domain.Programming.Planning;
using Xunit;
using GridPlanner = TeleWave.Domain.Programming.Planning.SchedulePlanner;
namespace TeleWave.Domain.Tests.Programming;
@@ -43,9 +43,10 @@ public class GoldenScheduleTests
.Items.OrderBy(i => i.StartsAtUtc)
.Select(item =>
{
var label = item.ShowId is { } showId && names.TryGetValue(showId, out var name)
? $"{name}#{item.UnitIndex}"
: item.Kind.ToString();
var label =
item.ShowId is { } showId && names.TryGetValue(showId, out var name)
? $"{name}#{item.UnitIndex}"
: item.Kind.ToString();
return string.Create(
CultureInfo.InvariantCulture,
$"{item.StartsAtUtc:HH:mm} {label}"
@@ -60,10 +61,7 @@ public class GoldenScheduleTests
.Range(0, episodes)
.Select(i => new PlanningUnit(Guid.NewGuid(), TimeSpan.FromMinutes(minutes), showId, i))
.ToList();
return (
new PlanningElement(GroupElementKind.Show, Guid.NewGuid(), 1, 0, units),
showId
);
return (new PlanningElement(GroupElementKind.Show, Guid.NewGuid(), 1, 0, units), showId);
}
private static PlanningSlot ContentSlot(
@@ -129,12 +127,7 @@ public class GoldenScheduleTests
.ToList();
Assert.Equal(
[
"07:00 Мультфильмы#0",
"07:20 Мультфильмы#1",
"07:40 Мультфильмы#2",
"20:00 Кино#0",
],
["07:00 Мультфильмы#0", "07:20 Мультфильмы#1", "07:40 Мультфильмы#2", "20:00 Кино#0"],
tape
);
}
@@ -148,9 +141,7 @@ public class GoldenScheduleTests
// и разбег накапливается, пока его не подберёт следующий целевой старт.
var slots = Enumerable
.Range(0, 3)
.Select(i =>
ContentSlot(Day.AddHours(i), 60, [series], SlotBlockMode.Count, 2)
)
.Select(i => ContentSlot(Day.AddHours(i), 60, [series], SlotBlockMode.Count, 2))
.ToArray();
var input = new PlanningInput(
@@ -22,10 +22,7 @@ public class JunctionFillerTests
{
showId = Guid.NewGuid();
var id = showId;
var units = Enumerable
.Range(0, episodes)
.Select(i => Unit(minutes, id, i))
.ToList();
var units = Enumerable.Range(0, episodes).Select(i => Unit(minutes, id, i)).ToList();
return new PlanningElement(GroupElementKind.Show, Guid.NewGuid(), 1, 0, units);
}
@@ -116,7 +116,10 @@ public class SchedulePlannerTests
{
var element = Element(10, 20);
var result = Run(
Input([Slot(T0, 300, [element], SlotBlockMode.Duration, blockValue: 50)], horizonHours: 2)
Input(
[Slot(T0, 300, [element], SlotBlockMode.Duration, blockValue: 50)],
horizonHours: 2
)
);
// 20+20 < 50, третья добирает до 60 — обрезать видеофайл нельзя.
@@ -60,7 +60,9 @@ public sealed class GridScheduleGeneratorIntegrationTests(PostgresFixture fixtur
Assert.True(state.NextUnitIndex > 0);
// И шаблон помечен применённым: баннер «правила изменены» должен погаснуть.
Assert.False(verify.ScheduleTemplates.Single(t => t.Id == world.TemplateId).HasPendingChanges);
Assert.False(
verify.ScheduleTemplates.Single(t => t.Id == world.TemplateId).HasPendingChanges
);
}
[SkippableFact]
@@ -154,7 +156,9 @@ public sealed class GridScheduleGeneratorIntegrationTests(PostgresFixture fixtur
Assert.Empty(verify.ScheduleEntries.Where(e => e.ChannelId == world.ChannelId));
Assert.Empty(verify.SlotStates.Where(s => s.SlotId == world.SlotId));
// Отметку о применении сухой прогон тоже не ставит: применять по-прежнему есть что.
Assert.True(verify.ScheduleTemplates.Single(t => t.Id == world.TemplateId).HasPendingChanges);
Assert.True(
verify.ScheduleTemplates.Single(t => t.Id == world.TemplateId).HasPendingChanges
);
}
[SkippableFact]
@@ -29,12 +29,10 @@ public sealed class ManualInboxIntegrationTests(PostgresFixture fixture)
var storage = Substitute.For<IMediaStorage>();
storage
.ListManualInbox(Arg.Any<int>())
.Returns(
[
new IMediaStorage.ManualInboxFile($"Сериал/{first}", first, 1000),
new IMediaStorage.ManualInboxFile($"Сериал/{second}", second, 1000),
]
);
.Returns([
new IMediaStorage.ManualInboxFile($"Сериал/{first}", first, 1000),
new IMediaStorage.ManualInboxFile($"Сериал/{second}", second, 1000),
]);
await using var db = fixture.CreateContext();
var result = await new ImportManualInboxCommandHandler(
@@ -101,12 +99,10 @@ public sealed class ManualInboxIntegrationTests(PostgresFixture fixture)
var storage = Substitute.For<IMediaStorage>();
storage
.ListManualInbox(Arg.Any<int>())
.Returns(
[
new IMediaStorage.ManualInboxFile(good, good, 1000),
new IMediaStorage.ManualInboxFile("readme.txt", "readme.txt", 10),
]
);
.Returns([
new IMediaStorage.ManualInboxFile(good, good, 1000),
new IMediaStorage.ManualInboxFile("readme.txt", "readme.txt", 10),
]);
await using var db = fixture.CreateContext();
var result = await new ImportManualInboxCommandHandler(
@@ -148,7 +144,10 @@ public sealed class ManualInboxIntegrationTests(PostgresFixture fixture)
db,
storage,
Substitute.For<IMediaProcessingQueue>()
).Handle(new ImportManualInboxCommand([new ManualImportItem(name, 4, 12)], showId), default);
).Handle(
new ImportManualInboxCommand([new ManualImportItem(name, 4, 12)], showId),
default
);
Assert.True(result.IsSuccess);
await db.SaveChangesAsync();
@@ -147,7 +147,10 @@ public sealed class ManualInboxStorageTests : IDisposable
private void Write(string relativePath)
{
var full = Path.Combine(_paths.ManualDir, relativePath.Replace('/', Path.DirectorySeparatorChar));
var full = Path.Combine(
_paths.ManualDir,
relativePath.Replace('/', Path.DirectorySeparatorChar)
);
Directory.CreateDirectory(Path.GetDirectoryName(full)!);
File.WriteAllText(full, "x");
}
@@ -100,7 +100,11 @@ public sealed class TemplateOperationsIntegrationTests(PostgresFixture fixture)
// Канал из старой ротации: сетки нет, ссылки на неё тоже.
await using var seedDb = fixture.CreateContext();
var suffix = Guid.NewGuid().ToString("N")[..8];
var channel = Channel.Create($"Без сетки {suffix}", $"nogrid-{suffix}", DateTimeOffset.UtcNow);
var channel = Channel.Create(
$"Без сетки {suffix}",
$"nogrid-{suffix}",
DateTimeOffset.UtcNow
);
seedDb.Channels.Add(channel);
await seedDb.SaveChangesAsync();
@@ -131,7 +135,9 @@ public sealed class TemplateOperationsIntegrationTests(PostgresFixture fixture)
}
/// <summary>Два канала: у источника слой со слотом, группой и стыком; у приёмника — пустая сетка.</summary>
private static async Task<(Guid Source, Guid Target, Guid GroupId)> SeedPairAsync(AppDbContext db)
private static async Task<(Guid Source, Guid Target, Guid GroupId)> SeedPairAsync(
AppDbContext db
)
{
var suffix = Guid.NewGuid().ToString("N")[..8];
@@ -53,7 +53,16 @@ public sealed class TransactionIntegrationTests(PostgresFixture fixture)
var show = Show.Create("Show", ShowKind.Series);
var asset = MediaAsset.Register("ep.mkv", ".mkv", MediaSource.Upload);
asset.MarkReady(
new MediaReadyInfo(TimeSpan.FromMinutes(20), 2, 600, 1920, 1080, "h264", "aac", "assets/x")
new MediaReadyInfo(
TimeSpan.FromMinutes(20),
2,
600,
1920,
1080,
"h264",
"aac",
"assets/x"
)
);
show.AddEpisode(asset.Id);
var entry = ScheduleEntry.Program(