import httpx import pytest import respx from fansifter_common.httpclient import HTTPClientError from pydantic import SecretStr from resonance_engine.adapters.deezer import ( API_BASE, DeezerClient, DeezerHTTPError, ) class TestDeezerHTTPError: def test_detail_appends_api_error_message(self) -> None: response = httpx.Response(403, json={"error": {"message": "Forbidden"}}) exc = DeezerHTTPError("status 403", response=response) assert exc.detail == "Forbidden" assert str(exc) == "status 403: Forbidden" def test_detail_none_for_non_json_body(self) -> None: exc = DeezerHTTPError( "status 500", response=httpx.Response(500, content=b"nope") ) assert exc.detail is None assert str(exc) == "status 500" def test_detail_none_without_response(self) -> None: exc = DeezerHTTPError("status 500") assert exc.detail is None assert str(exc) == "status 500" class TestDeezerClient: @pytest.fixture(scope="class") def client(self) -> DeezerClient: return DeezerClient() def test_refresh_token_echoes_token_without_request( self, client: DeezerClient, respx_mock: respx.MockRouter ) -> None: # No token exchange — refresh_token is a pass-through and makes no HTTP call. result = client.refresh_token(SecretStr("acc")) assert result == {"access_token": SecretStr("acc")} assert not respx_mock.calls def test_resource_raises_401_on_oauth_exception( self, client: DeezerClient, respx_mock: respx.MockRouter ) -> None: respx_mock.get(f"{API_BASE}/user/me").mock( return_value=httpx.Response( 200, json={"error": {"type": "OAuthException", "message": "Invalid token"}}, ) ) with pytest.raises(DeezerHTTPError, match="Invalid token") as exc_info: client.get_current_user_profile(SecretStr("acc")) assert exc_info.value.status_code == 401 def test_get_profile_returns_profile( self, client: DeezerClient, respx_mock: respx.MockRouter ) -> None: profile = {"id": 1, "name": "Fan", "country": "FR"} respx_mock.get(f"{API_BASE}/user/me").mock( return_value=httpx.Response(200, json=profile) ) result = client.get_current_user_profile(SecretStr("acc")) assert result == profile def test_get_passes_access_token_as_query_param( self, client: DeezerClient, respx_mock: respx.MockRouter ) -> None: respx_mock.get(f"{API_BASE}/user/me").mock( return_value=httpx.Response(200, json={"id": 1}) ) client.get_current_user_profile(SecretStr("acc")) assert respx_mock.calls[0].request.url.params["access_token"] == "acc" def test_get_raises_429_on_quota_error( self, client: DeezerClient, respx_mock: respx.MockRouter ) -> None: respx_mock.get(f"{API_BASE}/user/me").mock( return_value=httpx.Response( 200, json={"error": {"code": 4, "message": "Quota limit exceeded"}} ) ) with pytest.raises(DeezerHTTPError, match="Quota limit exceeded") as exc_info: client.get_current_user_profile(SecretStr("acc")) assert exc_info.value.status_code == 429 def test_get_raises_500_on_other_error( self, client: DeezerClient, respx_mock: respx.MockRouter ) -> None: respx_mock.get(f"{API_BASE}/user/me").mock( return_value=httpx.Response( 200, json={"error": {"code": 800, "message": "No data"}} ) ) with pytest.raises(DeezerHTTPError, match="No data") as exc_info: client.get_current_user_profile(SecretStr("acc")) assert exc_info.value.status_code == 500 def test_get_raises_http_error_on_5xx( self, client: DeezerClient, respx_mock: respx.MockRouter ) -> None: respx_mock.get(f"{API_BASE}/user/me").mock(return_value=httpx.Response(500)) with pytest.raises(HTTPClientError) as exc_info: client.get_current_user_profile(SecretStr("acc")) assert exc_info.value.status_code == 500 def test_get_top_artists_returns_items_on_single_page( self, client: DeezerClient, respx_mock: respx.MockRouter ) -> None: artist = {"id": 1, "name": "Artist One", "type": "artist"} respx_mock.get(f"{API_BASE}/user/me/charts/artists").mock( return_value=httpx.Response(200, json={"data": [artist], "total": 1}) ) result = client.get_current_user_top_artists(SecretStr("acc")) assert result == [artist] def test_get_top_artists_fetches_all_pages( self, client: DeezerClient, respx_mock: respx.MockRouter ) -> None: artist1 = {"id": 1, "name": "Artist One", "type": "artist"} artist2 = {"id": 2, "name": "Artist Two", "type": "artist"} def side_effect(request: httpx.Request) -> httpx.Response: index = int(request.url.params.get("index", 0)) if index == 0: return httpx.Response( 200, json={ "data": [artist1], "total": 2, "next": f"{API_BASE}/user/me/charts/artists?index=1", }, ) return httpx.Response(200, json={"data": [artist2], "total": 2}) respx_mock.get(f"{API_BASE}/user/me/charts/artists").mock( side_effect=side_effect ) result = client.get_current_user_top_artists(SecretStr("acc")) assert result == [artist1, artist2] def test_get_top_artists_passes_index_param( self, client: DeezerClient, respx_mock: respx.MockRouter ) -> None: respx_mock.get(f"{API_BASE}/user/me/charts/artists").mock( return_value=httpx.Response(200, json={"data": [], "total": 0}) ) client.get_current_user_top_artists(SecretStr("acc")) assert respx_mock.calls[0].request.url.params["index"] == "0" def test_get_top_tracks_returns_items( self, client: DeezerClient, respx_mock: respx.MockRouter ) -> None: track = {"id": 1, "title": "Track One", "type": "track"} respx_mock.get(f"{API_BASE}/user/me/charts/tracks").mock( return_value=httpx.Response(200, json={"data": [track], "total": 1}) ) result = client.get_current_user_top_tracks(SecretStr("acc")) assert result == [track] def test_get_history_returns_items( self, client: DeezerClient, respx_mock: respx.MockRouter ) -> None: track = {"id": 1, "title": "Track One", "type": "track"} respx_mock.get(f"{API_BASE}/user/me/history").mock( return_value=httpx.Response(200, json={"data": [track], "total": 1}) ) result = client.get_current_user_history(SecretStr("acc")) assert result == [track] def test_get_playlists_tolerates_null_items( self, client: DeezerClient, respx_mock: respx.MockRouter ) -> None: playlist = {"id": 1, "title": "Playlist One", "type": "playlist"} respx_mock.get(f"{API_BASE}/user/me/playlists").mock( return_value=httpx.Response( 200, json={"data": [playlist, None], "total": 2} ) ) result = client.get_current_user_playlists(SecretStr("acc")) assert result == [playlist] def test_get_albums_returns_items( self, client: DeezerClient, respx_mock: respx.MockRouter ) -> None: album = {"id": 1, "title": "Album One", "type": "album"} respx_mock.get(f"{API_BASE}/user/me/albums").mock( return_value=httpx.Response(200, json={"data": [album], "total": 1}) ) result = client.get_current_user_albums(SecretStr("acc")) assert result == [album] def test_get_tracks_returns_items( self, client: DeezerClient, respx_mock: respx.MockRouter ) -> None: track = {"id": 1, "title": "Track One", "type": "track"} respx_mock.get(f"{API_BASE}/user/me/tracks").mock( return_value=httpx.Response(200, json={"data": [track], "total": 1}) ) result = client.get_current_user_tracks(SecretStr("acc")) assert result == [track] def test_get_artists_returns_items( self, client: DeezerClient, respx_mock: respx.MockRouter ) -> None: artist = {"id": 1, "name": "Artist One", "type": "artist"} respx_mock.get(f"{API_BASE}/user/me/artists").mock( return_value=httpx.Response(200, json={"data": [artist], "total": 1}) ) result = client.get_current_user_artists(SecretStr("acc")) assert result == [artist] def test_get_artists_raises_http_error_on_network_error( self, client: DeezerClient, respx_mock: respx.MockRouter ) -> None: respx_mock.get(f"{API_BASE}/user/me/artists").mock( side_effect=httpx.ConnectError("timeout") ) with pytest.raises(HTTPClientError): client.get_current_user_artists(SecretStr("acc"))