spotify-mcp-server / tests /test_models.py
LeoWalker's picture
Implement nine-tool Spotify MCP v2 suite (#3)
93a887a unverified
Raw
History Blame Contribute Delete
4.83 kB
from __future__ import annotations
import pytest
from pydantic import ValidationError
from spotify_mcp_server.tools.common import SpotifyReferenceInput, SpotifyType
from spotify_mcp_server.tools.models import (
GetItemInput,
LibraryAction,
ListeningActivityInput,
PlayerAction,
PlayerStatusInput,
PlaylistAction,
PlaylistReadRequest,
SearchCatalogInput,
)
@pytest.mark.parametrize(
("supplied", "expected_type", "expected_id"),
[
(
SpotifyReferenceInput(value="spotify:track:abc123"),
SpotifyType.TRACK,
"abc123",
),
(
SpotifyReferenceInput(value="https://open.spotify.com/album/def456?si=x"),
SpotifyType.ALBUM,
"def456",
),
(
SpotifyReferenceInput(value="https://open.spotify.com/intl-fr/show/ghi789"),
SpotifyType.SHOW,
"ghi789",
),
(SpotifyReferenceInput(value="jkl012", type="artist"), SpotifyType.ARTIST, "jkl012"),
],
)
def test_reference_normalization(supplied, expected_type, expected_id) -> None:
result = supplied.normalized()
assert result.type == expected_type
assert result.id == expected_id
assert result.uri == f"spotify:{expected_type}:{expected_id}"
@pytest.mark.parametrize(
"supplied",
[
SpotifyReferenceInput(value="bareid"),
SpotifyReferenceInput(value="https://example.com/track/abc"),
SpotifyReferenceInput(value="https://open.spotify.com/abc"),
SpotifyReferenceInput(value="spotify:track:bad-id"),
],
)
def test_reference_rejects_ambiguous_or_invalid_values(supplied) -> None:
with pytest.raises(ValueError):
supplied.normalized()
def test_reference_rejects_disallowed_type() -> None:
with pytest.raises(ValueError, match="must be one of"):
SpotifyReferenceInput(value="spotify:track:abc").normalized(allowed={SpotifyType.ALBUM})
def test_strict_search_contract_and_duplicate_types() -> None:
with pytest.raises(ValidationError, match="extra_forbidden"):
SearchCatalogInput(query="ambient", types=["track"], typo=True)
with pytest.raises(ValidationError, match="duplicates"):
SearchCatalogInput(query="ambient", types=["track", "track"])
with pytest.raises(ValidationError):
SearchCatalogInput(query="ambient", types=["track"], limit=11)
def test_action_specific_validation() -> None:
with pytest.raises(ValidationError, match="position_ms"):
PlayerAction(action="seek")
with pytest.raises(ValidationError, match="not both"):
PlayerAction(action="start_playback", context_uri="spotify:album:a", uris=["x"])
with pytest.raises(ValidationError, match="one offset"):
PlayerAction(action="start_playback", offset_uri="x", offset_position=1)
assert PlayerAction(action="pause_playback", context_uri="x", uris=["y"])
with pytest.raises(ValidationError, match="name is required"):
PlaylistAction(action="create_playlist")
with pytest.raises(ValidationError, match="playlist is required"):
PlaylistAction(action="add_items", uris=[])
with pytest.raises(ValidationError, match="range_start"):
PlaylistAction(
action="reorder_items",
playlist=SpotifyReferenceInput(value="spotify:playlist:p"),
range_start=1,
)
with pytest.raises(ValidationError, match="at least one detail"):
PlaylistAction(
action="change_playlist_details",
playlist=SpotifyReferenceInput(value="spotify:playlist:p"),
)
with pytest.raises(ValidationError, match="must not be empty"):
PlaylistAction(
action="remove_items",
playlist=SpotifyReferenceInput(value="spotify:playlist:p"),
uris=[],
)
def test_library_and_read_validation() -> None:
with pytest.raises(ValidationError, match="does not support"):
LibraryAction(action="follow", items=[SpotifyReferenceInput(value="spotify:track:t")])
with pytest.raises(ValidationError, match="required for get_playlists"):
PlaylistReadRequest(operation="get_playlists")
with pytest.raises(ValidationError, match="mutually exclusive"):
ListeningActivityInput(before=1, after=2)
def test_duplicate_read_sections_are_rejected() -> None:
with pytest.raises(ValidationError, match="expansions"):
GetItemInput(
items=[SpotifyReferenceInput(value="spotify:album:a")],
expansions=["album_tracks", "album_tracks"],
)
with pytest.raises(ValidationError, match="include"):
PlayerStatusInput(include=["devices", "devices"])
with pytest.raises(ValidationError, match="time_ranges"):
ListeningActivityInput(time_ranges=["short_term", "short_term"])