"""Unit tests for the Spotify service layer.""" import pytest from pytest_mock import MockerFixture from contributor.connectors.spotify import SpotifyArtist pytestmark = pytest.mark.anyio @pytest.fixture def spotify_artist(): return SpotifyArtist( identifier="0OdUWJ0sBjDrqHygGUXeCF", name="Test Artist", followers=1000, genres=["pop", "rock"], url="https://open.spotify.com/artist/0OdUWJ0sBjDrqHygGUXeCF", image="https://i.scdn.co/image/abc123", ) @pytest.fixture def mock_redis(mocker: MockerFixture): redis_mock = mocker.AsyncMock() mocker.patch( "contributor.logic.service.spotify.datasources.get_redis_client", return_value=redis_mock, ) return redis_mock @pytest.fixture def mock_spotify_client(mocker: MockerFixture): return mocker.patch("contributor.logic.service.spotify._spotify_client") class TestArtistSearch: @pytest.fixture(autouse=True) def _setup(self, mock_redis, mock_spotify_client): self.redis = mock_redis self.client = mock_spotify_client async def test_returns_cached_results(self, spotify_artist: SpotifyArtist): self.redis.get.return_value = [spotify_artist.model_dump(mode="json")] from contributor.logic.service.spotify import artist_search result = await artist_search("test", limit=10, offset=0) assert len(result) == 1 assert result[0] == spotify_artist self.client.search.assert_not_called() async def test_calls_spotify_on_cache_miss(self, spotify_artist: SpotifyArtist): self.redis.get.return_value = None self.client.search.return_value = [spotify_artist] from contributor.logic.service.spotify import artist_search result = await artist_search("test", limit=10, offset=0) assert result == [spotify_artist] self.client.search.assert_called_once_with( q="test*", limit=10, offset=0, type="artist", locale="en", ) async def test_caches_response_after_fetch(self, spotify_artist: SpotifyArtist): self.redis.get.return_value = None self.client.search.return_value = [spotify_artist] from contributor.logic.service.spotify import artist_search await artist_search("test", limit=5, offset=0) self.redis.set.assert_called_once_with( "test,5,0,en", [spotify_artist.model_dump(mode="json")], ) async def test_uses_correct_redis_key_with_localization( self, spotify_artist: SpotifyArtist ): self.redis.get.return_value = None self.client.search.return_value = [spotify_artist] from contributor.logic.service.spotify import artist_search await artist_search("query", limit=10, offset=5, localization="fr") self.redis.get.assert_called_once_with("query,10,5,fr") async def test_raises_value_error_when_limit_offset_exceeds_max(self): from contributor.logic.service.spotify import artist_search with pytest.raises(ValueError, match="limit \\+ offset must not exceed 1000"): await artist_search("test", limit=900, offset=200) async def test_raises_value_error_at_boundary(self): from contributor.logic.service.spotify import artist_search with pytest.raises(ValueError): await artist_search("test", limit=1000, offset=1) async def test_does_not_raise_at_exact_limit(self, spotify_artist: SpotifyArtist): self.redis.get.return_value = None self.client.search.return_value = [] from contributor.logic.service.spotify import artist_search result = await artist_search("test", limit=1000, offset=0) assert result == [] class TestGetArtistById: @pytest.fixture(autouse=True) def _setup(self, mock_redis, mock_spotify_client): self.redis = mock_redis self.client = mock_spotify_client async def test_returns_cached_artist(self, spotify_artist: SpotifyArtist): self.redis.get.return_value = spotify_artist.model_dump(mode="json") from contributor.logic.service.spotify import get_artist_by_id result = await get_artist_by_id("0OdUWJ0sBjDrqHygGUXeCF") assert result == spotify_artist self.client.artist.assert_not_called() async def test_fetches_from_spotify_on_cache_miss( self, spotify_artist: SpotifyArtist ): self.redis.get.return_value = None self.client.artist.return_value = spotify_artist from contributor.logic.service.spotify import get_artist_by_id result = await get_artist_by_id("0OdUWJ0sBjDrqHygGUXeCF") assert result == spotify_artist self.client.artist.assert_called_once_with("0OdUWJ0sBjDrqHygGUXeCF") async def test_caches_artist_after_fetch(self, spotify_artist: SpotifyArtist): self.redis.get.return_value = None self.client.artist.return_value = spotify_artist from contributor.logic.service.spotify import get_artist_by_id await get_artist_by_id("0OdUWJ0sBjDrqHygGUXeCF") self.redis.set.assert_called_once_with( "0OdUWJ0sBjDrqHygGUXeCF", spotify_artist.model_dump(mode="json"), ) async def test_does_not_cache_when_artist_not_found(self): self.redis.get.return_value = None self.client.artist.return_value = None from contributor.logic.service.spotify import get_artist_by_id result = await get_artist_by_id("nonexistent1234567890") assert result is None self.redis.set.assert_not_called() class TestGetArtistsByIds: @pytest.fixture(autouse=True) def _setup(self, mock_redis, mock_spotify_client): self.redis = mock_redis self.client = mock_spotify_client async def test_returns_all_cached_artists(self, spotify_artist: SpotifyArtist): self.redis.get.return_value = spotify_artist.model_dump(mode="json") from contributor.logic.service.spotify import get_artists_by_ids result = await get_artists_by_ids(["0OdUWJ0sBjDrqHygGUXeCF"]) assert result == {"0OdUWJ0sBjDrqHygGUXeCF": spotify_artist} self.client.get_artists_by_ids.assert_not_called() async def test_fetches_uncached_ids_from_spotify( self, spotify_artist: SpotifyArtist ): cached_id = "0OdUWJ0sBjDrqHygGUXeCF" uncached_id = "1111111111111111111111" async def fake_get(key): if key == cached_id: return spotify_artist.model_dump(mode="json") return None self.redis.get.side_effect = fake_get uncached_artist_data = {"id": uncached_id, "name": "Other"} self.client.get_artists_by_ids.return_value = { uncached_id: uncached_artist_data } from contributor.logic.service.spotify import get_artists_by_ids result = await get_artists_by_ids([cached_id, uncached_id]) assert result[cached_id] == spotify_artist assert result[uncached_id] == uncached_artist_data self.client.get_artists_by_ids.assert_called_once_with(artist_ids=[uncached_id]) async def test_does_not_call_spotify_when_all_cached( self, spotify_artist: SpotifyArtist ): self.redis.get.return_value = spotify_artist.model_dump(mode="json") from contributor.logic.service.spotify import get_artists_by_ids await get_artists_by_ids(["0OdUWJ0sBjDrqHygGUXeCF"]) self.client.get_artists_by_ids.assert_not_called() async def test_empty_list_returns_empty_dict(self): from contributor.logic.service.spotify import get_artists_by_ids result = await get_artists_by_ids([]) assert result == {} self.client.get_artists_by_ids.assert_not_called() async def test_caches_fetched_artists(self): self.redis.get.return_value = None artist_data = {"id": "0OdUWJ0sBjDrqHygGUXeCF", "name": "Fetched"} self.client.get_artists_by_ids.return_value = { "0OdUWJ0sBjDrqHygGUXeCF": artist_data } from contributor.logic.service.spotify import get_artists_by_ids await get_artists_by_ids(["0OdUWJ0sBjDrqHygGUXeCF"]) self.redis.set.assert_called_once_with("0OdUWJ0sBjDrqHygGUXeCF", artist_data)