c62ae81275
Anti-corruption boundary tightened on the TMDB adapter: * TmdbMovieInfo / TmdbShowInfo now carry domain VOs (TmdbId, ImdbId, MovieTitle, ReleaseYear, ShowStatus) instead of raw scalars — validation happens at the boundary, not three layers later. * ShowStatus enum added (domain/tv_shows/value_objects) with a from_tmdb() mapper that falls back to UNKNOWN + logs a warning on unrecognized values. TVShow.status is now ShowStatus, not str. * MovieTitle cap raised from 100 to 150 chars. * MediaResult / ExternalIds dropped. Replaced by per-media search DTOs: TmdbMovieSearchResult and TmdbShowSearchResult. Neither carries imdb_id — search no longer enriches with external_ids (callers needing imdb_id follow up with get_movie_info / get_tv_show_info on the chosen tmdb_id). * TMDBClient: search_multi / search_media / _parse_result removed. search_movies (/search/movie) and search_shows (/search/tv) added, each parsing hits into VO-typed DTOs. * SearchMovieUseCase returns a list of MovieHit (flattened to primitives for the agent). New symmetric SearchShowUseCase + ShowHit / SearchShowResponse DTOs. * agent/tools/api.py: find_media_imdb_id → search_movies + search_shows wrappers. * FileEntry moved from domain/shared/ports/filesystem_scanner.py to domain/shared/file_entry.py (it's a DTO, not a Protocol); size_kb (float) → size (int bytes). Scanner and SubtitleIdentifier updated. Tests: 79/79 pass on tests/infrastructure/api/ + tests/application/test_search_movie.py + tests/application/test_search_show.py.
102 lines
3.2 KiB
Python
102 lines
3.2 KiB
Python
"""Tests for ``alfred.application.tv_shows.search_show.SearchShowUseCase``.
|
|
|
|
Symmetric to ``test_search_movie.py``. The use case wraps
|
|
:meth:`TMDBClient.search_shows` and flattens each hit into a
|
|
:class:`ShowHit` wrapped in a :class:`SearchShowResponse` envelope.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from unittest.mock import MagicMock
|
|
|
|
import pytest
|
|
|
|
from alfred.application.tv_shows.search_show import SearchShowUseCase
|
|
from alfred.domain.shared.value_objects import TmdbId
|
|
from alfred.infrastructure.api.tmdb.dto import TmdbShowSearchResult
|
|
from alfred.infrastructure.api.tmdb.exceptions import (
|
|
TMDBAPIError,
|
|
TMDBConfigurationError,
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def client():
|
|
return MagicMock()
|
|
|
|
|
|
@pytest.fixture
|
|
def use_case(client):
|
|
return SearchShowUseCase(client)
|
|
|
|
|
|
def _hit(**kw) -> TmdbShowSearchResult:
|
|
defaults = dict(
|
|
tmdb_id=TmdbId(84958),
|
|
name="Foundation",
|
|
first_air_year=2021,
|
|
)
|
|
defaults.update(kw)
|
|
return TmdbShowSearchResult(**defaults)
|
|
|
|
|
|
class TestSuccess:
|
|
def test_single_hit_is_flattened(self, client, use_case):
|
|
client.search_shows.return_value = [_hit()]
|
|
r = use_case.execute("Foundation")
|
|
assert r.status == "ok"
|
|
assert len(r.hits) == 1
|
|
h = r.hits[0]
|
|
assert h.tmdb_id == 84958
|
|
assert h.name == "Foundation"
|
|
assert h.first_air_year == 2021
|
|
|
|
def test_multiple_hits_preserve_order(self, client, use_case):
|
|
client.search_shows.return_value = [
|
|
_hit(),
|
|
_hit(tmdb_id=TmdbId(42), name="Fallout"),
|
|
]
|
|
r = use_case.execute("Foundation")
|
|
assert [h.tmdb_id for h in r.hits] == [84958, 42]
|
|
|
|
def test_hit_without_first_air_year(self, client, use_case):
|
|
client.search_shows.return_value = [_hit(first_air_year=None)]
|
|
r = use_case.execute("Foundation")
|
|
assert r.hits[0].first_air_year is None
|
|
|
|
def test_empty_results_returns_ok_with_no_hits(self, client, use_case):
|
|
client.search_shows.return_value = []
|
|
r = use_case.execute("nothing")
|
|
assert r.status == "ok"
|
|
assert r.hits == []
|
|
assert r.error is None
|
|
|
|
|
|
class TestErrorTranslation:
|
|
def test_configuration_error(self, client, use_case):
|
|
client.search_shows.side_effect = TMDBConfigurationError("missing key")
|
|
r = use_case.execute("x")
|
|
assert r.status == "error"
|
|
assert r.error == "configuration_error"
|
|
|
|
def test_api_error(self, client, use_case):
|
|
client.search_shows.side_effect = TMDBAPIError("500 oops")
|
|
r = use_case.execute("x")
|
|
assert r.status == "error"
|
|
assert r.error == "api_error"
|
|
assert "500" in r.message
|
|
|
|
def test_validation_error(self, client, use_case):
|
|
client.search_shows.side_effect = ValueError("query too long")
|
|
r = use_case.execute("x")
|
|
assert r.status == "error"
|
|
assert r.error == "validation_failed"
|
|
assert "too long" in r.message
|
|
|
|
|
|
class TestPassThrough:
|
|
def test_query_forwarded_verbatim(self, client, use_case):
|
|
client.search_shows.return_value = []
|
|
use_case.execute("Foundation")
|
|
client.search_shows.assert_called_once_with("Foundation")
|