Skip to main content
Glama

Spotify MCP Server

test_fastmcp_tools.py14.7 kB
""" Tests for FastMCP server tools. """ import pytest from spotipy import SpotifyException # Import the FastMCP tools directly from spotify_mcp.fastmcp_server import ( PlaybackState, Playlist, Track, add_to_queue, add_tracks_to_playlist, create_playlist, get_playlist_info, get_playlist_tracks, get_queue, get_track_info, get_user_playlists, playback_control, search_tracks, ) class TestPlaybackControl: """Test playback control tool.""" def test_get_playback_state(self, mock_spotify_api, sample_playback_data): """Test getting current playback state.""" mock_spotify_api.current_user_playing_track.return_value = sample_playback_data result = playback_control("get") assert isinstance(result, PlaybackState) assert result.is_playing assert result.track is not None assert result.track.name == "Never Gonna Give You Up" mock_spotify_api.current_user_playing_track.assert_called_once() def test_start_playback(self, mock_spotify_api, sample_playback_data): """Test starting playback.""" mock_spotify_api.current_user_playing_track.return_value = sample_playback_data result = playback_control("start") assert isinstance(result, PlaybackState) mock_spotify_api.start_playback.assert_called_once() mock_spotify_api.current_user_playing_track.assert_called() def test_pause_playback(self, mock_spotify_api, sample_playback_data): """Test pausing playback.""" mock_spotify_api.current_user_playing_track.return_value = sample_playback_data result = playback_control("pause") assert isinstance(result, PlaybackState) mock_spotify_api.pause_playback.assert_called_once() def test_skip_track(self, mock_spotify_api, sample_playback_data): """Test skipping to next track.""" mock_spotify_api.current_user_playing_track.return_value = sample_playback_data result = playback_control("skip") assert isinstance(result, PlaybackState) mock_spotify_api.next_track.assert_called_once() def test_invalid_action(self, mock_spotify_api): """Test invalid playback action.""" with pytest.raises(ValueError, match="Invalid action"): playback_control("invalid_action") class TestSearchTracks: """Test search functionality.""" def test_basic_search(self, mock_spotify_api, sample_search_results): """Test basic track search.""" mock_spotify_api.search.return_value = sample_search_results result = search_tracks("Never Gonna Give You Up") assert isinstance(result, dict) assert "items" in result assert len(result["items"]) == 1 assert isinstance(result["items"][0], Track) assert result["items"][0].name == "Never Gonna Give You Up" assert result["total"] == 1 assert result["limit"] == 10 assert result["offset"] == 0 mock_spotify_api.search.assert_called_once_with( q="Never Gonna Give You Up", type="track", limit=10, offset=0 ) def test_search_with_type(self, mock_spotify_api): """Test search with specific type.""" artist_results = { "artists": { "items": [ { "id": "artist123", "name": "Rick Astley", "external_urls": { "spotify": "https://open.spotify.com/artist/artist123" }, } ], "total": 1, "limit": 10, "offset": 0, "next": None, "previous": None, } } mock_spotify_api.search.return_value = artist_results result = search_tracks("Rick Astley", qtype="artist") assert isinstance(result, dict) assert "items" in result assert len(result["items"]) == 1 assert result["items"][0].name == "Rick Astley" mock_spotify_api.search.assert_called_with( q="Rick Astley", type="artist", limit=10, offset=0 ) def test_search_with_limit(self, mock_spotify_api, sample_search_results): """Test search with custom limit.""" mock_spotify_api.search.return_value = sample_search_results search_tracks("test", limit=5) mock_spotify_api.search.assert_called_with( q="test", type="track", limit=5, offset=0 ) def test_empty_results(self, mock_spotify_api): """Test search with no results.""" empty_results = { "tracks": { "items": [], "total": 0, "limit": 10, "offset": 0, "next": None, "previous": None, } } mock_spotify_api.search.return_value = empty_results result = search_tracks("nonexistent") assert isinstance(result, dict) assert "items" in result assert len(result["items"]) == 0 assert result["total"] == 0 class TestQueueManagement: """Test queue management.""" def test_add_to_queue(self, mock_spotify_api): """Test adding track to queue.""" result = add_to_queue("4iV5W9uYEdYUVa79Axb7Rh") assert isinstance(result, dict) assert result["status"] == "success" assert "Added track to queue" in result["message"] mock_spotify_api.add_to_queue.assert_called_once_with( "spotify:track:4iV5W9uYEdYUVa79Axb7Rh" ) def test_get_queue(self, mock_spotify_api, sample_track_data): """Test getting current queue.""" queue_data = { "currently_playing": sample_track_data, "queue": [sample_track_data], } mock_spotify_api.queue.return_value = queue_data result = get_queue() assert isinstance(result, dict) assert "currently_playing" in result assert "queue" in result assert isinstance(result["queue"], list) mock_spotify_api.queue.assert_called_once() def test_add_without_track_id(self, mock_spotify_api): """Test adding to queue without track ID.""" # add_to_queue requires track_id parameter, so this test is no longer applicable # The function signature enforces this requirement pass def test_invalid_action(self, mock_spotify_api): """Test invalid queue action.""" # This test is no longer applicable since we split into separate functions # No action parameter exists anymore pass class TestItemInfo: """Test getting item information.""" def test_get_track_info(self, mock_spotify_api, sample_track_data): """Test getting track information.""" mock_spotify_api.track.return_value = sample_track_data result = get_track_info("4iV5W9uYEdYUVa79Axb7Rh") assert isinstance(result, dict) # Returns dict, not Track object assert result["name"] == "Never Gonna Give You Up" assert result["artist"] == "Rick Astley" mock_spotify_api.track.assert_called_once_with("4iV5W9uYEdYUVa79Axb7Rh") def test_get_playlist_info(self, mock_spotify_api, sample_playlist_data): """Test getting playlist information.""" mock_spotify_api.playlist.return_value = sample_playlist_data result = get_playlist_info("37i9dQZF1DX0XUsuxWHRQd") assert isinstance(result, dict) # Returns dict, not Playlist object assert result["name"] == "RapCaviar" mock_spotify_api.playlist.assert_called_once_with( "37i9dQZF1DX0XUsuxWHRQd", fields="id,name,description,owner,public,tracks.total", ) def test_invalid_item_type(self, mock_spotify_api): """Test invalid item type.""" # This test is no longer applicable since we have specific functions # Type safety is enforced by having separate functions pass class TestCreatePlaylist: """Test playlist creation.""" def test_create_basic_playlist(self, mock_spotify_api, sample_playlist_data): """Test creating a basic playlist.""" mock_spotify_api.current_user.return_value = {"id": "testuser"} mock_spotify_api.user_playlist_create.return_value = sample_playlist_data result = create_playlist("My Test Playlist") assert isinstance(result, dict) assert result["name"] == "RapCaviar" # from sample data mock_spotify_api.user_playlist_create.assert_called_once_with( "testuser", "My Test Playlist", public=True, description="" ) def test_create_playlist_with_description( self, mock_spotify_api, sample_playlist_data ): """Test creating playlist with description.""" mock_spotify_api.current_user.return_value = {"id": "testuser"} mock_spotify_api.user_playlist_create.return_value = sample_playlist_data create_playlist("Test Playlist", description="Test description") mock_spotify_api.user_playlist_create.assert_called_with( "testuser", "Test Playlist", public=True, description="Test description" ) class TestAddTracksToPlaylist: """Test adding tracks to playlists.""" def test_add_single_track(self, mock_spotify_api): """Test adding single track to playlist.""" mock_spotify_api.playlist_add_items.return_value = {"snapshot_id": "test123"} result = add_tracks_to_playlist( "37i9dQZF1DX0XUsuxWHRQd", ["4iV5W9uYEdYUVa79Axb7Rh"] ) assert isinstance(result, dict) assert result["status"] == "success" assert "Added 1 tracks" in result["message"] mock_spotify_api.playlist_add_items.assert_called_once() def test_add_multiple_tracks(self, mock_spotify_api): """Test adding multiple tracks to playlist.""" mock_spotify_api.playlist_add_items.return_value = {"snapshot_id": "test123"} tracks = ["4iV5W9uYEdYUVa79Axb7Rh", "5iV5W9uYEdYUVa79Axb7Ri"] result = add_tracks_to_playlist("37i9dQZF1DX0XUsuxWHRQd", tracks) assert isinstance(result, dict) assert "Added 2 tracks" in result["message"] mock_spotify_api.playlist_add_items.assert_called_once() def test_add_empty_track_list(self, mock_spotify_api): """Test adding empty track list.""" result = add_tracks_to_playlist("37i9dQZF1DX0XUsuxWHRQd", []) # Should handle gracefully, not raise error assert isinstance(result, dict) assert "Added 0 tracks" in result["message"] class TestGetUserPlaylists: """Test getting user playlists.""" def test_get_playlists(self, mock_spotify_api, sample_playlist_data): """Test getting user playlists.""" mock_spotify_api.current_user_playlists.return_value = { "items": [sample_playlist_data] } result = get_user_playlists() assert isinstance(result, dict) assert "items" in result assert len(result["items"]) == 1 assert isinstance(result["items"][0], Playlist) assert result["items"][0].name == "RapCaviar" assert result["limit"] == 20 assert result["offset"] == 0 mock_spotify_api.current_user_playlists.assert_called_once_with( limit=20, offset=0 ) def test_get_playlists_with_limit(self, mock_spotify_api, sample_playlist_data): """Test getting playlists with limit.""" mock_spotify_api.current_user_playlists.return_value = { "items": [sample_playlist_data] } get_user_playlists(limit=10) mock_spotify_api.current_user_playlists.assert_called_with(limit=10, offset=0) class TestGetPlaylistTracks: """Test getting playlist tracks with pagination.""" def test_get_playlist_tracks_basic(self, mock_spotify_api, sample_track_data): """Test getting playlist tracks without pagination.""" # Mock playlist tracks response tracks_response = { "items": [{"track": sample_track_data}, {"track": sample_track_data}], "total": 2, "next": None, } mock_spotify_api.playlist_tracks.return_value = tracks_response # Mock playlist info response playlist_info = {"tracks": {"total": 2}} mock_spotify_api.playlist.return_value = playlist_info result = get_playlist_tracks("playlist123", limit=50) assert isinstance(result, dict) assert "items" in result assert len(result["items"]) == 2 assert result["total"] == 2 assert result["limit"] == 50 assert result["offset"] == 0 assert result["returned"] == 2 mock_spotify_api.playlist_tracks.assert_called_with( "playlist123", limit=50, offset=0 ) mock_spotify_api.playlist.assert_called_with( "playlist123", fields="tracks.total" ) def test_get_playlist_tracks_all(self, mock_spotify_api, sample_track_data): """Test getting all tracks from playlist.""" # Mock playlist tracks response tracks_response = { "items": [{"track": sample_track_data}], "total": 1, "next": None, } mock_spotify_api.playlist_tracks.return_value = tracks_response # Mock playlist info response playlist_info = {"tracks": {"total": 1}} mock_spotify_api.playlist.return_value = playlist_info result = get_playlist_tracks("playlist123", limit=None) assert result["limit"] is None assert result["total"] == 1 class TestErrorHandling: """Test error handling across tools.""" def test_spotify_exception_handling(self, mock_spotify_api): """Test handling of Spotify API exceptions.""" # Mock a Spotify API error mock_spotify_api.current_user_playing_track.side_effect = SpotifyException( 404, -1, "No active device found" ) # Should raise the handled error with pytest.raises( (SpotifyException, ValueError) ): # handle_spotify_error converts to different exception playback_control("get") def test_general_exception_handling(self, mock_spotify_api): """Test handling of general exceptions.""" mock_spotify_api.search.side_effect = Exception("Network error") # Should propagate the exception with pytest.raises(Exception, match="Network error"): search_tracks("test query")

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/jamiew/spotify-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server