"""Unit tests for TMDB client.""" from unittest.mock import AsyncMock, MagicMock, patch import pytest from aiohttp import ClientResponseError, ClientSession from src.core.services.tmdb_client import TMDBAPIError, TMDBClient @pytest.fixture def tmdb_client(): """Create TMDB client with test API key.""" return TMDBClient(api_key="test_api_key") @pytest.fixture def mock_response(): """Create mock aiohttp response.""" mock = AsyncMock() mock.status = 200 mock.json = AsyncMock(return_value={"success": True}) return mock class TestTMDBClientInit: """Test TMDB client initialization.""" def test_init_with_api_key(self): """Test initialization with API key.""" client = TMDBClient(api_key="my_key") assert client.api_key == "my_key" assert client.base_url == "https://api.themoviedb.org/3" assert client.image_base_url == "https://image.tmdb.org/t/p" assert client.session is None assert client._cache == {} def test_init_sets_attributes(self): """Test all attributes are set correctly.""" client = TMDBClient(api_key="test") assert hasattr(client, "api_key") assert hasattr(client, "base_url") assert hasattr(client, "image_base_url") assert hasattr(client, "session") assert hasattr(client, "_cache") class TestTMDBClientContextManager: """Test TMDB client as context manager.""" @pytest.mark.asyncio async def test_async_context_manager(self): """Test async context manager creates session.""" client = TMDBClient(api_key="test") async with client as c: assert c.session is not None assert isinstance(c.session, ClientSession) # Session should be closed after context assert client.session is None @pytest.mark.asyncio async def test_close_closes_session(self): """Test close method closes session.""" client = TMDBClient(api_key="test") await client.__aenter__() assert client.session is not None await client.close() assert client.session is None class TestTMDBClientSearchTVShow: """Test search_tv_show method.""" @pytest.mark.asyncio async def test_search_tv_show_success(self, tmdb_client, mock_response): """Test successful TV show search.""" mock_response.json = AsyncMock(return_value={ "results": [ {"id": 1, "name": "Test Show"}, {"id": 2, "name": "Another Show"} ] }) with patch.object(tmdb_client, "_make_request", return_value=mock_response.json.return_value): result = await tmdb_client.search_tv_show("Test Show") assert "results" in result assert len(result["results"]) == 2 assert result["results"][0]["name"] == "Test Show" @pytest.mark.asyncio async def test_search_tv_show_with_year(self, tmdb_client): """Test TV show search with year filter.""" mock_data = {"results": [{"id": 1, "name": "Test Show", "first_air_date": "2020-01-01"}]} with patch.object(tmdb_client, "_make_request", return_value=mock_data): result = await tmdb_client.search_tv_show("Test Show", year=2020) assert "results" in result @pytest.mark.asyncio async def test_search_tv_show_empty_results(self, tmdb_client): """Test search with no results.""" with patch.object(tmdb_client, "_make_request", return_value={"results": []}): result = await tmdb_client.search_tv_show("NonexistentShow") assert result["results"] == [] @pytest.mark.asyncio async def test_search_tv_show_uses_cache(self, tmdb_client): """Test search results are cached.""" mock_data = {"results": [{"id": 1, "name": "Cached Show"}]} with patch.object(tmdb_client, "_make_request", return_value=mock_data) as mock_request: # First call should hit API result1 = await tmdb_client.search_tv_show("Cached Show") assert mock_request.call_count == 1 # Second call should use cache result2 = await tmdb_client.search_tv_show("Cached Show") assert mock_request.call_count == 1 # Not called again assert result1 == result2 class TestTMDBClientGetTVShowDetails: """Test get_tv_show_details method.""" @pytest.mark.asyncio async def test_get_tv_show_details_success(self, tmdb_client): """Test successful TV show details retrieval.""" mock_data = { "id": 123, "name": "Test Show", "overview": "A test show", "first_air_date": "2020-01-01" } with patch.object(tmdb_client, "_make_request", return_value=mock_data): result = await tmdb_client.get_tv_show_details(123) assert result["id"] == 123 assert result["name"] == "Test Show" @pytest.mark.asyncio async def test_get_tv_show_details_with_append(self, tmdb_client): """Test details with append_to_response.""" mock_data = { "id": 123, "name": "Test Show", "credits": {"cast": []}, "images": {"posters": []} } with patch.object(tmdb_client, "_make_request", return_value=mock_data) as mock_request: result = await tmdb_client.get_tv_show_details(123, append_to_response="credits,images") assert "credits" in result assert "images" in result # Verify append_to_response was passed call_args = mock_request.call_args assert "credits,images" in str(call_args) class TestTMDBClientGetExternalIDs: """Test get_tv_show_external_ids method.""" @pytest.mark.asyncio async def test_get_external_ids_success(self, tmdb_client): """Test successful external IDs retrieval.""" mock_data = { "imdb_id": "tt1234567", "tvdb_id": 98765 } with patch.object(tmdb_client, "_make_request", return_value=mock_data): result = await tmdb_client.get_tv_show_external_ids(123) assert result["imdb_id"] == "tt1234567" assert result["tvdb_id"] == 98765 class TestTMDBClientGetImages: """Test get_tv_show_images method.""" @pytest.mark.asyncio async def test_get_images_success(self, tmdb_client): """Test successful images retrieval.""" mock_data = { "posters": [{"file_path": "/poster.jpg"}], "backdrops": [{"file_path": "/backdrop.jpg"}], "logos": [{"file_path": "/logo.png"}] } with patch.object(tmdb_client, "_make_request", return_value=mock_data): result = await tmdb_client.get_tv_show_images(123) assert "posters" in result assert "backdrops" in result assert "logos" in result assert len(result["posters"]) == 1 class TestTMDBClientImageURL: """Test get_image_url method.""" def test_get_image_url_with_size(self, tmdb_client): """Test image URL generation with size.""" url = tmdb_client.get_image_url("/test.jpg", "w500") assert url == "https://image.tmdb.org/t/p/w500/test.jpg" def test_get_image_url_original(self, tmdb_client): """Test image URL with original size.""" url = tmdb_client.get_image_url("/test.jpg", "original") assert url == "https://image.tmdb.org/t/p/original/test.jpg" def test_get_image_url_strips_leading_slash(self, tmdb_client): """Test path without leading slash works.""" url = tmdb_client.get_image_url("test.jpg", "w500") assert url == "https://image.tmdb.org/t/p/w500/test.jpg" class TestTMDBClientMakeRequest: """Test _make_request private method.""" @pytest.mark.asyncio async def test_make_request_success(self, tmdb_client): """Test successful request.""" mock_session = AsyncMock() mock_response = AsyncMock() mock_response.status = 200 mock_response.json = AsyncMock(return_value={"data": "test"}) mock_session.get = AsyncMock(return_value=mock_response) tmdb_client.session = mock_session result = await tmdb_client._make_request("tv/search", {"query": "test"}) assert result == {"data": "test"} @pytest.mark.asyncio async def test_make_request_unauthorized(self, tmdb_client): """Test 401 unauthorized error.""" mock_session = AsyncMock() mock_response = AsyncMock() mock_response.status = 401 mock_response.raise_for_status = MagicMock( side_effect=ClientResponseError(None, None, status=401) ) mock_session.get = AsyncMock(return_value=mock_response) tmdb_client.session = mock_session with pytest.raises(TMDBAPIError, match="Invalid API key"): await tmdb_client._make_request("tv/search", {}) @pytest.mark.asyncio async def test_make_request_not_found(self, tmdb_client): """Test 404 not found error.""" mock_session = AsyncMock() mock_response = AsyncMock() mock_response.status = 404 mock_response.raise_for_status = MagicMock( side_effect=ClientResponseError(None, None, status=404) ) mock_session.get = AsyncMock(return_value=mock_response) tmdb_client.session = mock_session with pytest.raises(TMDBAPIError, match="not found"): await tmdb_client._make_request("tv/99999", {}) @pytest.mark.asyncio async def test_make_request_rate_limit(self, tmdb_client): """Test 429 rate limit error.""" mock_session = AsyncMock() mock_response = AsyncMock() mock_response.status = 429 mock_response.raise_for_status = MagicMock( side_effect=ClientResponseError(None, None, status=429) ) mock_session.get = AsyncMock(return_value=mock_response) tmdb_client.session = mock_session with pytest.raises(TMDBAPIError, match="rate limit"): await tmdb_client._make_request("tv/search", {}) class TestTMDBClientDownloadImage: """Test download_image method.""" @pytest.mark.asyncio async def test_download_image_success(self, tmdb_client, tmp_path): """Test successful image download.""" image_data = b"fake_image_data" mock_session = AsyncMock() mock_response = AsyncMock() mock_response.status = 200 mock_response.read = AsyncMock(return_value=image_data) mock_session.get = AsyncMock(return_value=mock_response) tmdb_client.session = mock_session output_path = tmp_path / "test.jpg" await tmdb_client.download_image("https://test.com/image.jpg", output_path) assert output_path.exists() assert output_path.read_bytes() == image_data @pytest.mark.asyncio async def test_download_image_failure(self, tmdb_client, tmp_path): """Test image download failure.""" mock_session = AsyncMock() mock_response = AsyncMock() mock_response.status = 404 mock_response.raise_for_status = MagicMock( side_effect=ClientResponseError(None, None, status=404) ) mock_session.get = AsyncMock(return_value=mock_response) tmdb_client.session = mock_session output_path = tmp_path / "test.jpg" with pytest.raises(TMDBAPIError): await tmdb_client.download_image("https://test.com/missing.jpg", output_path)