using Microsoft.Extensions.Options; using TeleWave.Application.Tests.TestSupport; using TeleWave.Domain.Library; using TeleWave.Infrastructure.Metadata; using Xunit; namespace TeleWave.Application.Tests.Metadata; /// /// Разбор ответов TMDb. Наружу тесты не ходят — HTTP подменён, проверяем и собранный адрес /// (сериалы и фильмы живут в разных пространствах API), и то, что из ответа вычитано. /// 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":"Фильм","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); } [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); } }