/
/
/
1"""Tests for the Spotify provider setup and legacy-token migration."""
2
3from __future__ import annotations
4
5from unittest.mock import MagicMock
6
7import pytest
8from music_assistant_models.errors import LoginFailed
9
10from music_assistant.constants import ENCRYPT_SUFFIX
11from music_assistant.providers.spotify import setup
12from music_assistant.providers.spotify.constants import (
13 CONF_CLIENT_ID,
14 CONF_REFRESH_TOKEN_DEPRECATED,
15 CONF_REFRESH_TOKEN_DEV,
16 CONF_REFRESH_TOKEN_GLOBAL,
17)
18
19
20def _fake_encrypt(value: str) -> str:
21 """Mirror ConfigController.encrypt_string: prefix once, idempotent for encrypted values."""
22 return value if value.startswith(ENCRYPT_SUFFIX) else ENCRYPT_SUFFIX + value
23
24
25async def test_setup_migrates_legacy_global_token(monkeypatch: pytest.MonkeyPatch) -> None:
26 """
27 A legacy token (no custom client id) is migrated into setup_data under the global key.
28
29 The provider isn't registered during setup, so the migration writes straight to the
30 setup_data store; the presence check then passes without forcing re-auth.
31 """
32 values = {CONF_REFRESH_TOKEN_DEPRECATED: "legacy_tok"}
33 config = MagicMock(instance_id="spotify--test")
34 config.get_value = MagicMock(side_effect=lambda key, default=None: values.get(key, default))
35 config.setup_data = {}
36 mass = MagicMock()
37 mass.config.encrypt_string = MagicMock(side_effect=lambda value: value)
38 mass.config.set = MagicMock()
39 mass.config.set_raw_provider_config_value = MagicMock()
40 sentinel = object()
41 monkeypatch.setattr(
42 "music_assistant.providers.spotify.SpotifyProvider", MagicMock(return_value=sentinel)
43 )
44 result = await setup(mass, MagicMock(), config)
45 assert result is sentinel
46 # legacy token migrated into setup_data under the global key (encrypt is identity here)
47 mass.config.set.assert_any_call(
48 f"providers/spotify--test/setup_data/{CONF_REFRESH_TOKEN_GLOBAL}", "legacy_tok"
49 )
50 assert config.setup_data[CONF_REFRESH_TOKEN_GLOBAL] == "legacy_tok"
51 # the deprecated legacy key is cleared once migrated
52 mass.config.set_raw_provider_config_value.assert_any_call(
53 "spotify--test", CONF_REFRESH_TOKEN_DEPRECATED, None
54 )
55
56
57async def test_setup_migrates_legacy_dev_token_with_migrated_client_id(
58 monkeypatch: pytest.MonkeyPatch,
59) -> None:
60 """
61 A legacy token is recognised as a developer one from a client id already in setup_data.
62
63 The setup_data migration moves the client id out of the config values, so the legacy
64 split has to find it there; the stored value is encrypted and must reach setup_data
65 unchanged rather than encrypted a second time.
66 """
67 encrypted_client_id = _fake_encrypt("my-client-id")
68 values = {CONF_REFRESH_TOKEN_DEPRECATED: "legacy_tok"}
69 config = MagicMock(instance_id="spotify--test")
70 config.get_value = MagicMock(side_effect=lambda key, default=None: values.get(key, default))
71 config.setup_data = {CONF_CLIENT_ID: encrypted_client_id}
72 mass = MagicMock()
73 mass.config.encrypt_string = MagicMock(side_effect=_fake_encrypt)
74 mass.config.set = MagicMock()
75 mass.config.set_raw_provider_config_value = MagicMock()
76 # patched so that misreading the client id fails the raises check below rather than
77 # crashing on a MagicMock somewhere inside the provider it would then construct
78 monkeypatch.setattr(
79 "music_assistant.providers.spotify.SpotifyProvider", MagicMock(return_value=object())
80 )
81 # a developer-only legacy token leaves no global token, so re-auth is still required
82 with pytest.raises(LoginFailed):
83 await setup(mass, MagicMock(), config)
84 base = "providers/spotify--test/setup_data"
85 mass.config.set.assert_any_call(f"{base}/{CONF_REFRESH_TOKEN_DEV}", _fake_encrypt("legacy_tok"))
86 assert config.setup_data[CONF_REFRESH_TOKEN_DEV] == _fake_encrypt("legacy_tok")
87 # the already encrypted client id survives the round trip untouched
88 mass.config.set.assert_any_call(f"{base}/{CONF_CLIENT_ID}", encrypted_client_id)
89 assert config.setup_data[CONF_CLIENT_ID] == encrypted_client_id
90