"""Unit tests for Spotify API Service.""" from unittest.mock import MagicMock import pytest from src.services.spotify_api import requests from src.services.spotify_api import SpotifyAPI from src.services.spotify_api import time @pytest.fixture def access_token(): """Access token fixture.""" return 'access_token' @pytest.fixture def spotify_api(mocker, access_token): """Spotify API object fixture.""" requests_post = mocker.patch.object(requests, 'post') response_mock = MagicMock() response_mock.json.return_value = {'access_token': access_token} requests_post.return_value = response_mock return SpotifyAPI('my_key', 'my_secret') @pytest.fixture def successful_response(): """Successful response fixture.""" return {'successful': 'response'} @pytest.fixture def response_status_code_200(mocker, successful_response): """Successful response fixture.""" requests_get = mocker.patch.object(requests, 'get') response_mock = MagicMock() response_mock.status_code = 200 response_mock.json.return_value = successful_response requests_get.return_value = response_mock return requests_get def test_spotify_api_successful( spotify_api, response_status_code_200, successful_response): """Test successful API call.""" call_response = spotify_api.call('/search', {'my': 'param'}) assert call_response.json() == successful_response assert call_response == response_status_code_200() def test_spotify_api_params_passed_as_keyword_argument( mocker, spotify_api, successful_response): """Test that params are passed as a keyword argument to requests.get. This test verifies the fix for the bug where params were passed as a positional argument, causing them to be ignored by the Spotify API. """ requests_get = mocker.patch.object(requests, 'get') response_mock = MagicMock() response_mock.status_code = 200 response_mock.json.return_value = successful_response requests_get.return_value = response_mock test_params = {'q': 'artist:The Beatles', 'type': 'artist'} spotify_api.call('/search', test_params) # Verify requests.get was called with params as a keyword argument requests_get.assert_called_once() call_kwargs = requests_get.call_args.kwargs assert 'params' in call_kwargs assert call_kwargs['params'] == test_params assert call_kwargs['headers'] == {'Authorization': 'Bearer access_token'} assert call_kwargs['timeout'] == 10 @pytest.fixture def response_status_code_401(mocker): """Unauthorized response fixture.""" requests_get = mocker.patch.object(requests, 'get') response_mock = MagicMock() response_mock.status_code = 401 requests_get.return_value = response_mock return requests_get def test_spotify_api_unauthorized( mocker, spotify_api, response_status_code_401): """Test unauthorized API call.""" method_mock = mocker.patch.object( spotify_api, '_recreate_access_token_for_creds') call_response = spotify_api.call('search', {'my': 'param'}) assert method_mock.called assert call_response == response_status_code_401() @pytest.fixture def response_status_code_429(mocker): """Too many requests response fixture.""" requests_get = mocker.patch.object(requests, 'get') response_mock = MagicMock() response_mock.status_code = 429 requests_get.return_value = response_mock return requests_get def test_spotify_api_too_many_requests( mocker, spotify_api, response_status_code_429): """Test unauthorized API call.""" method_mock = mocker.patch.object(time, 'sleep') call_response = spotify_api.call('search', {'my': 'param'}) assert method_mock.called assert call_response == response_status_code_429() @pytest.fixture def response_status_code_500(mocker): """Too many requests response fixture.""" requests_get = mocker.patch.object(requests, 'get') response_mock = MagicMock() response_mock.status_code = 500 requests_get.return_value = response_mock return requests_get def test_spotify_api_server_error( mocker, spotify_api, response_status_code_500): """Test unauthorized API call.""" call_response = spotify_api.call('search', {'my': 'param'}) assert call_response.status_code == 500 assert call_response == response_status_code_500() @pytest.fixture def response_status_code_403(mocker): """Forbidden response fixture.""" requests_get = mocker.patch.object(requests, 'get') response_mock = MagicMock() response_mock.status_code = 403 requests_get.return_value = response_mock return requests_get def test_spotify_api_forbidden( mocker, spotify_api, response_status_code_403): """Test forbidden API call - should retry with fresh token.""" method_mock = mocker.patch.object( spotify_api, '_recreate_access_token_for_creds') call_response = spotify_api.call('search', {'my': 'param'}) assert method_mock.called assert call_response == response_status_code_403() def test_spotify_api_forbidden_refreshes_headers( mocker, spotify_api, successful_response): """Test that headers are refreshed with new token after 403 retry. This test verifies that when a 403 is encountered, the retry uses fresh headers with the new access token, not the old one. """ # Mock requests.get to return 403 first, then 200 requests_get = mocker.patch.object(requests, 'get') forbidden_response = MagicMock() forbidden_response.status_code = 403 forbidden_response.text = 'Forbidden' success_response = MagicMock() success_response.status_code = 200 success_response.json.return_value = successful_response requests_get.side_effect = [forbidden_response, success_response] # Mock token refresh to return a new token original_token = spotify_api._access_token new_token = 'new_access_token_after_403' requests_post = mocker.patch.object(requests, 'post') token_response = MagicMock() token_response.json.return_value = {'access_token': new_token} requests_post.return_value = token_response # Call the API result = spotify_api.call('/audio-features', {'ids': 'track123'}) # Verify the call succeeded after retry assert result.status_code == 200 assert result.json() == successful_response # Verify requests.get was called twice (first attempt + retry) assert requests_get.call_count == 2 # Verify first call used original token first_call_headers = requests_get.call_args_list[0].kwargs['headers'] assert first_call_headers == { 'Authorization': f'Bearer {original_token}'} # Verify second call used new token (this is the critical assertion) second_call_headers = requests_get.call_args_list[1].kwargs['headers'] assert second_call_headers == {'Authorization': f'Bearer {new_token}'} # Verify token was refreshed requests_post.assert_called_once()