Files
TeleWave/backend/tests/TeleWave.Application.Tests/Metadata/TmdbMetadataProviderTests.cs
T
Leonid Pershin 1758a93011
ci / build-backend (push) Successful in 1m40s
ci / build-frontend (push) Successful in 54s
ci / tests (push) Successful in 1m44s
ci / sonar (push) Successful in 4m57s
Enhance TmdbMetadataProvider to support fallback for localized poster retrieval
Updated the TmdbMetadataProvider class to implement a fallback mechanism for obtaining show posters when localized images are unavailable. Introduced a new method, NeutralPosterPathAsync, which retrieves a language-neutral poster if the localized one is missing. Additionally, updated related tests to verify the correct behavior of this fallback logic, ensuring that shows without localized posters still display appropriate imagery.
2026-07-30 01:54:16 +03:00

276 lines
10 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using Microsoft.Extensions.Options;
using TeleWave.Application.Tests.TestSupport;
using TeleWave.Domain.Library;
using TeleWave.Infrastructure.Metadata;
using Xunit;
namespace TeleWave.Application.Tests.Metadata;
/// <summary>
/// Разбор ответов TMDb. Наружу тесты не ходят — HTTP подменён, проверяем и собранный адрес
/// (сериалы и фильмы живут в разных пространствах API), и то, что из ответа вычитано.
/// </summary>
public class TmdbMetadataProviderTests
{
private static TmdbMetadataProvider Provider(FakeHttpClientFactory http) =>
new(
http,
Options.Create(
new MetadataOptions
{
Language = "ru-RU",
Tmdb = new TmdbOptions { ApiKey = "key" },
}
)
);
[Fact]
public async Task SearchShows_Series_ReadsNameAndFirstAirDate()
{
var http = FakeHttpClientFactory.Json(
"""
{"results":[
{"id":42,"name":"Сериал","first_air_date":"2001-05-06","overview":"о чём-то","poster_path":"/p.jpg"},
{"name":"без идентификатора"}
]}
"""
);
var found = await Provider(http)
.SearchShowsAsync("сериал", ShowKind.Series, CancellationToken.None);
Assert.Contains("/search/tv", http.LastUrl);
Assert.Contains("language=ru-RU", http.LastUrl);
// Запись без id пропущена: обратиться по ней всё равно не получится.
var candidate = Assert.Single(found);
Assert.Equal("42", candidate.ExternalId);
Assert.Equal("Сериал", candidate.Title);
Assert.Equal(2001, candidate.Year);
Assert.Equal("о чём-то", candidate.Overview);
Assert.Equal("https://image.tmdb.org/t/p/w500/p.jpg", candidate.PosterUrl);
Assert.Equal(ShowKind.Series, candidate.Kind);
}
[Fact]
public async Task SearchShows_Single_UsesMovieNamespace()
{
var http = FakeHttpClientFactory.Json(
"""{"results":[{"id":7,"title":"Фильм","release_date":"1999-03-31"}]}"""
);
var found = await Provider(http)
.SearchShowsAsync("фильм", ShowKind.Single, CancellationToken.None);
Assert.Contains("/search/movie", http.LastUrl);
var candidate = Assert.Single(found);
Assert.Equal("Фильм", candidate.Title);
Assert.Equal(1999, candidate.Year);
Assert.Null(candidate.PosterUrl);
Assert.Equal(ShowKind.Single, candidate.Kind);
}
[Fact]
public async Task SearchShows_ReturnsEmpty_WhenNoResultsSection()
{
var found = await Provider(FakeHttpClientFactory.Json("{}"))
.SearchShowsAsync("ничего", ShowKind.Series, CancellationToken.None);
Assert.Empty(found);
}
[Fact]
public async Task GetShow_Series_ReadsGenresAndContentRating()
{
var http = FakeHttpClientFactory.Json(
"""
{"name":"Сериал","first_air_date":"2001-05-06","overview":"описание","poster_path":"/p.jpg",
"genres":[{"id":28,"name":"Боевик"},{"name":"Драма"},{"id":99}],
"content_ratings":{"results":[{"iso_3166_1":"RU","rating":"16+"},{"iso_3166_1":"US","rating":"TV-14"}]}}
"""
);
var show = await Provider(http).GetShowAsync("42", ShowKind.Series, CancellationToken.None);
Assert.Contains("/tv/42", http.LastUrl);
Assert.Contains("append_to_response=content_ratings", http.LastUrl);
Assert.NotNull(show);
Assert.Equal("Сериал", show.Title);
Assert.Equal(2001, show.Year);
// Идентификатор впереди названия, безымянный жанр — только идентификатором.
Assert.Equal(["tmdb:28", "Боевик", "Драма", "tmdb:99"], show.Genres);
// US приоритетнее RU, хотя в ответе идёт вторым.
Assert.Equal("TV-14", show.ContentRating);
}
[Fact]
public async Task GetShow_Movie_PrefersTheatricalCertification()
{
// Постер в ответе есть — значит, добора язык-нейтрального не будет и запрос ровно один.
var http = FakeHttpClientFactory.Json(
"""
{"title":"Фильм","poster_path":"/p.jpg","release_dates":{"results":[{"iso_3166_1":"US","release_dates":[
{"type":4,"certification":"R"},
{"type":3,"certification":"PG-13"}
]}]}}
"""
);
var show = await Provider(http).GetShowAsync("7", ShowKind.Single, CancellationToken.None);
Assert.Contains("/movie/7", http.LastUrl);
Assert.Contains("append_to_response=release_dates", http.LastUrl);
Assert.Equal("PG-13", show?.ContentRating);
}
/// <summary>
/// TMDb отдаёт постер под запрошенный язык, и у шоу без локализованной обложки поле пустое —
/// хотя в поиске картинка была. Без добора «Применить» молча оставляло бы шоу без постера.
/// </summary>
[Fact]
public async Task GetShow_WithoutLocalizedPoster_FallsBackToNeutralOne()
{
// Локализованный ответ (с language) — без постера, язык-нейтральный — с ним.
var http = new FakeHttpClientFactory(url =>
FakeHttpClientFactory.Ok(
url.Contains("language=", StringComparison.Ordinal)
? """{"name":"Сериал"}"""
: """{"name":"Сериал","poster_path":"/neutral.jpg"}"""
)
);
var show = await Provider(http).GetShowAsync("9", ShowKind.Series, CancellationToken.None);
Assert.Equal(2, http.Urls.Count);
Assert.DoesNotContain("language=", http.LastUrl);
Assert.Contains("/tv/9", http.LastUrl);
Assert.EndsWith("/neutral.jpg", show?.PosterUrl);
}
[Fact]
public async Task GetShow_Movie_FallsBackToAnyReleaseCertification()
{
var http = FakeHttpClientFactory.Json(
"""
{"title":"Фильм","release_dates":{"results":[{"iso_3166_1":"US","release_dates":[
{"type":5,"certification":"NC-17"}
]}]}}
"""
);
var show = await Provider(http).GetShowAsync("7", ShowKind.Single, CancellationToken.None);
Assert.Equal("NC-17", show?.ContentRating);
}
[Fact]
public async Task GetShow_Movie_SkipsCountriesWithoutCertification()
{
var http = FakeHttpClientFactory.Json(
"""
{"title":"Фильм","release_dates":{"results":[
{"iso_3166_1":"US"},
{"iso_3166_1":"US","release_dates":[{"type":3,"certification":" "}]},
{"iso_3166_1":"RU","release_dates":[{"type":3,"certification":"18+"}]}
]}}
"""
);
var show = await Provider(http).GetShowAsync("7", ShowKind.Single, CancellationToken.None);
// У США записи есть, но сертификации в них нет — ответом становится следующая страна.
Assert.Equal("18+", show?.ContentRating);
}
[Fact]
public async Task GetShow_WithoutOptionalFields_FallsBackToPlaceholders()
{
var show = await Provider(FakeHttpClientFactory.Json("{}"))
.GetShowAsync("42", ShowKind.Series, CancellationToken.None);
Assert.NotNull(show);
Assert.Equal("42", show.ExternalId);
Assert.Equal("—", show.Title);
Assert.Null(show.Year);
Assert.Null(show.PosterUrl);
Assert.Null(show.ContentRating);
Assert.Empty(show.Genres!);
}
[Fact]
public async Task GetShow_ReturnsNull_WhenRequestFails()
{
var show = await Provider(FakeHttpClientFactory.Failing())
.GetShowAsync("42", ShowKind.Series, CancellationToken.None);
Assert.Null(show);
}
[Fact]
public async Task GetEpisode_ReadsStillAndAirDate()
{
var http = FakeHttpClientFactory.Json(
"""{"name":"Серия","overview":"о серии","still_path":"/s.jpg","air_date":"2001-06-07"}"""
);
var episode = await Provider(http).GetEpisodeAsync("42", 2, 3, CancellationToken.None);
Assert.Contains("/tv/42/season/2/episode/3", http.LastUrl);
Assert.NotNull(episode);
Assert.Equal("Серия", episode.Title);
Assert.Equal("о серии", episode.Overview);
Assert.Equal("https://image.tmdb.org/t/p/w300/s.jpg", episode.StillUrl);
Assert.Equal(new DateOnly(2001, 6, 7), episode.AirDate);
}
[Fact]
public async Task GetEpisode_ReturnsNull_WhenRequestFails()
{
var episode = await Provider(FakeHttpClientFactory.Failing())
.GetEpisodeAsync("42", 1, 1, CancellationToken.None);
Assert.Null(episode);
}
[Fact]
public async Task GetSeasonEpisodeCount_CountsEpisodes()
{
var http = FakeHttpClientFactory.Json("""{"episodes":[{"id":1},{"id":2},{"id":3}]}""");
var count = await Provider(http)
.GetSeasonEpisodeCountAsync("42", 2, CancellationToken.None);
Assert.Contains("/tv/42/season/2", http.LastUrl);
Assert.Equal(3, count);
}
[Fact]
public async Task GetSeasonEpisodeCount_ReturnsNull_WhenSectionMissing()
{
var count = await Provider(FakeHttpClientFactory.Json("{}"))
.GetSeasonEpisodeCountAsync("42", 9, CancellationToken.None);
Assert.Null(count);
}
[Fact]
public async Task GetSeasonNumbers_SkipsSpecialsAndSorts()
{
var http = FakeHttpClientFactory.Json(
"""{"seasons":[{"season_number":2},{"season_number":0},{"season_number":1},{"name":"без номера"}]}"""
);
var seasons = await Provider(http).GetSeasonNumbersAsync("42", CancellationToken.None);
Assert.Equal([1, 2], seasons);
}
[Fact]
public async Task GetSeasonNumbers_ReturnsEmpty_WhenRequestFails()
{
var seasons = await Provider(FakeHttpClientFactory.Failing())
.GetSeasonNumbersAsync("42", CancellationToken.None);
Assert.Empty(seasons);
}
}