music-assistant-server

6.3 KBPY
test_config_entries.py
6.3 KB204 lines • python
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