/
/
/
1"""
2Regression tests for removing a player config with linked protocol players.
3
4Reproduces the case where a player is first disabled and then removed: disabling
5cascades to the linked protocol players, so none of them is registered anymore when
6the removal comes in. The leftover (disabled) protocol config is not shown anywhere
7and keeps the device from ever registering again.
8"""
9
10from unittest.mock import MagicMock
11
12from music_assistant.constants import CONF_PLAYER_DSP, CONF_PLAYERS, CONF_PROTOCOL_PARENT_ID
13from music_assistant.mass import MusicAssistant
14
15PARENT_ID = "up_esp32"
16PROTOCOL_ID = "spb_esp32"
17
18
19def _store_configs(mass: MusicAssistant, enabled: bool) -> None:
20 """Store a universal player config with a single linked protocol player."""
21 mass.config.set(
22 f"{CONF_PLAYERS}/{PARENT_ID}",
23 {
24 "player_id": PARENT_ID,
25 "provider": "universal_player",
26 "player_type": "player",
27 "enabled": enabled,
28 "values": {"linked_protocol_ids": [PROTOCOL_ID]},
29 },
30 )
31 mass.config.set(
32 f"{CONF_PLAYERS}/{PROTOCOL_ID}",
33 {
34 "player_id": PROTOCOL_ID,
35 "provider": "sendspin",
36 "player_type": "protocol",
37 "enabled": enabled,
38 "values": {CONF_PROTOCOL_PARENT_ID: PARENT_ID},
39 },
40 )
41 mass.config.set(f"{CONF_PLAYER_DSP}/{PROTOCOL_ID}", {"enabled": True})
42
43
44async def test_remove_wipes_unregistered_protocol_configs(mass: MusicAssistant) -> None:
45 """Removing a disabled player also wipes the config of its linked protocol player."""
46 _store_configs(mass, enabled=False)
47
48 await mass.config.remove_player_config(PARENT_ID)
49
50 assert mass.config.get(f"{CONF_PLAYERS}/{PARENT_ID}") is None
51 assert mass.config.get(f"{CONF_PLAYERS}/{PROTOCOL_ID}") is None
52 assert mass.config.get(f"{CONF_PLAYER_DSP}/{PROTOCOL_ID}") is None
53
54
55async def test_remove_wipes_protocol_configs_with_a_half_broken_link(
56 mass: MusicAssistant,
57) -> None:
58 """A protocol player is wiped by its own parent reference, not by the parent's list."""
59 _store_configs(mass, enabled=False)
60 mass.config.set(f"{CONF_PLAYERS}/{PARENT_ID}/values/linked_protocol_ids", [])
61
62 await mass.config.remove_player_config(PARENT_ID)
63
64 assert mass.config.get(f"{CONF_PLAYERS}/{PROTOCOL_ID}") is None
65
66
67async def test_remove_keeps_reparented_protocol_configs(mass: MusicAssistant) -> None:
68 """A protocol player that already moved to another parent keeps its config."""
69 _store_configs(mass, enabled=True)
70 mass.config.set(f"{CONF_PLAYERS}/{PROTOCOL_ID}/values/{CONF_PROTOCOL_PARENT_ID}", "cast_1")
71
72 mass.players.delete_player_config(PARENT_ID)
73
74 assert mass.config.get(f"{CONF_PLAYERS}/{PARENT_ID}") is None
75 assert mass.config.get(f"{CONF_PLAYERS}/{PROTOCOL_ID}") is not None
76
77
78async def test_remove_keeps_registered_protocol_configs(mass: MusicAssistant) -> None:
79 """A protocol player that is still registered keeps its config to be re-parented."""
80 _store_configs(mass, enabled=True)
81 mass.players._players[PROTOCOL_ID] = MagicMock()
82
83 mass.players.delete_player_config(PARENT_ID)
84
85 assert mass.config.get(f"{CONF_PLAYERS}/{PARENT_ID}") is None
86 assert mass.config.get(f"{CONF_PLAYERS}/{PROTOCOL_ID}") is not None
87 assert mass.config.get(f"{CONF_PLAYER_DSP}/{PROTOCOL_ID}") is not None
88