Normalize line endings to LF via .gitattributes
Репозиторий хранил фронтенд в CRLF, а часть бэкенда — вперемешку, хотя CI и Docker-сборка работают под Linux. Прибиваем LF атрибутом `* text=auto eol=lf` и разово нормализуем дерево, чтобы форматтеры не переписывали файлы целиком на каждом прогоне. Коммит чисто механический: изменений содержимого нет, только концы строк. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
9d1c6d2fc3
commit
0442056367
@@ -1,176 +1,176 @@
|
||||
using NSubstitute;
|
||||
using TeleWave.Application.Common.Interfaces;
|
||||
using TeleWave.Application.Media.ManualInbox;
|
||||
using TeleWave.Domain.Library;
|
||||
using TeleWave.Domain.Media;
|
||||
using TeleWave.Infrastructure.Persistence;
|
||||
using Xunit;
|
||||
|
||||
namespace TeleWave.Integration.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Ручной разбор <c>manual/</c>: выбранные файлы уходят из каталога в шоу. Проверяется против
|
||||
/// настоящей БД, потому что вся суть операции — в связке «ассет + серия шоу + файл на диске».
|
||||
/// </summary>
|
||||
[Collection("postgres")]
|
||||
public sealed class ManualInboxIntegrationTests(PostgresFixture fixture)
|
||||
{
|
||||
[SkippableFact]
|
||||
public async Task Import_AddsEpisodesToShow_AndConsumesFiles()
|
||||
{
|
||||
Skip.IfNot(fixture.Available, "Docker недоступен");
|
||||
|
||||
await using var seedDb = fixture.CreateContext();
|
||||
var showId = await SeedShowAsync(seedDb);
|
||||
var tag = Guid.NewGuid().ToString("N")[..8];
|
||||
var first = $"{tag}-s01e01.mkv";
|
||||
var second = $"{tag}-s01e02.mkv";
|
||||
|
||||
var storage = Substitute.For<IMediaStorage>();
|
||||
storage
|
||||
.ListManualInbox(Arg.Any<int>())
|
||||
.Returns([
|
||||
new IMediaStorage.ManualInboxFile($"Сериал/{first}", first, 1000),
|
||||
new IMediaStorage.ManualInboxFile($"Сериал/{second}", second, 1000),
|
||||
]);
|
||||
|
||||
await using var db = fixture.CreateContext();
|
||||
var result = await new ImportManualInboxCommandHandler(
|
||||
db,
|
||||
storage,
|
||||
Substitute.For<IMediaProcessingQueue>()
|
||||
).Handle(
|
||||
new ImportManualInboxCommand(
|
||||
[Item($"Сериал/{first}"), Item($"Сериал/{second}")],
|
||||
showId
|
||||
),
|
||||
default
|
||||
);
|
||||
|
||||
Assert.True(result.IsSuccess);
|
||||
Assert.Equal(2, result.Value.Imported);
|
||||
Assert.Empty(result.Value.Failed);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
// Файлы должны быть перенесены из manual/ — ровно как из обычного inbox.
|
||||
await storage
|
||||
.Received(2)
|
||||
.PromoteToOriginalAsync(
|
||||
MediaSource.ManualInbox,
|
||||
Arg.Any<string>(),
|
||||
Arg.Any<Guid>(),
|
||||
".mkv",
|
||||
Arg.Any<CancellationToken>()
|
||||
);
|
||||
|
||||
await using var verify = fixture.CreateContext();
|
||||
var episodes = verify
|
||||
.Shows.Where(s => s.Id == showId)
|
||||
.SelectMany(s => s.Episodes)
|
||||
.OrderBy(e => e.Position)
|
||||
.ToList();
|
||||
|
||||
Assert.Equal(2, episodes.Count);
|
||||
// Номера сезона и серии разбираются из имени файла, как при обычном добавлении.
|
||||
Assert.Equal(1, episodes[0].Season);
|
||||
Assert.Equal(1, episodes[0].Episode);
|
||||
Assert.Equal(2, episodes[1].Episode);
|
||||
|
||||
// Считаем ассеты своего шоу: база в интеграционных тестах общая, и соседний тест тоже
|
||||
// заводит записи из ручного inbox.
|
||||
var assetIds = episodes.Select(e => e.MediaAssetId).ToList();
|
||||
var assets = verify.MediaAssets.Where(a => assetIds.Contains(a.Id)).ToList();
|
||||
Assert.Equal(2, assets.Count);
|
||||
Assert.All(assets, a => Assert.Equal(MediaSource.ManualInbox, a.Source));
|
||||
Assert.All(assets, a => Assert.Equal(MediaAssetStatus.Pending, a.Status));
|
||||
}
|
||||
|
||||
[SkippableFact]
|
||||
public async Task Import_SkipsUnsupportedAndMissing_WithoutFailingTheBatch()
|
||||
{
|
||||
Skip.IfNot(fixture.Available, "Docker недоступен");
|
||||
|
||||
await using var seedDb = fixture.CreateContext();
|
||||
var showId = await SeedShowAsync(seedDb);
|
||||
|
||||
var tag = Guid.NewGuid().ToString("N")[..8];
|
||||
var good = $"{tag}-ok.mkv";
|
||||
|
||||
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),
|
||||
]);
|
||||
|
||||
await using var db = fixture.CreateContext();
|
||||
var result = await new ImportManualInboxCommandHandler(
|
||||
db,
|
||||
storage,
|
||||
Substitute.For<IMediaProcessingQueue>()
|
||||
).Handle(
|
||||
new ImportManualInboxCommand(
|
||||
[Item(good), Item("readme.txt"), Item("ушёл.mkv")],
|
||||
showId
|
||||
),
|
||||
default
|
||||
);
|
||||
|
||||
// Один взяли, два отклонили — и каждый со своей причиной, а не одной ошибкой на пакет.
|
||||
Assert.True(result.IsSuccess);
|
||||
Assert.Equal(1, result.Value.Imported);
|
||||
Assert.Equal(2, result.Value.Failed.Count);
|
||||
Assert.Contains(result.Value.Failed, f => f.RelativePath == "readme.txt");
|
||||
Assert.Contains(result.Value.Failed, f => f.RelativePath == "ушёл.mkv");
|
||||
}
|
||||
|
||||
[SkippableFact]
|
||||
public async Task Import_WithExplicitNumbers_UsesThemInsteadOfFileName()
|
||||
{
|
||||
Skip.IfNot(fixture.Available, "Docker недоступен");
|
||||
|
||||
await using var seedDb = fixture.CreateContext();
|
||||
var showId = await SeedShowAsync(seedDb);
|
||||
var name = $"{Guid.NewGuid():N}"[..8] + "-без-номеров.mkv";
|
||||
|
||||
var storage = Substitute.For<IMediaStorage>();
|
||||
storage
|
||||
.ListManualInbox(Arg.Any<int>())
|
||||
.Returns([new IMediaStorage.ManualInboxFile(name, name, 1000)]);
|
||||
|
||||
await using var db = fixture.CreateContext();
|
||||
var result = await new ImportManualInboxCommandHandler(
|
||||
db,
|
||||
storage,
|
||||
Substitute.For<IMediaProcessingQueue>()
|
||||
).Handle(
|
||||
new ImportManualInboxCommand([new ManualImportItem(name, 4, 12)], showId),
|
||||
default
|
||||
);
|
||||
|
||||
Assert.True(result.IsSuccess);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
await using var verify = fixture.CreateContext();
|
||||
var episode = verify.Shows.Where(s => s.Id == showId).SelectMany(s => s.Episodes).Single();
|
||||
|
||||
// Из имени номера не вытащить — значит сохранилось ровно то, что видел пользователь.
|
||||
Assert.Equal(4, episode.Season);
|
||||
Assert.Equal(12, episode.Episode);
|
||||
|
||||
// И спутники рядом с забранным файлом убираются.
|
||||
await storage.Received(1).CleanupManualLeftoversAsync(name, Arg.Any<CancellationToken>());
|
||||
}
|
||||
|
||||
/// <summary>Файл без явных номеров — сервер разбирает имя сам, как при обычном добавлении.</summary>
|
||||
private static ManualImportItem Item(string relativePath) => new(relativePath, null, null);
|
||||
|
||||
private static async Task<Guid> SeedShowAsync(AppDbContext db)
|
||||
{
|
||||
var show = Show.Create($"Сериал {Guid.NewGuid():N}"[..20], ShowKind.Series);
|
||||
db.Shows.Add(show);
|
||||
await db.SaveChangesAsync();
|
||||
return show.Id;
|
||||
}
|
||||
}
|
||||
using NSubstitute;
|
||||
using TeleWave.Application.Common.Interfaces;
|
||||
using TeleWave.Application.Media.ManualInbox;
|
||||
using TeleWave.Domain.Library;
|
||||
using TeleWave.Domain.Media;
|
||||
using TeleWave.Infrastructure.Persistence;
|
||||
using Xunit;
|
||||
|
||||
namespace TeleWave.Integration.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Ручной разбор <c>manual/</c>: выбранные файлы уходят из каталога в шоу. Проверяется против
|
||||
/// настоящей БД, потому что вся суть операции — в связке «ассет + серия шоу + файл на диске».
|
||||
/// </summary>
|
||||
[Collection("postgres")]
|
||||
public sealed class ManualInboxIntegrationTests(PostgresFixture fixture)
|
||||
{
|
||||
[SkippableFact]
|
||||
public async Task Import_AddsEpisodesToShow_AndConsumesFiles()
|
||||
{
|
||||
Skip.IfNot(fixture.Available, "Docker недоступен");
|
||||
|
||||
await using var seedDb = fixture.CreateContext();
|
||||
var showId = await SeedShowAsync(seedDb);
|
||||
var tag = Guid.NewGuid().ToString("N")[..8];
|
||||
var first = $"{tag}-s01e01.mkv";
|
||||
var second = $"{tag}-s01e02.mkv";
|
||||
|
||||
var storage = Substitute.For<IMediaStorage>();
|
||||
storage
|
||||
.ListManualInbox(Arg.Any<int>())
|
||||
.Returns([
|
||||
new IMediaStorage.ManualInboxFile($"Сериал/{first}", first, 1000),
|
||||
new IMediaStorage.ManualInboxFile($"Сериал/{second}", second, 1000),
|
||||
]);
|
||||
|
||||
await using var db = fixture.CreateContext();
|
||||
var result = await new ImportManualInboxCommandHandler(
|
||||
db,
|
||||
storage,
|
||||
Substitute.For<IMediaProcessingQueue>()
|
||||
).Handle(
|
||||
new ImportManualInboxCommand(
|
||||
[Item($"Сериал/{first}"), Item($"Сериал/{second}")],
|
||||
showId
|
||||
),
|
||||
default
|
||||
);
|
||||
|
||||
Assert.True(result.IsSuccess);
|
||||
Assert.Equal(2, result.Value.Imported);
|
||||
Assert.Empty(result.Value.Failed);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
// Файлы должны быть перенесены из manual/ — ровно как из обычного inbox.
|
||||
await storage
|
||||
.Received(2)
|
||||
.PromoteToOriginalAsync(
|
||||
MediaSource.ManualInbox,
|
||||
Arg.Any<string>(),
|
||||
Arg.Any<Guid>(),
|
||||
".mkv",
|
||||
Arg.Any<CancellationToken>()
|
||||
);
|
||||
|
||||
await using var verify = fixture.CreateContext();
|
||||
var episodes = verify
|
||||
.Shows.Where(s => s.Id == showId)
|
||||
.SelectMany(s => s.Episodes)
|
||||
.OrderBy(e => e.Position)
|
||||
.ToList();
|
||||
|
||||
Assert.Equal(2, episodes.Count);
|
||||
// Номера сезона и серии разбираются из имени файла, как при обычном добавлении.
|
||||
Assert.Equal(1, episodes[0].Season);
|
||||
Assert.Equal(1, episodes[0].Episode);
|
||||
Assert.Equal(2, episodes[1].Episode);
|
||||
|
||||
// Считаем ассеты своего шоу: база в интеграционных тестах общая, и соседний тест тоже
|
||||
// заводит записи из ручного inbox.
|
||||
var assetIds = episodes.Select(e => e.MediaAssetId).ToList();
|
||||
var assets = verify.MediaAssets.Where(a => assetIds.Contains(a.Id)).ToList();
|
||||
Assert.Equal(2, assets.Count);
|
||||
Assert.All(assets, a => Assert.Equal(MediaSource.ManualInbox, a.Source));
|
||||
Assert.All(assets, a => Assert.Equal(MediaAssetStatus.Pending, a.Status));
|
||||
}
|
||||
|
||||
[SkippableFact]
|
||||
public async Task Import_SkipsUnsupportedAndMissing_WithoutFailingTheBatch()
|
||||
{
|
||||
Skip.IfNot(fixture.Available, "Docker недоступен");
|
||||
|
||||
await using var seedDb = fixture.CreateContext();
|
||||
var showId = await SeedShowAsync(seedDb);
|
||||
|
||||
var tag = Guid.NewGuid().ToString("N")[..8];
|
||||
var good = $"{tag}-ok.mkv";
|
||||
|
||||
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),
|
||||
]);
|
||||
|
||||
await using var db = fixture.CreateContext();
|
||||
var result = await new ImportManualInboxCommandHandler(
|
||||
db,
|
||||
storage,
|
||||
Substitute.For<IMediaProcessingQueue>()
|
||||
).Handle(
|
||||
new ImportManualInboxCommand(
|
||||
[Item(good), Item("readme.txt"), Item("ушёл.mkv")],
|
||||
showId
|
||||
),
|
||||
default
|
||||
);
|
||||
|
||||
// Один взяли, два отклонили — и каждый со своей причиной, а не одной ошибкой на пакет.
|
||||
Assert.True(result.IsSuccess);
|
||||
Assert.Equal(1, result.Value.Imported);
|
||||
Assert.Equal(2, result.Value.Failed.Count);
|
||||
Assert.Contains(result.Value.Failed, f => f.RelativePath == "readme.txt");
|
||||
Assert.Contains(result.Value.Failed, f => f.RelativePath == "ушёл.mkv");
|
||||
}
|
||||
|
||||
[SkippableFact]
|
||||
public async Task Import_WithExplicitNumbers_UsesThemInsteadOfFileName()
|
||||
{
|
||||
Skip.IfNot(fixture.Available, "Docker недоступен");
|
||||
|
||||
await using var seedDb = fixture.CreateContext();
|
||||
var showId = await SeedShowAsync(seedDb);
|
||||
var name = $"{Guid.NewGuid():N}"[..8] + "-без-номеров.mkv";
|
||||
|
||||
var storage = Substitute.For<IMediaStorage>();
|
||||
storage
|
||||
.ListManualInbox(Arg.Any<int>())
|
||||
.Returns([new IMediaStorage.ManualInboxFile(name, name, 1000)]);
|
||||
|
||||
await using var db = fixture.CreateContext();
|
||||
var result = await new ImportManualInboxCommandHandler(
|
||||
db,
|
||||
storage,
|
||||
Substitute.For<IMediaProcessingQueue>()
|
||||
).Handle(
|
||||
new ImportManualInboxCommand([new ManualImportItem(name, 4, 12)], showId),
|
||||
default
|
||||
);
|
||||
|
||||
Assert.True(result.IsSuccess);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
await using var verify = fixture.CreateContext();
|
||||
var episode = verify.Shows.Where(s => s.Id == showId).SelectMany(s => s.Episodes).Single();
|
||||
|
||||
// Из имени номера не вытащить — значит сохранилось ровно то, что видел пользователь.
|
||||
Assert.Equal(4, episode.Season);
|
||||
Assert.Equal(12, episode.Episode);
|
||||
|
||||
// И спутники рядом с забранным файлом убираются.
|
||||
await storage.Received(1).CleanupManualLeftoversAsync(name, Arg.Any<CancellationToken>());
|
||||
}
|
||||
|
||||
/// <summary>Файл без явных номеров — сервер разбирает имя сам, как при обычном добавлении.</summary>
|
||||
private static ManualImportItem Item(string relativePath) => new(relativePath, null, null);
|
||||
|
||||
private static async Task<Guid> SeedShowAsync(AppDbContext db)
|
||||
{
|
||||
var show = Show.Create($"Сериал {Guid.NewGuid():N}"[..20], ShowKind.Series);
|
||||
db.Shows.Add(show);
|
||||
await db.SaveChangesAsync();
|
||||
return show.Id;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user