Files
TeleWave/backend/tests/TeleWave.Application.Tests/Metadata/RefreshEpisodesTests.cs
T
Leonid Pershin 5032614773
ci / build-backend (push) Successful in 5m2s
ci / build-frontend (push) Successful in 55s
ci / tests (push) Successful in 1m44s
ci / sonar (push) Successful in 4m31s
Enhance logging in RefreshShowEpisodesMetadataCommandHandler and TmdbMetadataProvider for image retrieval failures
Updated the RefreshShowEpisodesMetadataCommandHandler to include logging for scenarios where episode still images are not retrieved successfully. Introduced warning logs for missing still URLs and download failures, improving traceability. Additionally, modified the TmdbMetadataProvider to implement a fallback mechanism for episode still images, ensuring that appropriate imagery is displayed even when localized images are unavailable. Updated related tests to verify the correct behavior of these logging enhancements and fallback logic.
2026-07-30 02:08:50 +03:00

274 lines
11 KiB
C#

using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging.Abstractions;
using NSubstitute;
using TeleWave.Application.Common.Interfaces;
using TeleWave.Application.Metadata;
using TeleWave.Application.Metadata.RefreshEpisodes;
using TeleWave.Application.Tests.Support;
using TeleWave.Domain.Library;
using TeleWave.Domain.Media;
using Xunit;
namespace TeleWave.Application.Tests.Metadata;
/// <summary>
/// Довыгрузка метаданных серий: что берётся за номер серии, что происходит с кадром и когда
/// операция отказывается работать вовсе.
/// </summary>
public class RefreshEpisodesTests
{
private static EpisodeMetadata Meta(string title, string? stillUrl = null) =>
new(title, "Описание", stillUrl, new DateOnly(2005, 3, 20));
private static (IMetadataProviderResolver Resolver, IMetadataProvider Provider) ProviderFor(
string key
)
{
var provider = Substitute.For<IMetadataProvider>();
var resolver = Substitute.For<IMetadataProviderResolver>();
resolver.Resolve(key).Returns(provider);
return (resolver, provider);
}
private static RefreshShowEpisodesMetadataCommandHandler Handler(
IAppDbContext db,
IMetadataProviderResolver resolver,
IImageDownloader? downloader = null,
IImageStore? store = null
) =>
new(
db,
resolver,
downloader ?? Substitute.For<IImageDownloader>(),
store ?? Substitute.For<IImageStore>(),
NullLogger<RefreshShowEpisodesMetadataCommandHandler>.Instance
);
/// <summary>Сериал с привязкой к источнику и одной серией на готовом ассете.</summary>
private static (Show Show, MediaAsset Asset) Linked(string fileName, bool withNumbers)
{
var asset = MediaAsset.Register(fileName, ".mkv", MediaSource.Upload);
var show = Show.Create("Симпсоны", ShowKind.Series);
show.ApplyMetadata("tmdb", "456", null, 2005, null);
var episode = show.AddEpisode(asset.Id);
if (withNumbers)
episode.SetNumbers(16, 21);
return (show, asset);
}
[Fact]
public async Task AppliesMetadata_AndSavesStillToRegistry()
{
var fixture = new TestDb();
var (show, asset) = Linked("s16e21.mkv", withNumbers: true);
await using (var seed = fixture.New())
{
seed.MediaAssets.Add(asset);
seed.Shows.Add(show);
await seed.SaveChangesAsync(CancellationToken.None);
}
var (resolver, provider) = ProviderFor("tmdb");
provider
.GetEpisodeAsync("456", 16, 21, Arg.Any<CancellationToken>())
.Returns(Meta("The Father, the Son", "https://img/still.jpg"));
var downloader = Substitute.For<IImageDownloader>();
downloader
.DownloadAsync("https://img/still.jpg", Arg.Any<CancellationToken>())
.Returns(new DownloadedImage([1, 2, 3], ".jpg"));
var store = Substitute.For<IImageStore>();
await using var db = fixture.New();
var result = await Handler(db, resolver, downloader, store)
.Handle(new RefreshShowEpisodesMetadataCommand(show.Id), CancellationToken.None);
await db.SaveChangesAsync(CancellationToken.None);
Assert.True(result.IsSuccess);
Assert.Equal(1, result.Value);
await using var check = fixture.New();
var stored = await check.Shows.Include(s => s.Episodes).SingleAsync(CancellationToken.None);
var episode = stored.Episodes.Single();
Assert.Equal("The Father, the Son", episode.Title);
Assert.NotNull(episode.StillImageId);
// Кадр попадает в общий реестр изображений, а не в своё файловое поле.
Assert.Equal(1, await check.Images.CountAsync(CancellationToken.None));
await store
.Received(1)
.SaveAsync(
episode.StillImageId!.Value,
".jpg",
Arg.Any<byte[]>(),
Arg.Any<CancellationToken>()
);
}
[Fact]
public async Task TakesNumbersFromFileName_WhenNotSet()
{
var fixture = new TestDb();
var (show, asset) = Linked("Simpsons.S16E21.1080p.mkv", withNumbers: false);
await using (var seed = fixture.New())
{
seed.MediaAssets.Add(asset);
seed.Shows.Add(show);
await seed.SaveChangesAsync(CancellationToken.None);
}
var (resolver, provider) = ProviderFor("tmdb");
provider
.GetEpisodeAsync("456", 16, 21, Arg.Any<CancellationToken>())
.Returns(Meta("Разобрано по имени"));
await using var db = fixture.New();
var result = await Handler(db, resolver)
.Handle(new RefreshShowEpisodesMetadataCommand(show.Id), CancellationToken.None);
await db.SaveChangesAsync(CancellationToken.None);
Assert.Equal(1, result.Value);
await using var check = fixture.New();
var episode = (await check.Shows.Include(s => s.Episodes).SingleAsync()).Episodes.Single();
// Разобранные номера тут же сохраняются: второй раз имя файла разбирать незачем.
Assert.Equal(16, episode.Season);
Assert.Equal(21, episode.Episode);
}
[Fact]
public async Task SkipsEpisode_WhenNumbersUnknownOrSourceSilent()
{
var fixture = new TestDb();
var unparsable = MediaAsset.Register("случайное имя.mkv", ".mkv", MediaSource.Upload);
var known = MediaAsset.Register("s01e02.mkv", ".mkv", MediaSource.Upload);
var show = Show.Create("Сериал", ShowKind.Series);
show.ApplyMetadata("tmdb", "1", null, null, null);
show.AddEpisode(unparsable.Id);
show.AddEpisode(known.Id);
await using (var seed = fixture.New())
{
seed.MediaAssets.AddRange(unparsable, known);
seed.Shows.Add(show);
await seed.SaveChangesAsync(CancellationToken.None);
}
var (resolver, provider) = ProviderFor("tmdb");
// Источник знает не всё: на распознанную серию он отвечает пустотой.
provider
.GetEpisodeAsync("1", Arg.Any<int>(), Arg.Any<int>(), Arg.Any<CancellationToken>())
.Returns((EpisodeMetadata?)null);
await using var db = fixture.New();
var result = await Handler(db, resolver)
.Handle(new RefreshShowEpisodesMetadataCommand(show.Id), CancellationToken.None);
Assert.True(result.IsSuccess);
Assert.Equal(0, result.Value);
// К нераспознанной серии источник не ходит вовсе — незачем.
await provider
.Received(1)
.GetEpisodeAsync("1", Arg.Any<int>(), Arg.Any<int>(), Arg.Any<CancellationToken>());
}
[Fact]
public async Task KeepsMetadata_WhenStillCannotBeDownloaded()
{
var fixture = new TestDb();
var (show, asset) = Linked("s01e01.mkv", withNumbers: true);
await using (var seed = fixture.New())
{
seed.MediaAssets.Add(asset);
seed.Shows.Add(show);
await seed.SaveChangesAsync(CancellationToken.None);
}
var (resolver, provider) = ProviderFor("tmdb");
provider
.GetEpisodeAsync("456", Arg.Any<int>(), Arg.Any<int>(), Arg.Any<CancellationToken>())
.Returns(Meta("Без кадра", "https://img/broken.jpg"));
var downloader = Substitute.For<IImageDownloader>();
downloader
.DownloadAsync(Arg.Any<string>(), Arg.Any<CancellationToken>())
.Returns((DownloadedImage?)null);
await using var db = fixture.New();
var result = await Handler(db, resolver, downloader)
.Handle(new RefreshShowEpisodesMetadataCommand(show.Id), CancellationToken.None);
await db.SaveChangesAsync(CancellationToken.None);
// Недоступный кадр не повод отменять текст: серия обновлена, картинки просто нет.
Assert.Equal(1, result.Value);
await using var check = fixture.New();
var episode = (await check.Shows.Include(s => s.Episodes).SingleAsync()).Episodes.Single();
Assert.Equal("Без кадра", episode.Title);
Assert.Null(episode.StillImageId);
Assert.Equal(0, await check.Images.CountAsync(CancellationToken.None));
}
[Fact]
public async Task Fails_WhenShowIsMissingOrNotSeriesOrUnlinked()
{
var fixture = new TestDb();
var movie = Show.Create("Фильм", ShowKind.Single);
var unlinked = Show.Create("Сериал без источника", ShowKind.Series);
await using (var seed = fixture.New())
{
seed.Shows.AddRange(movie, unlinked);
await seed.SaveChangesAsync(CancellationToken.None);
}
var (resolver, _) = ProviderFor("tmdb");
await using var db = fixture.New();
var handler = Handler(db, resolver);
var missing = await handler.Handle(
new RefreshShowEpisodesMetadataCommand(Guid.NewGuid()),
CancellationToken.None
);
var notSeries = await handler.Handle(
new RefreshShowEpisodesMetadataCommand(movie.Id),
CancellationToken.None
);
var noSource = await handler.Handle(
new RefreshShowEpisodesMetadataCommand(unlinked.Id),
CancellationToken.None
);
Assert.False(missing.IsSuccess);
Assert.Equal(MetadataErrors.EpisodesForSeriesOnly, notSeries.Error);
Assert.Equal(MetadataErrors.NoLinkedSource, noSource.Error);
}
[Fact]
public async Task Fails_WhenLinkedProviderIsNotConfigured()
{
var fixture = new TestDb();
var show = Show.Create("Сериал", ShowKind.Series);
show.ApplyMetadata("omdb", "tt1", null, null, null);
await using (var seed = fixture.New())
{
seed.Shows.Add(show);
await seed.SaveChangesAsync(CancellationToken.None);
}
var resolver = Substitute.For<IMetadataProviderResolver>();
// Явно: NSubstitute сам подставляет заглушку вместо интерфейсного null.
resolver.Resolve(Arg.Any<string>()).Returns((IMetadataProvider?)null);
await using var db = fixture.New();
var result = await Handler(db, resolver)
.Handle(new RefreshShowEpisodesMetadataCommand(show.Id), CancellationToken.None);
Assert.Equal(MetadataErrors.ProviderNotAvailable, result.Error);
}
}