/
/
/
1"""
2Tests for the ``None``-return contract of ``ConfigEntryType.ACTION`` handlers.
3
4An action handler returning ``None`` means the one-shot side effect ran with nothing
5to re-render: ``config/*/invoke_action`` must surface that as an empty list, distinct
6from the default-wrapped entries a non-empty result gets, so the frontend can tell
7"ran fine, nothing changed" apart from "here is the refreshed form".
8"""
9
10from __future__ import annotations
11
12from unittest.mock import AsyncMock, MagicMock, patch
13
14from music_assistant_models.config_entries import ConfigEntry
15from music_assistant_models.enums import ConfigEntryType
16
17from music_assistant.constants import CONF_PROTOCOL_KEY_SPLITTER
18
19from .helpers import build_bare_config_controller
20
21
22async def test_provider_action_returning_none_yields_empty_list() -> None:
23 """A provider action returning None surfaces as [], not the default-wrapped entries."""
24 mock_mass = MagicMock()
25 provider = MagicMock()
26 provider.domain = "some_provider"
27 provider.handle_config_action = AsyncMock(return_value=None)
28 mock_mass.get_provider.return_value = provider
29 controller = build_bare_config_controller(mock_mass)
30
31 result = await controller.invoke_provider_config_action("some_instance", "some_action")
32
33 assert result == []
34
35
36async def test_provider_action_returning_entries_still_wraps_them() -> None:
37 """A provider action that still returns entries keeps the default-wrapped rendering."""
38 mock_mass = MagicMock()
39 provider = MagicMock()
40 provider.domain = "some_provider"
41 own_entry = ConfigEntry(key="foo", type=ConfigEntryType.LABEL)
42 provider.handle_config_action = AsyncMock(return_value=(own_entry,))
43 mock_mass.get_provider.return_value = provider
44 controller = build_bare_config_controller(mock_mass)
45
46 result = await controller.invoke_provider_config_action("some_instance", "some_action")
47
48 assert isinstance(result, list)
49 assert any(entry.key == "foo" for entry in result)
50 # the server default entries are still appended around the provider's own entry
51 assert len(result) > 1
52
53
54async def test_core_action_returning_none_yields_empty_list() -> None:
55 """A core-module action returning None surfaces as [], not the default-wrapped entries."""
56 mock_mass = MagicMock()
57 module = MagicMock()
58 module.handle_config_action = AsyncMock(return_value=None)
59 mock_mass.some_module = module
60 controller = build_bare_config_controller(mock_mass)
61
62 result = await controller.invoke_core_config_action("some_module", "some_action")
63
64 assert result == []
65
66
67async def test_core_action_returning_entries_still_wraps_them() -> None:
68 """A core-module action that still returns entries keeps the default-wrapped rendering."""
69 mock_mass = MagicMock()
70 module = MagicMock()
71 own_entry = ConfigEntry(key="foo", type=ConfigEntryType.LABEL)
72 module.handle_config_action = AsyncMock(return_value=(own_entry,))
73 mock_mass.some_module = module
74 controller = build_bare_config_controller(mock_mass)
75
76 result = await controller.invoke_core_config_action("some_module", "some_action")
77
78 assert isinstance(result, list)
79 assert any(entry.key == "foo" for entry in result)
80 assert len(result) > 1
81
82
83async def test_player_action_returning_none_yields_empty_list_without_rerender() -> None:
84 """A player action returning None surfaces as [] and skips the full-entries re-render."""
85 mock_mass = MagicMock()
86 player = MagicMock()
87 player.handle_config_action = AsyncMock(return_value=None)
88 mock_mass.players.get_player.return_value = player
89 controller = build_bare_config_controller(mock_mass)
90
91 with patch.object(controller, "get_player_config_entries", AsyncMock()) as mock_rerender:
92 result = await controller.invoke_player_config_action("player_1", "some_action")
93
94 assert result == []
95 mock_rerender.assert_not_awaited()
96
97
98async def test_player_action_returning_entries_still_rerenders() -> None:
99 """A player action that still returns entries keeps the existing full re-render."""
100 mock_mass = MagicMock()
101 player = MagicMock()
102 player.handle_config_action = AsyncMock(
103 return_value=[ConfigEntry(key="foo", type=ConfigEntryType.LABEL)]
104 )
105 mock_mass.players.get_player.return_value = player
106 controller = build_bare_config_controller(mock_mass)
107 rerendered = [ConfigEntry(key="bar", type=ConfigEntryType.LABEL)]
108
109 with patch.object(
110 controller, "get_player_config_entries", AsyncMock(return_value=rerendered)
111 ) as mock_rerender:
112 result = await controller.invoke_player_config_action("player_1", "some_action")
113
114 assert result == rerendered
115 mock_rerender.assert_awaited_once_with("player_1")
116
117
118async def test_protocol_action_returning_none_yields_empty_list_without_rerender() -> None:
119 """A protocol-prefixed action returning None surfaces as [] without re-rendering the parent."""
120 mock_mass = MagicMock()
121 parent = MagicMock()
122 parent.handle_config_action = AsyncMock()
123 protocol_player = MagicMock()
124 protocol_player.handle_config_action = AsyncMock(return_value=None)
125 mock_mass.players.get_player.side_effect = lambda player_id, *_args: {
126 "player_1": parent,
127 "protocol_1": protocol_player,
128 }[player_id]
129 controller = build_bare_config_controller(mock_mass)
130 action = f"protocol_1{CONF_PROTOCOL_KEY_SPLITTER}some_action"
131
132 with patch.object(controller, "get_player_config_entries", AsyncMock()) as mock_rerender:
133 result = await controller.invoke_player_config_action("player_1", action)
134
135 assert result == []
136 protocol_player.handle_config_action.assert_awaited_once_with("some_action")
137 parent.handle_config_action.assert_not_awaited()
138 mock_rerender.assert_not_awaited()
139
140
141async def test_protocol_action_returning_entries_rerenders_the_parent() -> None:
142 """A protocol-prefixed action returning entries re-renders the parent, not the target."""
143 mock_mass = MagicMock()
144 parent = MagicMock()
145 parent.handle_config_action = AsyncMock()
146 protocol_player = MagicMock()
147 protocol_player.handle_config_action = AsyncMock(
148 return_value=[ConfigEntry(key="foo", type=ConfigEntryType.LABEL)]
149 )
150 mock_mass.players.get_player.side_effect = lambda player_id, *_args: {
151 "player_1": parent,
152 "protocol_1": protocol_player,
153 }[player_id]
154 controller = build_bare_config_controller(mock_mass)
155 rerendered = [ConfigEntry(key="bar", type=ConfigEntryType.LABEL)]
156 action = f"protocol_1{CONF_PROTOCOL_KEY_SPLITTER}some_action"
157
158 with patch.object(
159 controller, "get_player_config_entries", AsyncMock(return_value=rerendered)
160 ) as mock_rerender:
161 result = await controller.invoke_player_config_action("player_1", action)
162
163 assert result == rerendered
164 mock_rerender.assert_awaited_once_with("player_1")
165