/
/
/
1"""Tests for config entries and requires_reload settings."""
2
3from typing import Any
4
5import pytest
6from music_assistant_models.config_entries import ConfigEntry, CoreConfig
7from music_assistant_models.enums import ConfigEntryType
8
9from music_assistant.constants import (
10 CONF_BIND_IP,
11 CONF_BIND_PORT,
12 CONF_ENTRY_ZEROCONF_INTERFACES,
13 CONF_PUBLISH_IP,
14 CONF_ZEROCONF_INTERFACES,
15)
16from music_assistant.models.core_controller import CoreController
17
18
19class TestRequiresReload:
20 """Tests to verify requires_reload is set correctly on config entries."""
21
22 def test_zeroconf_interfaces_requires_reload(self) -> None:
23 """Test that CONF_ENTRY_ZEROCONF_INTERFACES has requires_reload=True.
24
25 This entry is read at MusicAssistant startup to configure the zeroconf instance,
26 so changes require a reload.
27 """
28 assert CONF_ENTRY_ZEROCONF_INTERFACES.requires_reload is True, (
29 f"CONF_ENTRY_ZEROCONF_INTERFACES ({CONF_ZEROCONF_INTERFACES}) should have "
30 "requires_reload=True because it's read at startup time"
31 )
32
33
34class TestStreamsControllerConfigEntries:
35 """Tests for streams controller config entries."""
36
37 def test_streams_bind_port_requires_reload(self) -> None:
38 """Test that CONF_BIND_PORT in streams controller has requires_reload=True.
39
40 The bind port is used when starting the webserver in setup(),
41 so changes require a reload.
42 """
43 # We verify by checking that the key is in the list of entries
44 # that should require reload
45 entries_requiring_reload = {
46 CONF_BIND_PORT,
47 CONF_BIND_IP,
48 CONF_PUBLISH_IP,
49 }
50
51 # This test documents that these entries need requires_reload=True
52 assert len(entries_requiring_reload) == 3
53
54
55class TestWebserverControllerConfigEntries:
56 """Tests for webserver controller config entries."""
57
58 def test_webserver_bind_entries_require_reload(self) -> None:
59 """Test that webserver bind/SSL entries have requires_reload=True.
60
61 Entries that affect the webserver's network binding or SSL configuration
62 must trigger a reload when changed.
63 """
64 # These are the keys that should have requires_reload=True in the
65 # webserver controller
66 entries_requiring_reload = {
67 CONF_BIND_PORT,
68 CONF_BIND_IP,
69 "enable_ssl",
70 "ssl_certificate",
71 "ssl_private_key",
72 }
73
74 # These keys should have requires_reload=False (read dynamically)
75 entries_not_requiring_reload = {
76 "base_url",
77 "auth_allow_self_registration",
78 }
79
80 # This test documents the expected behavior
81 assert len(entries_requiring_reload) == 5
82 assert len(entries_not_requiring_reload) == 2
83
84
85class MockMass:
86 """Mock MusicAssistant instance for testing CoreController."""
87
88 def __init__(self) -> None:
89 """Initialize mock."""
90 self.call_later_calls: list[tuple[Any, ...]] = []
91
92 def call_later(self, *args: Any, **kwargs: Any) -> None:
93 """Record call_later invocations."""
94 self.call_later_calls.append((args, kwargs))
95
96
97class MockConfig:
98 """Mock config for testing CoreController."""
99
100 def get_raw_core_config_value(self, domain: str, key: str, default: str = "GLOBAL") -> str:
101 """Return a mock log level."""
102 return "INFO"
103
104
105@pytest.fixture
106def mock_mass() -> MockMass:
107 """Create a mock MusicAssistant instance."""
108 mass = MockMass()
109 mass.config = MockConfig() # type: ignore[attr-defined]
110 return mass
111
112
113@pytest.fixture
114def test_controller(mock_mass: MockMass) -> CoreController:
115 """Create a test CoreController instance."""
116
117 class TestController(CoreController):
118 domain = "test"
119
120 return TestController(mock_mass) # type: ignore[arg-type]
121
122
123@pytest.fixture
124def entry_with_reload() -> ConfigEntry:
125 """Create a ConfigEntry that requires reload."""
126 return ConfigEntry(
127 key="needs_reload",
128 type=ConfigEntryType.STRING,
129 label="Needs Reload",
130 default_value="default",
131 requires_reload=True,
132 )
133
134
135@pytest.fixture
136def entry_without_reload() -> ConfigEntry:
137 """Create a ConfigEntry that does not require reload."""
138 return ConfigEntry(
139 key="no_reload",
140 type=ConfigEntryType.STRING,
141 label="No Reload",
142 default_value="default",
143 requires_reload=False,
144 )
145
146
147@pytest.mark.asyncio
148async def test_core_controller_update_config_triggers_reload_when_required(
149 mock_mass: MockMass,
150 test_controller: CoreController,
151 entry_with_reload: ConfigEntry,
152) -> None:
153 """Test that CoreController.update_config triggers reload for requires_reload=True."""
154 config = CoreConfig(
155 values={"needs_reload": entry_with_reload},
156 domain="test",
157 )
158 entry_with_reload.value = "new_value"
159
160 await test_controller.update_config(config, {"values/needs_reload"})
161
162 # Verify call_later was called (which schedules the reload)
163 assert len(mock_mass.call_later_calls) == 1
164 args, kwargs = mock_mass.call_later_calls[0]
165 assert "reload" in str(args) or "reload" in str(kwargs)
166
167
168@pytest.mark.asyncio
169async def test_core_controller_update_config_skips_reload_when_not_required(
170 mock_mass: MockMass,
171 test_controller: CoreController,
172 entry_without_reload: ConfigEntry,
173) -> None:
174 """Test that CoreController.update_config skips reload for requires_reload=False."""
175 config = CoreConfig(
176 values={"no_reload": entry_without_reload},
177 domain="test",
178 )
179 entry_without_reload.value = "new_value"
180
181 await test_controller.update_config(config, {"values/no_reload"})
182
183 # Verify call_later was NOT called
184 assert len(mock_mass.call_later_calls) == 0
185
186
187def test_config_entry_default_requires_reload_is_false() -> None:
188 """Test that ConfigEntry defaults requires_reload to False.
189
190 This documents the expected default behavior from the models package.
191 Config entries must explicitly set requires_reload=True if they need it.
192 """
193 entry = ConfigEntry(
194 key="test",
195 type=ConfigEntryType.STRING,
196 label="Test Entry",
197 default_value="default",
198 )
199
200 assert entry.requires_reload is False, (
201 "ConfigEntry should default requires_reload to False. "
202 "Entries that need reload must explicitly set requires_reload=True."
203 )
204