Refactor media import components for improved user experience and layout consistency
Updated the ManualInboxPanel, MediaImportDialog, MovieImportPanel, and UploadToShowPanel to enhance their layout and responsiveness. Adjusted flex properties for consistent sizing and overflow handling. Simplified the MediaImportDialog by removing unnecessary file handling logic and improved localization strings for clarity. These changes aim to streamline user interactions and maintain a cohesive design across the media import interface.
This commit is contained in:
@@ -0,0 +1,182 @@
|
||||
using NSubstitute;
|
||||
using TeleWave.Application.Common.Interfaces;
|
||||
using TeleWave.Application.Library.Genres;
|
||||
using TeleWave.Application.Media.ManualInbox;
|
||||
using TeleWave.Application.Media.MovieImport;
|
||||
using TeleWave.Application.Metadata;
|
||||
using TeleWave.Application.Tests.Support;
|
||||
using TeleWave.Domain.Library;
|
||||
using TeleWave.Domain.Media;
|
||||
using Xunit;
|
||||
|
||||
namespace TeleWave.Application.Tests.Media;
|
||||
|
||||
/// <summary>
|
||||
/// Пути разбора фильмов, где всё идёт не по-хорошему: источник не настроен, ассет пропал, файл уже
|
||||
/// в чужом шоу. Каждый из них должен быть тихим отказом, а не исключением: разбор пакетный.
|
||||
/// </summary>
|
||||
public class MovieImportHandlerTests
|
||||
{
|
||||
private const string Provider = "tmdb";
|
||||
|
||||
private static IMetadataProviderResolver EmptyResolver()
|
||||
{
|
||||
var resolver = Substitute.For<IMetadataProviderResolver>();
|
||||
resolver.AvailableKeys.Returns([]);
|
||||
// Явный null: без него NSubstitute подсунул бы автозаглушку провайдера, и «источник
|
||||
// не настроен» перестало бы быть тем, что проверяет тест.
|
||||
resolver.Resolve(Arg.Any<string>()).Returns((IMetadataProvider?)null);
|
||||
return resolver;
|
||||
}
|
||||
|
||||
private static ShowMetadataApplier Applier(
|
||||
IAppDbContext db,
|
||||
IMetadataProviderResolver resolver
|
||||
) =>
|
||||
new(
|
||||
db,
|
||||
resolver,
|
||||
Substitute.For<IImageDownloader>(),
|
||||
Substitute.For<IImageStore>(),
|
||||
new GenreMatcher(db)
|
||||
);
|
||||
|
||||
[Fact]
|
||||
public async Task Match_ReturnsNothing_WhenProviderIsNotConfigured()
|
||||
{
|
||||
// Ключа источника нет — искать нечем, и это не ошибка: таблица разбора просто пуста.
|
||||
var fixture = new TestDb();
|
||||
await using var db = fixture.New();
|
||||
var matcher = new MovieMatcher(db, EmptyResolver());
|
||||
|
||||
var rows = await new MatchMoviesQueryHandler(matcher).Handle(
|
||||
new MatchMoviesQuery(["Gladiator.2000.mkv"], Provider),
|
||||
CancellationToken.None
|
||||
);
|
||||
|
||||
Assert.Empty(rows);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task MatchOne_ReturnsNotFound_WhenProviderIsNotConfigured()
|
||||
{
|
||||
var fixture = new TestDb();
|
||||
await using var db = fixture.New();
|
||||
var matcher = new MovieMatcher(db, EmptyResolver());
|
||||
|
||||
var row = await new MatchMovieQueryHandler(matcher).Handle(
|
||||
new MatchMovieQuery("file.mkv", "Гладиатор", 2000, Provider),
|
||||
CancellationToken.None
|
||||
);
|
||||
|
||||
Assert.Equal(MovieMatchStatus.NotFound, row.Status);
|
||||
Assert.Equal("file.mkv", row.Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Import_ReportsMissingAsset()
|
||||
{
|
||||
var fixture = new TestDb();
|
||||
await using var db = fixture.New();
|
||||
var storage = Substitute.For<IMediaStorage>();
|
||||
storage.ListManualInbox(Arg.Any<int>()).Returns([]);
|
||||
|
||||
var result = await new ImportMoviesCommandHandler(
|
||||
db,
|
||||
new ManualInboxTaker(db, storage),
|
||||
Substitute.For<IMediaProcessingQueue>(),
|
||||
Applier(db, EmptyResolver())
|
||||
).Handle(
|
||||
new ImportMoviesCommand(
|
||||
[new MovieImportItem(null, Guid.NewGuid(), "Пропавший", null, null)],
|
||||
Provider
|
||||
),
|
||||
CancellationToken.None
|
||||
);
|
||||
|
||||
Assert.True(result.IsSuccess);
|
||||
Assert.Equal(0, result.Value.Created);
|
||||
Assert.Single(result.Value.Failed);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AutoAttach_DoesNothing_WhenProviderIsNotConfigured()
|
||||
{
|
||||
var fixture = new TestDb();
|
||||
var asset = MediaAsset.Register("a.mkv", ".mkv", MediaSource.Inbox);
|
||||
await using (var seed = fixture.New())
|
||||
{
|
||||
seed.MediaAssets.Add(asset);
|
||||
await seed.SaveChangesAsync(CancellationToken.None);
|
||||
}
|
||||
|
||||
await using var db = fixture.New();
|
||||
var resolver = EmptyResolver();
|
||||
var result = await new AutoAttachMovieCommandHandler(
|
||||
db,
|
||||
resolver,
|
||||
new MovieMatcher(db, resolver),
|
||||
Applier(db, resolver)
|
||||
).Handle(new AutoAttachMovieCommand(asset.Id, "a.mkv"), CancellationToken.None);
|
||||
|
||||
Assert.True(result.IsSuccess);
|
||||
Assert.False(result.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AutoAttach_FailsOnMissingAsset()
|
||||
{
|
||||
var fixture = new TestDb();
|
||||
await using var db = fixture.New();
|
||||
|
||||
var provider = Substitute.For<IMetadataProvider>();
|
||||
var resolver = Substitute.For<IMetadataProviderResolver>();
|
||||
resolver.AvailableKeys.Returns([Provider]);
|
||||
resolver.Resolve(Provider).Returns(provider);
|
||||
|
||||
var result = await new AutoAttachMovieCommandHandler(
|
||||
db,
|
||||
resolver,
|
||||
new MovieMatcher(db, resolver),
|
||||
Applier(db, resolver)
|
||||
).Handle(new AutoAttachMovieCommand(Guid.NewGuid(), "a.mkv"), CancellationToken.None);
|
||||
|
||||
Assert.False(result.IsSuccess);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AutoAttach_SkipsAssetAlreadyInShow()
|
||||
{
|
||||
// Ассет мог уехать в шоу вручную, пока файл ждал очереди, — второе шоу по нему не заводим.
|
||||
var fixture = new TestDb();
|
||||
var asset = MediaAsset.Register("a.mkv", ".mkv", MediaSource.Inbox);
|
||||
var show = Show.Create("Уже есть", ShowKind.Single);
|
||||
show.AddEpisode(asset.Id);
|
||||
|
||||
await using (var seed = fixture.New())
|
||||
{
|
||||
seed.MediaAssets.Add(asset);
|
||||
seed.Shows.Add(show);
|
||||
await seed.SaveChangesAsync(CancellationToken.None);
|
||||
}
|
||||
|
||||
await using var db = fixture.New();
|
||||
var provider = Substitute.For<IMetadataProvider>();
|
||||
var resolver = Substitute.For<IMetadataProviderResolver>();
|
||||
resolver.AvailableKeys.Returns([Provider]);
|
||||
resolver.Resolve(Provider).Returns(provider);
|
||||
|
||||
var result = await new AutoAttachMovieCommandHandler(
|
||||
db,
|
||||
resolver,
|
||||
new MovieMatcher(db, resolver),
|
||||
Applier(db, resolver)
|
||||
).Handle(new AutoAttachMovieCommand(asset.Id, "a.mkv"), CancellationToken.None);
|
||||
|
||||
Assert.True(result.IsSuccess);
|
||||
Assert.False(result.Value);
|
||||
await provider
|
||||
.DidNotReceive()
|
||||
.SearchShowsAsync(Arg.Any<string>(), Arg.Any<ShowKind>(), Arg.Any<CancellationToken>());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
using TeleWave.Application.Media.MovieImport;
|
||||
using Xunit;
|
||||
|
||||
namespace TeleWave.Application.Tests.Media;
|
||||
|
||||
/// <summary>
|
||||
/// Валидаторы разбора фильмов. Проверяют вход до хендлера, поэтому важны обе стороны: пропустить
|
||||
/// нормальную строку и не пропустить ту, что уронила бы разбор или ушла бы во внешний источник.
|
||||
/// </summary>
|
||||
public class MovieImportValidatorTests
|
||||
{
|
||||
private static MovieImportItem FromManual(string? title = "Гладиатор") =>
|
||||
new("Gladiator.2000.mkv", null, title!, 2000, "98");
|
||||
|
||||
private static MovieImportItem Uploaded() => new(null, Guid.NewGuid(), "Гладиатор", 2000, null);
|
||||
|
||||
[Fact]
|
||||
public void Match_AcceptsNormalBatch()
|
||||
{
|
||||
var command = new MatchMoviesQuery(["a.mkv", "b.mkv"], "tmdb");
|
||||
|
||||
Assert.True(new MatchMoviesQueryValidator().Validate(command).IsValid);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(0)] // пустой список разбирать нечего
|
||||
[InlineData(201)] // каждая строка — запрос во внешний источник, и пачка ограничена
|
||||
public void Match_RejectsBadBatchSize(int count)
|
||||
{
|
||||
var names = Enumerable.Range(0, count).Select(i => $"{i}.mkv").ToList();
|
||||
|
||||
Assert.False(
|
||||
new MatchMoviesQueryValidator().Validate(new MatchMoviesQuery(names, "tmdb")).IsValid
|
||||
);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Match_RejectsMissingProvider()
|
||||
{
|
||||
Assert.False(
|
||||
new MatchMoviesQueryValidator().Validate(new MatchMoviesQuery(["a.mkv"], "")).IsValid
|
||||
);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("Шоу Трумана", 1998, true)]
|
||||
[InlineData("Шоу Трумана", null, true)] // год необязателен: в имени его часто нет
|
||||
[InlineData("", 1998, false)] // искать нечего
|
||||
[InlineData("Кино", 1800, false)] // раньше кинематографа
|
||||
[InlineData("Кино", 2300, false)]
|
||||
public void MatchOne_ChecksTitleAndYear(string title, int? year, bool valid)
|
||||
{
|
||||
var query = new MatchMovieQuery("file.mkv", title, year, "tmdb");
|
||||
|
||||
Assert.Equal(valid, new MatchMovieQueryValidator().Validate(query).IsValid);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Import_AcceptsBothSources()
|
||||
{
|
||||
var validator = new ImportMoviesCommandValidator();
|
||||
|
||||
Assert.True(validator.Validate(new ImportMoviesCommand([FromManual()], "tmdb")).IsValid);
|
||||
Assert.True(validator.Validate(new ImportMoviesCommand([Uploaded()], "tmdb")).IsValid);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Import_RejectsItemWithoutFile()
|
||||
{
|
||||
// Без файла шоу пустое — заводить его незачем.
|
||||
var item = new MovieImportItem(null, null, "Гладиатор", 2000, null);
|
||||
|
||||
Assert.False(
|
||||
new ImportMoviesCommandValidator()
|
||||
.Validate(new ImportMoviesCommand([item], "tmdb"))
|
||||
.IsValid
|
||||
);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Import_RejectsItemWithTwoSources()
|
||||
{
|
||||
// Путь в manual/ и уже загруженный ассет разом: неясно, какой файл брать.
|
||||
var item = new MovieImportItem("Gladiator.mkv", Guid.NewGuid(), "Гладиатор", 2000, null);
|
||||
|
||||
Assert.False(
|
||||
new ImportMoviesCommandValidator()
|
||||
.Validate(new ImportMoviesCommand([item], "tmdb"))
|
||||
.IsValid
|
||||
);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Import_RejectsEmptyTitle()
|
||||
{
|
||||
Assert.False(
|
||||
new ImportMoviesCommandValidator()
|
||||
.Validate(new ImportMoviesCommand([FromManual(title: " ")], "tmdb"))
|
||||
.IsValid
|
||||
);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Import_RejectsEmptyBatch()
|
||||
{
|
||||
Assert.False(
|
||||
new ImportMoviesCommandValidator().Validate(new ImportMoviesCommand([], "tmdb")).IsValid
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
using TeleWave.Application.Broadcast.Bumpers;
|
||||
using TeleWave.Application.Programming.Templates;
|
||||
using TeleWave.Application.Programming.Templates.Junctions;
|
||||
using TeleWave.Domain.Broadcast;
|
||||
using TeleWave.Domain.Programming;
|
||||
using Xunit;
|
||||
|
||||
namespace TeleWave.Application.Tests.Programming;
|
||||
|
||||
/// <summary>
|
||||
/// Валидаторы стыков и заставок. Проверяют границы, за которыми настройка перестаёт что-либо
|
||||
/// значить в эфире: врезка длиннее суток, допуск от часа шире самого часа, семь строк в кадре.
|
||||
/// </summary>
|
||||
public class JunctionBumperValidatorTests
|
||||
{
|
||||
private static JunctionElementInput Element(
|
||||
int amount = 1,
|
||||
int choiceWeight = 1,
|
||||
string? choiceKey = null,
|
||||
JunctionConditions? conditions = null
|
||||
) =>
|
||||
new(
|
||||
JunctionElementKind.Ad,
|
||||
Guid.NewGuid(),
|
||||
null,
|
||||
null,
|
||||
JunctionAmountMode.Count,
|
||||
amount,
|
||||
IsRequired: false,
|
||||
choiceKey,
|
||||
choiceWeight,
|
||||
conditions
|
||||
);
|
||||
|
||||
private static bool Valid(JunctionElementInput input) =>
|
||||
new UpdateJunctionElementCommandValidator()
|
||||
.Validate(new UpdateJunctionElementCommand(Guid.NewGuid(), Guid.NewGuid(), input))
|
||||
.IsValid;
|
||||
|
||||
[Fact]
|
||||
public void Element_AcceptsUsualSettings()
|
||||
{
|
||||
var conditions = new JunctionConditions(
|
||||
MinMinutesBetween: 30,
|
||||
Chance: 40,
|
||||
NearHourMinutes: 2
|
||||
);
|
||||
|
||||
Assert.True(
|
||||
Valid(Element(amount: 3, choiceWeight: 5, choiceKey: "fork", conditions: conditions))
|
||||
);
|
||||
// Условий может не быть вовсе — тогда проверять нечего.
|
||||
Assert.True(Valid(Element()));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(0)] // ноль врезок — это отсутствие врезки, а не настройка
|
||||
[InlineData(24 * 60 + 1)] // длиннее вещательных суток
|
||||
public void Element_RejectsAmountOutOfRange(int amount)
|
||||
{
|
||||
Assert.False(Valid(Element(amount)));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(-1)]
|
||||
[InlineData(1001)]
|
||||
public void Element_RejectsWeightOutOfRange(int weight)
|
||||
{
|
||||
Assert.False(Valid(Element(choiceWeight: weight)));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(101)] // вероятность выше ста
|
||||
[InlineData(-1)]
|
||||
public void Element_RejectsChanceOutOfRange(int chance)
|
||||
{
|
||||
Assert.False(Valid(Element(conditions: new JunctionConditions(Chance: chance))));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Element_RejectsNearHourWiderThanHalfHour()
|
||||
{
|
||||
// Допуск в 31 минуту покрывает час целиком — это уже не привязка к :00.
|
||||
Assert.False(Valid(Element(conditions: new JunctionConditions(NearHourMinutes: 31))));
|
||||
Assert.False(Valid(Element(conditions: new JunctionConditions(NearHourMinutes: -1))));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Junction_ChecksNameAndCap()
|
||||
{
|
||||
var validator = new UpdateJunctionCommandValidator();
|
||||
var id = Guid.NewGuid();
|
||||
|
||||
Assert.True(
|
||||
validator.Validate(new UpdateJunctionCommand(id, "Рекламный шов", 120)).IsValid
|
||||
);
|
||||
// Потолок можно не задавать — тогда стык ограничен только якорем.
|
||||
Assert.True(validator.Validate(new UpdateJunctionCommand(id, "Шов", null)).IsValid);
|
||||
Assert.False(validator.Validate(new UpdateJunctionCommand(id, " ", 120)).IsValid);
|
||||
Assert.False(validator.Validate(new UpdateJunctionCommand(id, "Шов", 0)).IsValid);
|
||||
Assert.False(
|
||||
validator.Validate(new UpdateJunctionCommand(id, "Шов", 24 * 60 * 60 + 1)).IsValid
|
||||
);
|
||||
Assert.False(
|
||||
new CreateJunctionCommandValidator().Validate(new CreateJunctionCommand("")).IsValid
|
||||
);
|
||||
}
|
||||
|
||||
private static bool ValidVariant(int weight, params string[] lines) =>
|
||||
new UpdateBumperVariantCommandValidator()
|
||||
.Validate(
|
||||
new UpdateBumperVariantCommand(
|
||||
Guid.NewGuid(),
|
||||
Guid.NewGuid(),
|
||||
new BumperVariantInput(
|
||||
"Подблок",
|
||||
BumperTrigger.Both,
|
||||
BumperBackground.Template,
|
||||
weight,
|
||||
[
|
||||
.. lines.Select(text => new BumperLineDto(
|
||||
BumperLineStyle.Title,
|
||||
BumperLineColor.Text,
|
||||
text
|
||||
)),
|
||||
]
|
||||
)
|
||||
)
|
||||
)
|
||||
.IsValid;
|
||||
|
||||
[Fact]
|
||||
public void Variant_AcceptsUpToSixLines()
|
||||
{
|
||||
Assert.True(ValidVariant(1, "СЕЙЧАС", "{now.title}"));
|
||||
Assert.True(ValidVariant(0, "1", "2", "3", "4", "5", "6"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Variant_RejectsSeventhLine()
|
||||
{
|
||||
// Больше шести строк в кадр не помещается ни при каком размере шрифта.
|
||||
Assert.False(ValidVariant(1, "1", "2", "3", "4", "5", "6", "7"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Variant_RejectsEmptyLineAndBadWeight()
|
||||
{
|
||||
Assert.False(ValidVariant(1, " "));
|
||||
Assert.False(ValidVariant(1001, "СЕЙЧАС"));
|
||||
Assert.False(ValidVariant(-1, "СЕЙЧАС"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
using NSubstitute;
|
||||
using TeleWave.Application.Common.Interfaces;
|
||||
using TeleWave.Application.Programming.Groups;
|
||||
using TeleWave.Application.Programming.Templates;
|
||||
using TeleWave.Application.Programming.Templates.Generate;
|
||||
using TeleWave.Application.Programming.Templates.Transfer;
|
||||
using TeleWave.Application.Tests.Support;
|
||||
using TeleWave.Domain.Broadcast;
|
||||
using TeleWave.Domain.Programming;
|
||||
using Xunit;
|
||||
|
||||
namespace TeleWave.Application.Tests.Programming;
|
||||
|
||||
/// <summary>
|
||||
/// Обмен конфигурацией там, где обмениваться нечем: нет канала, нет шаблона, нет источника
|
||||
/// метаданных. Всё это штатные исходы — админ открыл экран на канале, который ещё не настроен.
|
||||
/// </summary>
|
||||
public class TransferGuardsTests
|
||||
{
|
||||
private static readonly DateTimeOffset T0 = new(2026, 1, 1, 0, 0, 0, TimeSpan.Zero);
|
||||
|
||||
private static ExportGridQueryHandler Export(IAppDbContext db) =>
|
||||
new(db, GroupServices.Dynamic(db));
|
||||
|
||||
[Fact]
|
||||
public async Task Export_FailsWithoutChannel()
|
||||
{
|
||||
var fixture = new TestDb();
|
||||
await using var db = fixture.New();
|
||||
|
||||
var result = await Export(db)
|
||||
.Handle(new ExportGridQuery(Guid.NewGuid()), CancellationToken.None);
|
||||
|
||||
Assert.False(result.IsSuccess);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Export_FailsWithoutTemplate()
|
||||
{
|
||||
// Канал без сетки выгружать нечем — и это отдельная ошибка, а не пустой файл.
|
||||
var fixture = new TestDb();
|
||||
var channel = Channel.Create("Первый", "first", 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 Export(db)
|
||||
.Handle(new ExportGridQuery(channel.Id), CancellationToken.None);
|
||||
|
||||
Assert.False(result.IsSuccess);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Import_FailsWithoutTemplate()
|
||||
{
|
||||
var fixture = new TestDb();
|
||||
await using var db = fixture.New();
|
||||
|
||||
var config = new GridConfig([new GridConfigLayer("Основная сетка", 10, [])]);
|
||||
var result = await new ImportGridCommandHandler(
|
||||
db,
|
||||
new SlotWriter(db),
|
||||
GroupServices.Stats(db)
|
||||
).Handle(new ImportGridCommand(Guid.NewGuid(), config, false), CancellationToken.None);
|
||||
|
||||
Assert.False(result.IsSuccess);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Prompt_FailsWithoutChannel()
|
||||
{
|
||||
var fixture = new TestDb();
|
||||
await using var db = fixture.New();
|
||||
|
||||
var result = await new BuildGridPromptQueryHandler(db, Catalog(db)).Handle(
|
||||
new BuildGridPromptQuery(Guid.NewGuid(), [], null),
|
||||
CancellationToken.None
|
||||
);
|
||||
|
||||
Assert.False(result.IsSuccess);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Prompt_SaysWhatIsMissing_OnEmptyLibrary()
|
||||
{
|
||||
// Пустая библиотека — не ошибка: запрос собирается, но честно говорит, что групп нет,
|
||||
// иначе модель придумает их сама и вернёт сетку под несуществующий контент.
|
||||
var fixture = new TestDb();
|
||||
var channel = Channel.Create("Первый", "first", 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 BuildGridPromptQueryHandler(db, Catalog(db)).Handle(
|
||||
new BuildGridPromptQuery(channel.Id, [], null),
|
||||
CancellationToken.None
|
||||
);
|
||||
|
||||
Assert.True(result.IsSuccess);
|
||||
Assert.Equal(0, result.Value.Groups);
|
||||
Assert.Equal(0, result.Value.Shows);
|
||||
Assert.Contains("Готовых групп нет", result.Value.Prompt, StringComparison.Ordinal);
|
||||
Assert.Contains("Библиотека пуста", result.Value.Prompt, StringComparison.Ordinal);
|
||||
// Стыков и заставок тоже нет — модель должна знать, что ссылаться не на что.
|
||||
Assert.Contains("стыков ещё нет", result.Value.Prompt, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
private static GroupCatalog Catalog(IAppDbContext db) =>
|
||||
new(db, GroupServices.Dynamic(db), new GroupElementResolver(db));
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
using TeleWave.Application.Programming.Templates.Transfer;
|
||||
using TeleWave.Domain.Programming;
|
||||
using Xunit;
|
||||
|
||||
namespace TeleWave.Application.Tests.Programming;
|
||||
|
||||
/// <summary>
|
||||
/// Валидаторы обмена конфигурацией. Файл приходит извне — от другой установки или от языковой
|
||||
/// модели, — поэтому проверка формата и объёма здесь единственная преграда между чужим JSON
|
||||
/// и сеткой канала.
|
||||
/// </summary>
|
||||
public class TransferValidatorTests
|
||||
{
|
||||
private static GridConfigSlot Slot() =>
|
||||
new("Вечерний блок", new TimeOnly(20, 0), 120, Daypart.Prime, Group: "Сериалы");
|
||||
|
||||
private static GridConfig Config(params GridConfigLayer[] layers) => new(layers);
|
||||
|
||||
private static GridConfig Valid() =>
|
||||
Config(new GridConfigLayer("Основная сетка", 10, [Slot()]));
|
||||
|
||||
[Fact]
|
||||
public void Import_AcceptsNormalFile()
|
||||
{
|
||||
var command = new ImportGridCommand(Guid.NewGuid(), Valid(), Replace: true);
|
||||
|
||||
Assert.True(new ImportGridCommandValidator().Validate(command).IsValid);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Import_RejectsForeignFormat()
|
||||
{
|
||||
// Чужой формат с теми же полями разобрался бы во что-то бессмысленное — лучше отказать.
|
||||
var config = Valid() with
|
||||
{
|
||||
Format = "some.other.format",
|
||||
};
|
||||
|
||||
Assert.False(Validate(config));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Import_RejectsFutureVersion()
|
||||
{
|
||||
var config = Valid() with { Version = GridConfig.CurrentVersion + 1 };
|
||||
|
||||
Assert.False(Validate(config));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Import_RejectsEmptyAndNamelessLayers()
|
||||
{
|
||||
Assert.False(Validate(Config()));
|
||||
Assert.False(Validate(Config(new GridConfigLayer(" ", 10, [Slot()]))));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Import_RejectsOversizedFile()
|
||||
{
|
||||
// Потолки — про объём: недельная сетка это десятки слотов, а не десятки тысяч.
|
||||
var layers = Enumerable
|
||||
.Range(0, 65)
|
||||
.Select(i => new GridConfigLayer($"Слой {i}", 10, [Slot()]))
|
||||
.ToArray();
|
||||
|
||||
Assert.False(Validate(Config(layers)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Import_RejectsTooManyGroupsAndCollections()
|
||||
{
|
||||
var groups = Enumerable
|
||||
.Range(0, 257)
|
||||
.Select(i => new GridConfigGroup($"Группа {i}"))
|
||||
.ToList();
|
||||
var collections = Enumerable
|
||||
.Range(0, 257)
|
||||
.Select(i => new GridConfigCollection($"Цикл {i}", ["Шоу"]))
|
||||
.ToList();
|
||||
|
||||
Assert.False(Validate(Valid() with { Groups = groups }));
|
||||
Assert.False(Validate(Valid() with { Collections = collections }));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Prompt_AcceptsReferencesAndNotes()
|
||||
{
|
||||
var query = new BuildGridPromptQuery(Guid.NewGuid(), ["2×2"], "утром детское");
|
||||
|
||||
Assert.True(new BuildGridPromptQueryValidator().Validate(query).IsValid);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Prompt_RejectsTooManyReferencesAndEmptyOnes()
|
||||
{
|
||||
var validator = new BuildGridPromptQueryValidator();
|
||||
var many = Enumerable.Range(0, 17).Select(i => $"Канал {i}").ToList();
|
||||
|
||||
Assert.False(
|
||||
validator.Validate(new BuildGridPromptQuery(Guid.NewGuid(), many, null)).IsValid
|
||||
);
|
||||
Assert.False(
|
||||
validator.Validate(new BuildGridPromptQuery(Guid.NewGuid(), [" "], null)).IsValid
|
||||
);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Prompt_RejectsHugeNotes()
|
||||
{
|
||||
var notes = new string('x', 4001);
|
||||
|
||||
Assert.False(
|
||||
new BuildGridPromptQueryValidator()
|
||||
.Validate(new BuildGridPromptQuery(Guid.NewGuid(), [], notes))
|
||||
.IsValid
|
||||
);
|
||||
}
|
||||
|
||||
private static bool Validate(GridConfig config) =>
|
||||
new ImportGridCommandValidator()
|
||||
.Validate(new ImportGridCommand(Guid.NewGuid(), config, Replace: false))
|
||||
.IsValid;
|
||||
}
|
||||
Reference in New Issue
Block a user