/
/
/
1"""Tests for config resolution against the player's active audio output path."""
2
3from __future__ import annotations
4
5from typing import Any, cast
6from unittest.mock import MagicMock
7
8import pytest
9from music_assistant_models.config_entries import ConfigEntry, PlayerConfig
10from music_assistant_models.enums import ConfigEntryType, PlayerType
11
12from music_assistant.constants import CONF_HTTP_PROFILE
13from tests.common import MockPlayer, MockProvider
14
15
16@pytest.fixture
17def mock_mass() -> MagicMock:
18 """Create a mock MusicAssistant instance."""
19 mass = MagicMock()
20 mass.closing = False
21 mass.config.get = MagicMock(return_value=[])
22 mass.config.get_raw_player_config_value = MagicMock(return_value=None)
23 mass.config.get_raw_core_config_value = MagicMock(return_value="GLOBAL")
24 mass.config.set = MagicMock()
25 return mass
26
27
28def _http_profile_entry(default_value: str) -> ConfigEntry:
29 return ConfigEntry(
30 key=CONF_HTTP_PROFILE,
31 type=ConfigEntryType.STRING,
32 default_value=default_value,
33 )
34
35
36def _player_config(
37 player_id: str,
38 entries: list[ConfigEntry] | None = None,
39 values: dict[str, Any] | None = None,
40) -> PlayerConfig:
41 raw: dict[str, Any] = {"player_id": player_id, "provider": "test"}
42 if values:
43 raw["values"] = values
44 return cast("PlayerConfig", PlayerConfig.parse(entries or [], raw))
45
46
47def _make_parent_and_child(mock_mass: MagicMock) -> tuple[MockPlayer, MockPlayer]:
48 """Create a parent player and a protocol child wired into mass.players."""
49 provider = MockProvider("test", mass=mock_mass)
50 parent = MockPlayer(provider, "parent", "Parent")
51 child = MockPlayer(provider, "child", "Child", player_type=PlayerType.PROTOCOL)
52 players = {"parent": parent, "child": child}
53 mock_mass.players.get_player = MagicMock(side_effect=lambda pid: players.get(pid))
54 return parent, child
55
56
57class TestResolveOutputPlayer:
58 """Tests for Player.resolve_output_player."""
59
60 def test_returns_active_protocol_player(self, mock_mass: MagicMock) -> None:
61 """An active (non-native) output protocol resolves to the protocol player."""
62 parent, child = _make_parent_and_child(mock_mass)
63 parent.set_active_output_protocol("child")
64 assert parent.resolve_output_player() is child
65
66 def test_returns_self_for_native_output(self, mock_mass: MagicMock) -> None:
67 """Native output resolves to the player itself."""
68 parent, _child = _make_parent_and_child(mock_mass)
69 parent.set_active_output_protocol("native")
70 assert parent.resolve_output_player() is parent
71
72 def test_returns_self_without_active_protocol(self, mock_mass: MagicMock) -> None:
73 """No active output protocol resolves to the player itself."""
74 parent, _child = _make_parent_and_child(mock_mass)
75 assert parent.resolve_output_player() is parent
76
77 def test_returns_self_when_protocol_player_missing(self, mock_mass: MagicMock) -> None:
78 """A dangling active protocol id falls back to the player itself."""
79 parent, _child = _make_parent_and_child(mock_mass)
80 parent.set_active_output_protocol("ghost")
81 assert parent.resolve_output_player() is parent
82
83
84class TestGetOutputConfigValue:
85 """Tests for Player.get_output_config_value."""
86
87 def test_protocol_entry_default_beats_parent_value(self, mock_mass: MagicMock) -> None:
88 """The output player's provider entry default wins over the parent's stored value."""
89 parent, child = _make_parent_and_child(mock_mass)
90 parent.set_config(
91 _player_config(
92 "parent",
93 [_http_profile_entry("default")],
94 {CONF_HTTP_PROFILE: "chunked"},
95 )
96 )
97 child.set_config(_player_config("child", [_http_profile_entry("forced_content_length")]))
98 parent.set_active_output_protocol("child")
99
100 assert parent.get_output_config_value(CONF_HTTP_PROFILE, "default") == (
101 "forced_content_length"
102 )
103
104 def test_protocol_stored_value_wins(self, mock_mass: MagicMock) -> None:
105 """A value stored on the output player wins over its entry default."""
106 parent, child = _make_parent_and_child(mock_mass)
107 child.set_config(
108 _player_config(
109 "child",
110 [_http_profile_entry("forced_content_length")],
111 {CONF_HTTP_PROFILE: "no_content_length"},
112 )
113 )
114 parent.set_active_output_protocol("child")
115
116 assert parent.get_output_config_value(CONF_HTTP_PROFILE, "default") == "no_content_length"
117
118 def test_own_value_used_without_active_protocol(self, mock_mass: MagicMock) -> None:
119 """Without an active output protocol the player's own value applies."""
120 parent, _child = _make_parent_and_child(mock_mass)
121 parent.set_config(
122 _player_config(
123 "parent",
124 [_http_profile_entry("default")],
125 {CONF_HTTP_PROFILE: "chunked"},
126 )
127 )
128
129 assert parent.get_output_config_value(CONF_HTTP_PROFILE, "default") == "chunked"
130
131 def test_falls_back_to_own_value_when_protocol_lacks_entry(self, mock_mass: MagicMock) -> None:
132 """A key the output player has no entry for falls back to the player's own value."""
133 parent, child = _make_parent_and_child(mock_mass)
134 parent.set_config(
135 _player_config(
136 "parent",
137 [_http_profile_entry("default")],
138 {CONF_HTTP_PROFILE: "chunked"},
139 )
140 )
141 child.set_config(_player_config("child"))
142 parent.set_active_output_protocol("child")
143
144 assert parent.get_output_config_value(CONF_HTTP_PROFILE, "default") == "chunked"
145
146 def test_default_for_unknown_key(self, mock_mass: MagicMock) -> None:
147 """A key present on neither player resolves to the given default."""
148 parent, child = _make_parent_and_child(mock_mass)
149 parent.set_config(_player_config("parent"))
150 child.set_config(_player_config("child"))
151 parent.set_active_output_protocol("child")
152
153 assert parent.get_output_config_value(CONF_HTTP_PROFILE, "default") == "default"
154