File size: 4,829 Bytes
93a887a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
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"])