/
/
/
1"""Tests for the strings.json localization contract (spec 0003)."""
2
3from __future__ import annotations
4
5import json
6from typing import TYPE_CHECKING, Any, cast
7from unittest import mock
8
9from music_assistant.providers.yandex_music.constants import (
10 QUALITY_BALANCED,
11 QUALITY_EFFICIENT,
12 QUALITY_HIGH,
13 QUALITY_SUPERB,
14)
15from music_assistant.providers.yandex_music.provider import YandexMusicProvider
16
17from .conftest import provider_dir
18
19if TYPE_CHECKING:
20 from music_assistant_models.config_entries import ConfigEntry
21
22_PROVIDER_DIR = provider_dir()
23
24# The auth status label is dynamic (three states); it keeps an English
25# fallback in code and localizes via per-state translation keys. The
26# unofficial-provider note is a shared entry owned by the MA server and
27# authored in its common strings, not per provider.
28_DYNAMIC_LABEL_KEYS = {"label_text", "unofficial_provider_note"}
29
30
31def _load_strings() -> dict[str, Any]:
32 """Load the provider's strings.json authoring file."""
33 data = json.loads((_PROVIDER_DIR / "strings.json").read_text(encoding="utf-8"))
34 assert isinstance(data, dict)
35 return data
36
37
38async def _get_entries() -> tuple[ConfigEntry, ...]:
39 """Collect the provider option config entries (auth now lives in the setup flow)."""
40 provider = mock.MagicMock(spec=YandexMusicProvider)
41 provider.get_config_value = mock.MagicMock(
42 side_effect=lambda _key, default=None, **_kw: default
43 )
44 return await YandexMusicProvider.get_config_entries(provider)
45
46
47async def test_strings_json_covers_config_entries() -> None:
48 """Every config entry key is authored in strings.json config_entries."""
49 strings = _load_strings()
50 authored = strings["config_entries"]
51 entries = await _get_entries()
52 missing = [e.key for e in entries if e.key not in _DYNAMIC_LABEL_KEYS and e.key not in authored]
53 assert not missing, f"config entries missing from strings.json: {missing}"
54
55
56async def test_strings_json_has_media_and_manifest_sections() -> None:
57 """strings.json ships the media and manifest sections upstream authored."""
58 strings = _load_strings()
59 assert "folder" in strings["media"]
60 assert "playlist" in strings["media"]
61 assert "recommendations" in strings["media"]
62 assert strings["manifest"]["description"]
63
64
65def test_strings_json_covers_manual_auth_controls() -> None:
66 """Manual setup and replacement controls have complete user-facing guidance."""
67 strings = _load_strings()
68 config_entries = strings["config_entries"]
69 method = config_entries["method"]
70
71 assert method["options"]["token"]
72 assert config_entries["token"]["label"]
73 assert "refresh" in str(config_entries["token"]["description"]).lower()
74 assert config_entries["manual_token"]["label"]
75 replacement_description = str(config_entries["manual_token"]["description"])
76 assert "empty" in replacement_description.lower()
77 assert "Reconfigure" in replacement_description
78
79
80def test_device_flow_copy_describes_browser_confirmation() -> None:
81 """Device Flow tells users to enter the code on the shown web page."""
82 device_login = _load_strings()["setup_flow"]["device_login"]
83 description = str(device_login["description"])
84 progress_text = str(device_login["progress_text"])
85
86 assert "address shown" in description.lower()
87 assert "enter the code" in progress_text.lower()
88 assert "verification page" in progress_text.lower()
89 assert "app" not in progress_text.lower()
90
91
92async def test_config_entries_have_no_hardcoded_labels() -> None:
93 """Static entries author their text in strings.json, not in code."""
94 entries = await _get_entries()
95 offenders = [
96 e.key
97 for e in entries
98 if e.key not in _DYNAMIC_LABEL_KEYS and (e.label or e.description or e.action_label)
99 ]
100 assert not offenders, f"entries with hardcoded user-facing text: {offenders}"
101
102
103async def test_quality_options_use_value_first_signature() -> None:
104 """
105 Quality options store the QUALITY_* constants as their values.
106
107 Guards against the legacy (title, value) positional order, which the
108 current models interpret as value=title â silently corrupting the
109 stored setting.
110 """
111 entries = await _get_entries()
112 quality = next(e for e in entries if e.key == "quality")
113 assert quality.options is not None
114 values = {o.value for o in quality.options}
115 assert values == {
116 QUALITY_EFFICIENT,
117 QUALITY_BALANCED,
118 QUALITY_HIGH,
119 QUALITY_SUPERB,
120 }
121
122
123def _media_label_provider(authored: str | None) -> mock.MagicMock:
124 """Build a provider stand-in whose translations lookup returns *authored*."""
125 provider = mock.MagicMock()
126 provider.domain = "yandex_music"
127 provider.mass.translations.get_translation.return_value = authored
128 return provider
129
130
131async def test_media_label_falls_back_verbatim() -> None:
132 """Unauthored media keys keep their (already localized) name verbatim."""
133 provider = _media_label_provider(None)
134 name, translation_key = YandexMusicProvider._media_label(
135 cast("YandexMusicProvider", provider), "folder", "landing_tag_rock", "Рок"
136 )
137 assert name == "Рок"
138 assert translation_key is None
139
140
141async def test_media_label_returns_authored_name_and_key() -> None:
142 """Authored media keys resolve to the English source name plus the key."""
143 provider = _media_label_provider("My Wave")
144 name, translation_key = YandexMusicProvider._media_label(
145 cast("YandexMusicProvider", provider), "playlist", "my_wave", "fallback"
146 )
147 assert name == "My Wave"
148 assert translation_key == "my_wave"
149