"""Tests for Apple Music client module.""" from datetime import datetime, timedelta import pytest from freezegun import freeze_time from participant.models.service.apple_music_client import ( AppleMusicClient, AppleMusicClientCredentialsManager, ) from requests import HTTPError, Response class TestAppleMusicClientCredentialsManager: """Tests for Apple Music Client Credentials Manager.""" # Mocks and fixtures @pytest.fixture(autouse=True) def frozen_time(self): """Freeze time for all tests.""" with freeze_time('2019-11-01 01:40:00') as f: yield f @pytest.fixture def client_id(self): """Apple Music client id.""" return 'ABC123DEFG' @pytest.fixture def team_id(self): """Apple Music team id.""" return 'DEF123GHIJ' @pytest.fixture def private_key(self): """Apple Music private key.""" return '--\nprivatekey\n--' @pytest.fixture def token_expires_in(self): """Lifetime of token in seconds.""" return 3600 @pytest.fixture def token_expiration_time(self, token_expires_in): """Time when tokenn expires.""" return datetime.now() + timedelta(seconds=token_expires_in) @pytest.fixture def credentials_manager(self, client_id, team_id, private_key, token_expires_in): """Instance of credential manager.""" return AppleMusicClientCredentialsManager( client_id, team_id, private_key, token_expires_in ) @pytest.fixture def jwt_encode_mock(self, mocker): """Mock jwt encode method.""" return mocker.patch('jwt.encode') @pytest.fixture def new_token(self): """Fixture for new token value.""" return 'new-token' @pytest.fixture def token(self): """Fixture for existing token value.""" return 'token' @pytest.fixture def new_token_info(self, new_token, token_expiration_time): """Information about new token.""" return { 'access_token': new_token, 'token_expiration_time': token_expiration_time, } @pytest.fixture def token_info(self, token, token_expiration_time): """Information about existing token.""" return {'access_token': token, 'token_expiration_time': token_expiration_time} @pytest.fixture def expired_token_info(self, new_token): """Information about expired token.""" return { 'access_token': new_token, 'token_expiration_time': datetime.now() - timedelta(seconds=1), } @pytest.fixture def get_token_info_mock(self, mocker, new_token_info): """Mock _get_token_info method.""" return mocker.patch.object( AppleMusicClientCredentialsManager, '_get_token_info', return_value=new_token_info, ) @pytest.fixture def credentials_manager_with_token_info(self, credentials_manager, token_info): """Instance of credentials manager with existing token.""" credentials_manager._token_info = token_info return credentials_manager @pytest.fixture def credentials_manager_with_expired_token_info( self, credentials_manager, expired_token_info ): """Instance of credentials manager with expired token.""" credentials_manager._token_info = expired_token_info return credentials_manager @pytest.fixture def request_access_token_mock(self, mocker, token): """Mock _request_access_token method.""" return mocker.patch.object( AppleMusicClientCredentialsManager, '_request_access_token', return_value=token, ) # Tests def test_get_access_token_new( self, credentials_manager, get_token_info_mock, new_token ): """Test get_access_token generates new token.""" result = credentials_manager.get_access_token() assert result == new_token def test_get_access_token_not_expired( self, credentials_manager_with_token_info, token ): """Test get_access_token returns existing token.""" result = credentials_manager_with_token_info.get_access_token() assert result == token def test_get_access_token_expired( self, credentials_manager_with_expired_token_info, get_token_info_mock, new_token, ): """Test get_access_token generates new token when old has expired.""" result = credentials_manager_with_expired_token_info.get_access_token() assert result == new_token def test__get_token_info( self, credentials_manager, request_access_token_mock, token, token_expiration_time, ): """Test _get_token_info returns expected result.""" expected_result = { 'token_expiration_time': token_expiration_time, 'access_token': token, } result = credentials_manager._get_token_info() assert result == expected_result def test__request_access_token_correct_result( self, credentials_manager, jwt_encode_mock, token_expiration_time ): """Test _request_access_token returns expected result.""" token = jwt_encode_mock.return_value result = credentials_manager._request_access_token(token_expiration_time) assert token == result def test__request_access_token_correct_keys( self, credentials_manager, jwt_encode_mock, client_id, team_id, private_key, token_expiration_time, ): """Test jwt encodes with correct keys.""" headers = {'alg': 'ES256', 'kid': client_id} payload = { 'iss': team_id, 'iat': int(datetime.now().timestamp()), 'exp': int(token_expiration_time.timestamp()), } credentials_manager._request_access_token(token_expiration_time) jwt_encode_mock.assert_called_once_with( payload, private_key, algorithm='ES256', headers=headers ) def test__is_token_expired_expired( self, credentials_manager_with_expired_token_info ): """Test _is_token_expired when expired.""" res = credentials_manager_with_expired_token_info._is_token_expired() assert res is True def test__is_token_expired_not_expired(self, credentials_manager_with_token_info): """Test _is_token_expired when not expired.""" res = credentials_manager_with_token_info._is_token_expired() assert res is False class TestAppleMusicClient: """Tests for Apple Music Client.""" # Mocks and fixtures @pytest.fixture def requests_timeout(self): """Timeout of request to Apple API.""" return 3 @pytest.fixture def max_request_tries(self): """Fixture for retries number.""" return 3 @pytest.fixture def access_token(self): """Access token.""" return 'access-token' @pytest.fixture def client_credentials_manager_mock(self, mocker, access_token): """Credentials Manager mock.""" cred_manager = mocker.Mock() cred_manager.get_access_token.return_value = access_token return cred_manager @pytest.fixture def apple_music_client( self, client_credentials_manager_mock, requests_timeout, max_request_tries, ): """Instance of Apple Music Client.""" return AppleMusicClient( client_credentials_manager_mock, requests_timeout, max_request_tries, ) @pytest.fixture def requests_mock(self, mocker): """Mock requests module.""" return mocker.patch('participant.models.service.apple_music_client.requests') @pytest.fixture def get_valid_response(self, mocker): """Fixture for requests.get response.""" return mocker.Mock() @pytest.fixture def get_valid_response_mock(self, requests_mock, get_valid_response): """Mock requests.get result.""" requests_mock.get.return_value = get_valid_response return requests_mock @pytest.fixture def headers_mock(self, access_token): """Headers for request.""" return { 'Authorization': 'Bearer {}'.format(access_token), 'Content-Type': 'application/json', } @pytest.fixture def query(self): """Search query value.""" return 'query' @pytest.fixture def limit(self): """Search result limit.""" return 10 @pytest.fixture def offset(self): """Search result offset.""" return 0 @pytest.fixture def artist_search_params(self, query, limit, offset): """Search payload.""" return { 'term': query, 'limit': limit, 'offset': offset, 'types': 'artists', 'l': 'en', } @pytest.fixture def artist_id(self): """Artist ID.""" return 'ARTIST_ID' @pytest.fixture def requests_get_throttling_errors(self, requests_mock, max_request_tries): """Emulate throttling errors.""" response = Response() response.status_code = 429 requests_mock.get.side_effect = [ HTTPError(response=response) ] * max_request_tries return requests_mock @pytest.fixture def requests_get_unauthorized_error(self, requests_mock): """Emulate unauthorized error.""" response = Response() response.status_code = 401 requests_mock.get.side_effect = [HTTPError(response=response)] return requests_mock @pytest.fixture def requests_get_unexpected_exception(self, requests_mock): """Emulate general error.""" requests_mock.get.side_effect = [Exception()] return requests_mock @pytest.fixture def time_sleep_mock(self, mocker): """Mock time.sleep.""" return mocker.patch('time.sleep') # Tests def test__auth_headers(self, apple_music_client, access_token): """Test _auth_headers method.""" result = apple_music_client._auth_headers() expected_result = {'Authorization': 'Bearer {}'.format(access_token)} return result == expected_result def test_search_artist_valid_response( self, apple_music_client, get_valid_response_mock, get_valid_response, query ): """Test search_artist returns expected result.""" result = apple_music_client.search_artist(query) assert result == get_valid_response.json() def test_search_artist_correct_params( self, apple_music_client, requests_mock, requests_timeout, headers_mock, artist_search_params, query, limit, offset, ): """Test search work with correct params.""" apple_music_client.search_artist(query, limit, offset) requests_mock.get.assert_called_once_with( 'https://api.music.apple.com/v1/catalog/us/search', headers=headers_mock, params=artist_search_params, timeout=requests_timeout, ) def test_search_artist_retry_on_throttling( self, apple_music_client, requests_get_throttling_errors, query, max_request_tries, time_sleep_mock, ): """Test search_artist retries when requests are throttled.""" with pytest.raises(HTTPError): apple_music_client.search_artist(query) assert requests_get_throttling_errors.get.call_count == max_request_tries def test_search_artist_immediate_failure_on_unathorized( self, apple_music_client, requests_get_unauthorized_error, query ): """Test search_artist retries only proper HTTP Errors.""" with pytest.raises(HTTPError): apple_music_client.search_artist(query) assert requests_get_unauthorized_error.get.call_count == 1 def test_search_artist_unexpected_exception( self, apple_music_client, requests_get_unexpected_exception, query ): """Test search_artist fails on random errors.""" with pytest.raises(Exception): apple_music_client.search_artist(query) assert requests_get_unexpected_exception.get.call_count == 1 def test_get_artist_by_id_valid_response( self, apple_music_client, get_valid_response_mock, get_valid_response, artist_id ): """Test get_artist_by_id returns expected result.""" result = apple_music_client.get_artist_by_id(artist_id) assert result == get_valid_response.json() def test_get_artist_by_id_correct_params( self, apple_music_client, requests_mock, requests_timeout, headers_mock, artist_id, ): """Test get artist by ID work with correct params.""" apple_music_client.get_artist_by_id(artist_id) requests_mock.get.assert_called_once_with( 'https://api.music.apple.com/v1/catalog/us/artists/ARTIST_ID', headers=headers_mock, timeout=requests_timeout, ) def test_get_artist_by_id_retry_on_throttling( self, apple_music_client, requests_get_throttling_errors, max_request_tries, time_sleep_mock, artist_id, ): """Test get_artist_by_id retries when requests are throttled.""" with pytest.raises(HTTPError): apple_music_client.get_artist_by_id(artist_id) assert requests_get_throttling_errors.get.call_count == max_request_tries def test_get_artist_by_id_immediate_failure_on_unathorized( self, apple_music_client, requests_get_unauthorized_error, artist_id ): """Test get_artist_by_id retries only proper HTTP Errors.""" with pytest.raises(HTTPError): apple_music_client.get_artist_by_id(artist_id) assert requests_get_unauthorized_error.get.call_count == 1 def test_get_artist_by_id_unexpected_exception( self, apple_music_client, requests_get_unexpected_exception, artist_id ): """Test get_artist_by_id fails on random errors.""" with pytest.raises(Exception): apple_music_client.get_artist_by_id(artist_id) assert requests_get_unexpected_exception.get.call_count == 1