diff --git a/tests/test_deezer.py b/tests/test_deezer.py index 5b7b7d77..75f46ed2 100644 --- a/tests/test_deezer.py +++ b/tests/test_deezer.py @@ -1,6 +1,7 @@ import os import pytest -from unittest.mock import Mock, patch +import deezer +from unittest.mock import Mock, AsyncMock, patch from util import arun from streamrip.client.downloadable import DeezerDownloadable @@ -124,6 +125,171 @@ def test_deezer_no_fallback_when_disabled(mock_deezer_client): with pytest.raises(NonStreamableError, match="The requested quality 2 is not available and fallback is disabled"): arun(mock_deezer_client.get_downloadable("123", quality=2)) +# ===== get_downloadable — geoblocking ===== + +def test_deezer_geoblocked_with_fallback(mock_deezer_client): + """WrongGeolocation retries the download using the FALLBACK track ID.""" + def gw_get_track_side_effect(track_id): + if track_id == "123": + return { + "FILESIZE_FLAC": 25_000_000, + "FILESIZE_MP3_320": 5_000_000, + "FILESIZE_MP3_128": 2_000_000, + "TRACK_TOKEN": "token_123", + "FALLBACK": {"SNG_ID": "456"}, + } + return { + "FILESIZE_FLAC": 25_000_000, + "FILESIZE_MP3_320": 5_000_000, + "FILESIZE_MP3_128": 2_000_000, + "TRACK_TOKEN": "token_456", + } + + mock_deezer_client.client.gw.get_track.side_effect = gw_get_track_side_effect + + def url_side_effect(token, fmt): + if token == "token_123": + raise deezer.WrongGeolocation("FR") + return "https://test.flac" + + mock_deezer_client.client.get_track_url.side_effect = url_side_effect + + downloadable = arun(mock_deezer_client.get_downloadable("123", quality=2)) + assert downloadable.quality == 2 + assert mock_deezer_client.client.gw.get_track.call_count == 2 + + +def test_deezer_geoblocked_no_fallback(mock_deezer_client): + """WrongGeolocation raises NonStreamableError when no FALLBACK ID is available.""" + mock_track_info = { + "FILESIZE_FLAC": 25_000_000, + "FILESIZE_MP3_320": 5_000_000, + "FILESIZE_MP3_128": 2_000_000, + "TRACK_TOKEN": "test_token", + # no FALLBACK key + } + mock_deezer_client.client.gw.get_track.return_value = mock_track_info + mock_deezer_client.client.get_track_url.side_effect = deezer.WrongGeolocation("FR") + + with pytest.raises(NonStreamableError, match="country"): + arun(mock_deezer_client.get_downloadable("123", quality=2)) + + +# ===== get_downloadable — encrypted URL fallback ===== + +def test_deezer_encrypted_url_fallback(mock_deezer_client): + """When get_track_url returns None, falls back to the AES-encrypted CDN URL.""" + mock_track_info = { + "FILESIZE_FLAC": 25_000_000, + "FILESIZE_MP3_320": 5_000_000, + "FILESIZE_MP3_128": 2_000_000, + "TRACK_TOKEN": "test_token", + "MD5_ORIGIN": "abc123def456abc123def456abc12345", + "MEDIA_VERSION": "1", + } + mock_deezer_client.client.gw.get_track.return_value = mock_track_info + mock_deezer_client.client.get_track_url.return_value = None + + with patch.object( + mock_deezer_client, + "_get_encrypted_file_url", + return_value="https://e-cdns-proxy-a.dzcdn.net/mobile/1/deadbeef", + ) as mock_encrypted: + downloadable = arun(mock_deezer_client.get_downloadable("123", quality=2)) + + mock_encrypted.assert_called_once_with( + "123", "abc123def456abc123def456abc12345", "1" + ) + assert downloadable.url == "https://e-cdns-proxy-a.dzcdn.net/mobile/1/deadbeef" + + +# ===== get_track ===== + +def test_deezer_get_track(mock_deezer_client): + """get_track returns a track dict with full album metadata embedded.""" + mock_deezer_client.client.api.get_track.return_value = { + "id": "100", + "title": "Test Track", + "album": {"id": 200}, + } + mock_deezer_client.client.api.get_album.return_value = { + "id": "200", + "title": "Test Album", + } + mock_deezer_client.client.api.get_album_tracks.return_value = { + "data": [{"id": "100"}] + } + + track = arun(mock_deezer_client.get_track("100")) + + assert track["title"] == "Test Track" + assert track["album"]["title"] == "Test Album" + assert track["album"]["track_total"] == 1 + + +# ===== get_playlist ===== + +def test_deezer_get_playlist(mock_deezer_client): + """get_playlist returns playlist metadata enriched with tracks and track_total.""" + mock_deezer_client.client.api.get_playlist.return_value = { + "title": "My Playlist", + } + mock_deezer_client.client.api.get_playlist_tracks.return_value = { + "data": [{"id": "1"}, {"id": "2"}], + } + + result = arun(mock_deezer_client.get_playlist("playlist_123")) + + assert result["title"] == "My Playlist" + assert result["track_total"] == 2 + assert len(result["tracks"]) == 2 + + +# ===== get_metadata ===== + +def test_deezer_get_metadata_dispatch(mock_deezer_client): + """get_metadata dispatches to the correct handler for each media type.""" + with patch.object( + mock_deezer_client, "get_album", return_value={"id": "1"} + ) as mock_get_album: + result = arun(mock_deezer_client.get_metadata("1", "album")) + mock_get_album.assert_called_once_with("1") + assert result == {"id": "1"} + + +def test_deezer_get_metadata_invalid_type(mock_deezer_client): + """get_metadata raises for unsupported media types.""" + with pytest.raises(Exception, match="not available on deezer"): + arun(mock_deezer_client.get_metadata("1", "label")) + + +# ===== search ===== + +def test_deezer_search_track(mock_deezer_client): + """search returns a list containing the API response when results are found.""" + mock_deezer_client.client.api.search_track.return_value = { + "total": 2, + "data": [{"id": "1"}, {"id": "2"}], + } + + results = arun(mock_deezer_client.search("track", "test query")) + + assert len(results) == 1 + assert results[0]["total"] == 2 + mock_deezer_client.client.api.search_track.assert_called_once_with( + "test query", limit=200 + ) + + +def test_deezer_search_no_results(mock_deezer_client): + """search returns an empty list when the API reports zero results.""" + mock_deezer_client.client.api.search_track.return_value = {"total": 0, "data": []} + + results = arun(mock_deezer_client.search("track", "nonexistent")) + + assert results == [] + + # ===== INTEGRATION TEST ===== @pytest.mark.skipif(