from unittest.mock import MagicMock import pytest from pytest_mock import MockerFixture from app.dsp.backends.base import DSPBackend from app.dsp.exceptions import DSPNotRegisteredError from app.dsp.gateway import DSPGateway from app.dsp.guard import RateLimitGuard from app.dsp.types import ( ProfileResult, RecentlyPlayedResult, TokenResult, TopArtistsResult, ) class TestDSPGateway: @pytest.fixture def gateway(self) -> DSPGateway: return DSPGateway() @pytest.fixture def backend(self, mocker: MockerFixture) -> MagicMock: return mocker.MagicMock(spec=DSPBackend) def test_is_configured_false_before_register(self, gateway: DSPGateway) -> None: assert not gateway.is_configured("spotify") def test_is_configured_true_after_register( self, gateway: DSPGateway, backend: MagicMock ) -> None: gateway.register("spotify", backend) assert gateway.is_configured("spotify") def test_get_returns_registered_backend( self, gateway: DSPGateway, backend: MagicMock ) -> None: gateway.register("spotify", backend) assert gateway.get("spotify") is backend def test_get_raises_for_unknown_backend(self, gateway: DSPGateway) -> None: with pytest.raises(DSPNotRegisteredError) as exc_info: gateway.get("unknown") assert exc_info.value.name == "unknown" def test_refresh_token_delegates_to_backend( self, gateway: DSPGateway, backend: MagicMock ) -> None: backend.refresh_token.return_value = TokenResult( access_token="new", refresh_token="ref" ) gateway.register("spotify", backend) result = gateway.refresh_token("spotify", "old_ref") backend.refresh_token.assert_called_once_with("old_ref") assert result.access_token == "new" def test_get_profile_delegates_to_backend( self, gateway: DSPGateway, backend: MagicMock ) -> None: backend.get_profile.return_value = ProfileResult(email="user@example.com") gateway.register("spotify", backend) result = gateway.get_profile("spotify", "acc") backend.get_profile.assert_called_once_with("acc") assert result.email == "user@example.com" def test_get_top_artists_delegates_to_backend( self, gateway: DSPGateway, backend: MagicMock ) -> None: backend.get_top_artists.return_value = TopArtistsResult() gateway.register("spotify", backend) result = gateway.get_top_artists("spotify", "acc") backend.get_top_artists.assert_called_once_with("acc") assert result.items == [] def test_get_recently_played_delegates_to_backend( self, gateway: DSPGateway, backend: MagicMock ) -> None: backend.get_recently_played.return_value = RecentlyPlayedResult() gateway.register("spotify", backend) result = gateway.get_recently_played("spotify", "acc") backend.get_recently_played.assert_called_once_with("acc", after=None) assert result.items == [] def test_stats_returns_none_without_guard( self, gateway: DSPGateway, backend: MagicMock ) -> None: gateway.register("spotify", backend) assert gateway.stats("spotify") is None def test_stats_returns_guard_stats( self, gateway: DSPGateway, backend: MagicMock ) -> None: guard = RateLimitGuard() guard.execute(lambda: None) gateway.register("spotify", backend, guard=guard) stats = gateway.stats("spotify") assert stats is not None assert stats.requests == 1 def test_call_routes_through_guard( self, gateway: DSPGateway, backend: MagicMock, mocker: MockerFixture ) -> None: guard = mocker.MagicMock(spec=RateLimitGuard) guard.execute.side_effect = lambda fn: fn() backend.get_profile.return_value = ProfileResult() gateway.register("spotify", backend, guard=guard) gateway.get_profile("spotify", "acc") guard.execute.assert_called_once() def test_close_calls_close_on_all_backends( self, gateway: DSPGateway, mocker: MockerFixture ) -> None: backend_1 = mocker.MagicMock(spec=DSPBackend) backend_2 = mocker.MagicMock(spec=DSPBackend) gateway.register("spotify_a", backend_1) gateway.register("spotify_b", backend_2) gateway.close() backend_1.close.assert_called_once() backend_2.close.assert_called_once()