"""Unit tests for API response.""" import json from unittest.mock import ANY from unittest.mock import call import pytest from src.common.types import ApiRequestType from src.common.types import RequestType from src.common.types import Store from src.logic.api_response import \ submit_spotify_api_get_audio_features_response from src.logic.api_response import \ submit_spotify_api_product_artists_search_response from src.logic.api_response import submit_spotify_api_product_search_response from src.logic.api_response import \ submit_spotify_api_track_artists_search_response from src.models.spotify import SpotifyAlbum from src.models.spotify import SpotifyGetAudioFeaturesRequest from src.models.spotify import SpotifyGetAudioFeaturesRequestPayload from src.models.spotify import SpotifyArtist from src.models.spotify import SpotifySearchAlbumArtistMessage from src.models.spotify import SpotifySearchAlbumMessage from src.models.spotify import SpotifySearchTrackByISRCRequest from src.models.spotify import SpotifySearchTrackByISRCRequestPayload from src.models.spotify import SpotifySearchTrackArtistMessage @pytest.fixture def producer(mocker): """Mock produce method for the Kafka producer.""" mock_producer = mocker.Mock() mock_producer.produce = mocker.Mock() # Patch the module-level producer so functions use the mock # Make the mock usable as a context manager (supports `with producer:`) mock_producer.__enter__ = mocker.Mock(return_value=mock_producer) mock_producer.__exit__ = mocker.Mock(return_value=None) mocker.patch('src.logic.api_response.producer', mock_producer) return mock_producer @pytest.fixture def spotify_api_response(): """Generate Spotify API response mock.""" return { 'albums': { 'items': [ { 'artists': [ { 'id': '4KmQSzjYZlZTIaalJ9iTGS', 'name': 'Sujoy Bhowmik', 'type': 'artist', 'uri': 'spotify:artist:4KmQSzjYZlZTIaalJ9iTGS' }, { 'id': '2QnFHcqYp9crRPNxa8SViM', 'name': 'Soma Chowdhury', 'type': 'artist', 'uri': 'spotify:artist:2QnFHcqYp9crRPNxa8SViM' } ], 'id': '5ClsTNLo8XCONaNvq49yyC', 'name': 'Agomoni Dola Dilo', 'release_date': '2022-09-25', 'release_date_precision': 'day', 'total_tracks': 1, 'type': 'album', 'uri': 'spotify:album:5ClsTNLo8XCONaNvq49yyC', 'album_type': 'single', 'images': [ { 'url': ( 'https://i.scdn.co/image/' 'ab67616d0000b273208500450dcd0fd294d7bd3b' ), 'height': 640, 'width': 640 }, { 'url': ( 'https://i.scdn.co/image/' 'ab67616d00001e02208500450dcd0fd294d7bd3b' ), 'height': 300, 'width': 300 } ] } ] } } def test_submit_spotify_api_product_search_response( producer, spotify_search_album_by_upc_request, spotify_api_response): """Test submit_spotify_api_product_search_response handler.""" request = spotify_search_album_by_upc_request submit_spotify_api_product_search_response(request, spotify_api_response) album = SpotifyAlbum(**spotify_api_response['albums']['items'][0]) album_message = SpotifySearchAlbumMessage( api_request=request, album=album) producer.produce.assert_called_once_with( 'event.spotifyapi.album', f'product-{request.payload.upc}', album_message.model_dump_json(), auto_flush=False) def test_submit_spotify_api_product_artists_search_response( producer, spotify_search_album_by_upc_request, spotify_api_response): """Test submit_spotify_api_product_artists_search_response handler.""" request = spotify_search_album_by_upc_request submit_spotify_api_product_artists_search_response( request, spotify_api_response) resp_artist_1 = spotify_api_response['albums']['items'][0]['artists'][0] artist_1 = SpotifyArtist( id=resp_artist_1['id'], name=resp_artist_1['name']) artist_message_1 = SpotifySearchAlbumArtistMessage( api_request=request, artist=artist_1) resp_artist_2 = spotify_api_response['albums']['items'][0]['artists'][1] artist_2 = SpotifyArtist( id=resp_artist_2['id'], name=resp_artist_2['name']) artist_message_2 = SpotifySearchAlbumArtistMessage( api_request=request, artist=artist_2) producer.produce.assert_has_calls([ call( 'event.spotifyapi.album.artist', f'product-{request.payload.product_id}', artist_message_1.model_dump_json(), auto_flush=False), call( 'event.spotifyapi.album.artist', f'product-{request.payload.product_id}', artist_message_2.model_dump_json(), auto_flush=False), ]) @pytest.fixture def spotify_track_id(): """Spotify track id fixture.""" return '2ZhGgNSJ4f5Sfns5yBoMmi' @pytest.fixture def spotify_get_audio_features_request(spotify_track_id): """Fixture for SpotifyGetAudioFeaturesRequest.""" return SpotifyGetAudioFeaturesRequest( request_type=ApiRequestType( store=Store.SPOTIFY, type=RequestType.get_audio_features ), payload=SpotifyGetAudioFeaturesRequestPayload( spotify_track_ids=[spotify_track_id] ), ) @pytest.fixture def tracks_audio_features_response(): """Tracks audio features mock response.""" return {'audio_features': [ {'danceability': 0.245, 'energy': 0.345, 'key': 8, 'loudness': -13.378, 'mode': 1, 'speechiness': 0.0354, 'acousticness': 0.846, 'instrumentalness': 0.605, 'liveness': 0.26, 'valence': 0.242, 'tempo': 146.136, 'type': 'audio_features', 'id': '5DR1c8L3KiEvw0Tjcg6IfP', 'uri': 'spotify:track:5DR1c8L3KiEvw0Tjcg6IfP', 'track_href': 'https://api.spotify.com' '/v1/tracks/5DR1c8L3KiEvw0Tjcg6IfP', 'analysis_url': 'https://api.spotify.com' '/v1/audio-analysis/5DR1c8L3KiEvw0Tjcg6IfP', 'duration_ms': 153088, 'time_signature': 4}, ]} def test_submit_spotify_api_get_audio_features_response( producer, spotify_get_audio_features_request, tracks_audio_features_response, ): """Test submit_spotify_api_get_audio_features_response handler.""" submit_spotify_api_get_audio_features_response( spotify_get_audio_features_request, tracks_audio_features_response) features = tracks_audio_features_response['audio_features'][0] assert features == json.loads(producer.produce.call_args.args[2]) producer.produce.assert_called_once_with( 'event.spotifyapi.audioFeatures', features['id'], ANY, auto_flush=False) def test_submit_spotify_api_get_audio_features_invalid_response( producer, spotify_get_audio_features_request, tracks_audio_features_response, ): """Test submit_spotify_api_get_audio_features_response invalid response.""" tracks_audio_features_response['audio_features'][0].pop('uri') submit_spotify_api_get_audio_features_response( spotify_get_audio_features_request, tracks_audio_features_response) assert not producer.produce.called @pytest.fixture def tracks_audio_features_empty_response(): """Tracks audio features mock empty response.""" return {'audio_features': [None]} def test_submit_spotify_api_get_audio_features_empty_response( producer, spotify_get_audio_features_request, spotify_track_id, tracks_audio_features_empty_response): """Test submit_spotify_api_get_audio_features_response empty response.""" submit_spotify_api_get_audio_features_response( spotify_get_audio_features_request, tracks_audio_features_empty_response) producer.produce.assert_called_once_with( 'event.spotifyapi.audioFeatures.missing', spotify_track_id, None, auto_flush=False) @pytest.fixture def spotify_search_track_by_isrc_request(): """Fixture for SpotifySearchTrackByISRCRequest.""" return SpotifySearchTrackByISRCRequest( request_type=ApiRequestType( store=Store.SPOTIFY, type=RequestType.search_track_by_isrc ), payload=SpotifySearchTrackByISRCRequestPayload( isrc='USUG12345678', vendor_id=1, subaccount_id=2, ) ) @pytest.fixture def spotify_track_search_response(): """Generate Spotify track search API response mock.""" return { 'tracks': { 'items': [ { 'id': '3n3Ppam7vgaVa1iaRUc9Lp', 'name': 'Test Track', 'type': 'track', 'uri': 'spotify:track:3n3Ppam7vgaVa1iaRUc9Lp', 'artists': [ { 'id': '4KmQSzjYZlZTIaalJ9iTGS', 'name': 'Artist One', 'type': 'artist', 'uri': 'spotify:artist:4KmQSzjYZlZTIaalJ9iTGS' }, { 'id': '2QnFHcqYp9crRPNxa8SViM', 'name': 'Artist Two', 'type': 'artist', 'uri': 'spotify:artist:2QnFHcqYp9crRPNxa8SViM' } ], 'album': { 'images': [ { 'url': ( 'https://i.scdn.co/image/' 'ab67616d0000b273208500450dcd0fd294d7bd3b' ), 'height': 640, 'width': 640 }, { 'url': ( 'https://i.scdn.co/image/' 'ab67616d00001e02208500450dcd0fd294d7bd3b' ), 'height': 300, 'width': 300 } ] } } ] } } def test_submit_spotify_api_track_artists_search_response_artists( producer, spotify_search_track_by_isrc_request, spotify_track_search_response): """Test function produces artists.""" request = spotify_search_track_by_isrc_request submit_spotify_api_track_artists_search_response( request, spotify_track_search_response ) track_data = spotify_track_search_response['tracks']['items'][0] artist_1 = SpotifyArtist( id=track_data['artists'][0]['id'], name=track_data['artists'][0]['name']) artist_message_1 = SpotifySearchTrackArtistMessage( api_request=request, artist=artist_1) artist_2 = SpotifyArtist( id=track_data['artists'][1]['id'], name=track_data['artists'][1]['name']) artist_message_2 = SpotifySearchTrackArtistMessage( api_request=request, artist=artist_2) producer.produce.assert_has_calls([ call( 'event.spotifyapi.track.artist', f'isrc-{request.payload.isrc}', artist_message_1.model_dump_json(), auto_flush=False), call( 'event.spotifyapi.track.artist', f'isrc-{request.payload.isrc}', artist_message_2.model_dump_json(), auto_flush=False), ]) def test_submit_spotify_api_track_artists_search_response_default( producer, spotify_search_track_by_isrc_request, spotify_track_search_response): """Test function with default parameters.""" request = spotify_search_track_by_isrc_request submit_spotify_api_track_artists_search_response( request, spotify_track_search_response) # Should produce track and artist messages assert producer.produce.call_count >= 2