/
/
/
1"""Unit tests for the provider options surface (get_config_entries)."""
2
3from __future__ import annotations
4
5import json
6from typing import TYPE_CHECKING
7from unittest.mock import AsyncMock, Mock
8
9from music_assistant_models.enums import ConfigEntryType
10
11from music_assistant.providers.yandex_music.constants import (
12 CONF_ACTION_SAVE_WAVE_PRESET,
13 CONF_BASE_URL,
14 CONF_LIKED_TRACKS_MAX_TRACKS,
15 CONF_MY_WAVE_MAX_TRACKS,
16 CONF_QUALITY,
17 CONF_RESTRICTIVE_RATE_LIMITS,
18 CONF_WAVE_PRESET_DRAFT_NAME,
19 CONF_WAVE_PRESETS_DATA,
20)
21from music_assistant.providers.yandex_music.provider import YandexMusicProvider
22
23if TYPE_CHECKING:
24 from music_assistant_models.config_entries import ConfigValueType
25
26_AUTH_KEYS = frozenset(
27 {
28 "auth_device",
29 "auth_qr",
30 "clear_auth",
31 "remember_session",
32 "token",
33 "x_token",
34 "refresh_token",
35 "label_text",
36 "session_id",
37 }
38)
39
40
41def _provider(stored: dict[str, ConfigValueType] | None = None) -> Mock:
42 """Build a provider stub backed by a mutable config dict."""
43 values = stored if stored is not None else {}
44 provider = Mock(spec=YandexMusicProvider)
45 provider.get_config_value = Mock(
46 side_effect=lambda key, default=None, **_kw: values.get(key, default)
47 )
48
49 def _update(key: str, value: ConfigValueType, **_kw: object) -> None:
50 values[key] = value
51
52 provider._update_config_value = Mock(side_effect=_update)
53 return provider
54
55
56async def test_get_config_entries_has_no_auth_entries_or_actions() -> None:
57 """Authentication moved to the setup flow: no auth entries/actions are emitted."""
58 entries = await YandexMusicProvider.get_config_entries(_provider())
59 keys = {e.key for e in entries}
60 assert keys.isdisjoint(_AUTH_KEYS)
61 actions = {e.action for e in entries if e.action}
62 assert "auth_device" not in actions
63 assert "auth_qr" not in actions
64 assert "clear_auth" not in actions
65
66
67async def test_get_config_entries_has_advanced_token_replacement() -> None:
68 """A new token can be supplied once without exposing the stored credential."""
69 entries = await YandexMusicProvider.get_config_entries(_provider())
70 entry = next(item for item in entries if item.key == "manual_token")
71
72 assert entry.type == ConfigEntryType.SECURE_STRING
73 assert entry.required is False
74 assert entry.advanced is True
75 assert entry.requires_reload is True
76 assert entry.value is None
77
78
79async def test_get_config_entries_keeps_genuine_options() -> None:
80 """The genuine playback options remain on the options surface."""
81 entries = await YandexMusicProvider.get_config_entries(_provider())
82 keys = {e.key for e in entries}
83 assert {
84 CONF_QUALITY,
85 CONF_MY_WAVE_MAX_TRACKS,
86 CONF_LIKED_TRACKS_MAX_TRACKS,
87 CONF_BASE_URL,
88 CONF_RESTRICTIVE_RATE_LIMITS,
89 } <= keys
90
91
92async def test_get_config_entries_keeps_wave_preset_builder() -> None:
93 """The My Wave preset builder stays on the options surface."""
94 entries = await YandexMusicProvider.get_config_entries(_provider())
95 actions = {e.action for e in entries if e.action}
96 assert "save_wave_preset" in actions
97 assert "delete_wave_preset" in actions
98
99
100async def test_save_wave_preset_action_still_handled() -> None:
101 """The save action persists the draft and clears its name."""
102 stored: dict[str, ConfigValueType] = {
103 CONF_WAVE_PRESET_DRAFT_NAME: "Focus",
104 CONF_WAVE_PRESETS_DATA: "",
105 }
106 provider = _provider(stored)
107 provider.get_config_entries = AsyncMock(return_value=())
108
109 await YandexMusicProvider.handle_config_action(provider, CONF_ACTION_SAVE_WAVE_PRESET)
110
111 saved = json.loads(str(stored[CONF_WAVE_PRESETS_DATA]))
112 assert [p["name"] for p in saved] == ["Focus"]
113 assert stored[CONF_WAVE_PRESET_DRAFT_NAME] is None
114