/
/
/
1"""
2Tests for Player.playback_domains and Player.get_linked_protocol.
3
4Covers the live availability view: native playback, linked protocol players that
5are (un)available, and wrapper players that only contribute their linked protocols.
6"""
7
8from __future__ import annotations
9
10from unittest.mock import MagicMock
11
12import pytest
13from music_assistant_models.enums import PlayerFeature, PlayerType
14
15from music_assistant.models.player import LinkedOutputProtocol
16from tests.common import MockPlayer, MockProvider
17
18
19@pytest.fixture
20def mock_mass() -> MagicMock:
21 """Create a mock MusicAssistant instance."""
22 mass = MagicMock()
23 mass.closing = False
24 mass.config.get = MagicMock(return_value=[])
25 mass.config.get_raw_player_config_value = MagicMock(return_value=None)
26 mass.config.get_raw_core_config_value = MagicMock(return_value="GLOBAL")
27 mass.config.set = MagicMock()
28 mass.players.get_player = MagicMock(return_value=None)
29 return mass
30
31
32def _make_native_player(mock_mass: MagicMock, domain: str = "sonos") -> MockPlayer:
33 """Create a native player (own playback path) of the given provider domain."""
34 provider = MockProvider(domain, instance_id=domain, mass=mock_mass)
35 player = MockPlayer(provider, "parent", "Parent")
36 player._attr_supported_features = {PlayerFeature.PLAY_MEDIA}
37 player._cache.clear()
38 return player
39
40
41def _link_protocol(
42 mock_mass: MagicMock,
43 parent: MockPlayer,
44 protocol_id: str,
45 domain: str,
46 available: bool,
47 needs_setup: bool = False,
48) -> MockPlayer:
49 """Link a protocol player of the given domain to the parent and register it."""
50 provider = MockProvider(domain, instance_id=domain, mass=mock_mass)
51 protocol_player = MockPlayer(provider, protocol_id, domain, player_type=PlayerType.PROTOCOL)
52 protocol_player._attr_available = available
53 protocol_player._attr_needs_setup = needs_setup
54 protocol_player._cache.clear()
55 parent.set_linked_output_protocols(
56 [
57 *parent.linked_output_protocols,
58 LinkedOutputProtocol(
59 output_protocol_id=protocol_id,
60 protocol_domain=domain,
61 priority=10,
62 ),
63 ]
64 )
65 registered = {protocol_player.player_id: protocol_player}
66 mock_mass.players.get_player = MagicMock(side_effect=lambda pid: registered.get(pid))
67 parent._cache.clear()
68 return protocol_player
69
70
71class TestPlaybackDomains:
72 """Tests for Player.playback_domains."""
73
74 def test_native_player_reports_own_domain(self, mock_mass: MagicMock) -> None:
75 """Test that a native player reports its own provider domain."""
76 player = _make_native_player(mock_mass)
77 assert player.playback_domains == {"sonos"}
78
79 def test_unavailable_native_player_reports_nothing(self, mock_mass: MagicMock) -> None:
80 """Test that an offline native player no longer offers its own domain."""
81 player = _make_native_player(mock_mass)
82 player._attr_available = False
83 player._cache.clear()
84 assert player.playback_domains == set()
85
86 def test_native_player_needing_setup_reports_nothing(self, mock_mass: MagicMock) -> None:
87 """Test that a player that still needs setup cannot accept a stream."""
88 player = _make_native_player(mock_mass)
89 player._attr_needs_setup = True
90 player._cache.clear()
91 assert player.playback_domains == set()
92
93 def test_linked_protocol_adds_its_domain(self, mock_mass: MagicMock) -> None:
94 """Test that an available linked protocol adds its domain."""
95 player = _make_native_player(mock_mass)
96 _link_protocol(mock_mass, player, "ap_1", "airplay", available=True)
97 assert player.playback_domains == {"sonos", "airplay"}
98
99 def test_offline_linked_protocol_is_left_out(self, mock_mass: MagicMock) -> None:
100 """Test that a linked protocol whose player went offline is not reported."""
101 player = _make_native_player(mock_mass)
102 _link_protocol(mock_mass, player, "ap_1", "airplay", available=False)
103 assert player.playback_domains == {"sonos"}
104
105 def test_linked_protocol_needing_setup_is_left_out(self, mock_mass: MagicMock) -> None:
106 """Test that an unpaired protocol receiver cannot accept a stream."""
107 player = _make_native_player(mock_mass)
108 _link_protocol(mock_mass, player, "ap_1", "airplay", available=True, needs_setup=True)
109 assert player.playback_domains == {"sonos"}
110
111 def test_unregistered_linked_protocol_is_left_out(self, mock_mass: MagicMock) -> None:
112 """Test that a linked protocol without a live player is not reported."""
113 player = _make_native_player(mock_mass)
114 _link_protocol(mock_mass, player, "ap_1", "airplay", available=True)
115 mock_mass.players.get_player = MagicMock(return_value=None)
116 player._cache.clear()
117 assert player.playback_domains == {"sonos"}
118
119 def test_wrapper_player_only_reports_linked_protocols(self, mock_mass: MagicMock) -> None:
120 """Test that a UniversalPlayer never contributes its own domain."""
121 provider = MockProvider("universal_player", instance_id="universal_player", mass=mock_mass)
122 player = MockPlayer(provider, "up_1", "Universal")
123 player._attr_supported_features = {PlayerFeature.PLAY_MEDIA}
124 player._cache.clear()
125 _link_protocol(mock_mass, player, "ap_1", "airplay", available=True)
126 assert player.playback_domains == {"airplay"}
127
128 def test_protocol_endpoint_reports_own_domain(self, mock_mass: MagicMock) -> None:
129 """Test that a player that is itself a protocol endpoint reports its domain."""
130 provider = MockProvider("airplay", instance_id="airplay", mass=mock_mass)
131 player = MockPlayer(provider, "ap_1", "AirPlay", player_type=PlayerType.PROTOCOL)
132 player._attr_supported_features = {PlayerFeature.SET_MEMBERS}
133 player._cache.clear()
134 assert player.playback_domains == {"airplay"}
135
136 player._attr_available = False
137 player._cache.clear()
138 assert player.playback_domains == set()
139
140 def test_recomputed_after_state_update(self, mock_mass: MagicMock) -> None:
141 """Test that the cached value follows the protocol player going offline."""
142 player = _make_native_player(mock_mass)
143 protocol_player = _link_protocol(mock_mass, player, "ap_1", "airplay", available=True)
144 assert player.playback_domains == {"sonos", "airplay"}
145
146 protocol_player._attr_available = False
147 player.update_state(signal_event=False)
148 assert player.playback_domains == {"sonos"}
149
150
151class TestGetLinkedProtocol:
152 """Tests for Player.get_linked_protocol."""
153
154 def test_resolves_name_and_availability_from_live_player(self, mock_mass: MagicMock) -> None:
155 """Test that the returned entry describes the protocol player as it is right now."""
156 player = _make_native_player(mock_mass)
157 _link_protocol(mock_mass, player, "ap_1", "airplay", available=True)
158
159 output_protocol = player.get_linked_protocol("ap_1")
160 assert output_protocol is not None
161 assert output_protocol.name == "Mock Airplay"
162 assert output_protocol.available is True
163 assert output_protocol.protocol_domain == "airplay"
164 assert output_protocol.priority == 10
165
166 def test_reports_offline_protocol_player(self, mock_mass: MagicMock) -> None:
167 """Test that an unchanged link stops claiming availability once its player drops."""
168 player = _make_native_player(mock_mass)
169 protocol_player = _link_protocol(mock_mass, player, "ap_1", "airplay", available=True)
170
171 protocol_player._attr_available = False
172 output_protocol = player.get_linked_protocol("ap_1")
173 assert output_protocol is not None
174 assert output_protocol.available is False
175
176 def test_unregistered_protocol_player_falls_back_to_domain(self, mock_mass: MagicMock) -> None:
177 """Test that a link without a live player reports the domain and no availability."""
178 player = _make_native_player(mock_mass)
179 _link_protocol(mock_mass, player, "ap_1", "airplay", available=True)
180 mock_mass.players.get_player = MagicMock(return_value=None)
181
182 output_protocol = player.get_linked_protocol("ap_1")
183 assert output_protocol is not None
184 assert output_protocol.name == "Airplay"
185 assert output_protocol.available is False
186
187 def test_unknown_protocol_id_returns_none(self, mock_mass: MagicMock) -> None:
188 """Test that an id that is not linked resolves to nothing."""
189 player = _make_native_player(mock_mass)
190 _link_protocol(mock_mass, player, "ap_1", "airplay", available=True)
191 assert player.get_linked_protocol("cast_1") is None
192