/
/
/
1"""Tests for ``provider.config.build_config_entries``."""
2
3from __future__ import annotations
4
5from typing import TYPE_CHECKING
6
7from music_assistant.providers.fastmcp_server.config import build_config_entries
8from music_assistant.providers.fastmcp_server.constants import (
9 CONF_CONFIG_READ,
10 CONF_CONFIG_WRITE_CORE,
11 CONF_CONFIG_WRITE_PLAYER,
12 CONF_CONFIG_WRITE_PROVIDER,
13 CONF_CONFIG_WRITE_SECRET,
14 CONF_DEBUG_EVENT_BUFFER_CAPACITY,
15 CONF_DEBUG_EVENTS,
16 CONF_DEBUG_INSPECT,
17 CONF_DEBUG_LOGS,
18 CONF_DEBUG_PROVIDERS,
19 CONF_DEBUG_RELOAD,
20 CONF_DELETE_LIBRARY,
21 CONF_QUERY_LIBRARY,
22 CONF_REQUIRE_AUTH,
23 DEFAULT_MOUNT_PATH,
24 PERMISSION_KEYS,
25 RESOURCE_KEYS,
26)
27
28if TYPE_CHECKING:
29 from unittest.mock import MagicMock
30
31
32def test_total_entry_count(mock_mass: MagicMock) -> None:
33 """41 entries: 1 info label + 1 connect-wizard action + 9 server + 16 perms + 3 resources + 6 debug + 5 config."""
34 entries = build_config_entries(mock_mass, DEFAULT_MOUNT_PATH)
35 assert len(entries) == 1 + 1 + 9 + 16 + 3 + 6 + 5
36
37
38def test_all_permission_keys_present(mock_mass: MagicMock) -> None:
39 """Every permission key from PERMISSION_KEYS has a matching ConfigEntry."""
40 entries = build_config_entries(mock_mass, DEFAULT_MOUNT_PATH)
41 keys = {e.key for e in entries}
42 assert PERMISSION_KEYS.issubset(keys)
43 assert RESOURCE_KEYS.issubset(keys)
44 assert CONF_REQUIRE_AUTH in keys
45
46
47def test_delete_keys_default_false(mock_mass: MagicMock) -> None:
48 """All delete-family permissions default to False (least-privilege)."""
49 entries = {e.key: e for e in build_config_entries(mock_mass, DEFAULT_MOUNT_PATH)}
50 mutation_prefixes = ("delete_", "control_", "edit_")
51 for key in PERMISSION_KEYS:
52 if key.startswith(mutation_prefixes):
53 assert entries[key].default_value is False, f"{key} should default False"
54
55
56def test_query_keys_default_true(mock_mass: MagicMock) -> None:
57 """All query-family permissions default to True."""
58 entries = {e.key: e for e in build_config_entries(mock_mass, DEFAULT_MOUNT_PATH)}
59 assert entries[CONF_QUERY_LIBRARY].default_value is True
60
61
62def test_categories_match_pr2889_ux(mock_mass: MagicMock) -> None:
63 """Categories mirror upstream PR #2889 grouping for familiarity at review time."""
64 entries = build_config_entries(mock_mass, DEFAULT_MOUNT_PATH)
65 categories = {getattr(e, "category", None) for e in entries if getattr(e, "category", None)}
66 # ``Generic`` comes from the Connect Wizard ACTION entry, which mirrors the
67 # Spotify provider's ``CONF_ACTION_AUTH`` button (no explicit category).
68 assert categories == {
69 "server",
70 "query_permissions",
71 "control_permissions",
72 "edit_permissions",
73 "delete_permissions",
74 "mcp_resources",
75 "debug",
76 "mcp_config",
77 "generic",
78 }
79
80
81def test_info_label_includes_base_url(mock_mass: MagicMock) -> None:
82 """The info label embeds MA's base_url so users see where to point clients."""
83 entries = build_config_entries(mock_mass, DEFAULT_MOUNT_PATH)
84 info = entries[0]
85 assert mock_mass.webserver.base_url in str(info.label)
86 assert "/mcp/v1" in str(info.label)
87
88
89def test_info_label_normalises_mount_path_without_leading_slash(
90 mock_mass: MagicMock,
91) -> None:
92 """
93 A user-typed ``mcp/v1`` (no leading slash) must still render a valid URL.
94
95 Regression for upstream PR #3858 Copilot comment: the runtime normalises
96 the mount path, but the info label did not â so the displayed endpoint
97 glued the host to the path with no separator (``â¦:8095mcp/v1``).
98 """
99 entries = build_config_entries(mock_mass, "mcp/v1")
100 label = str(entries[0].label)
101 base_url = mock_mass.webserver.base_url
102 assert f"{base_url}/mcp/v1" in label
103 assert f"{base_url}mcp" not in label
104
105
106def test_delete_library_default(mock_mass: MagicMock) -> None:
107 """Specifically: delete_library defaults False (a hard-to-undo permission)."""
108 entries = {e.key: e for e in build_config_entries(mock_mass, DEFAULT_MOUNT_PATH)}
109 assert entries[CONF_DELETE_LIBRARY].default_value is False
110
111
112def test_debug_entries_present_with_off_defaults(mock_mass: MagicMock) -> None:
113 """Debug ConfigEntries are present and default to off (least privilege)."""
114 entries = {e.key: e for e in build_config_entries(mock_mass, DEFAULT_MOUNT_PATH)}
115 for key in (
116 CONF_DEBUG_INSPECT,
117 CONF_DEBUG_LOGS,
118 CONF_DEBUG_EVENTS,
119 CONF_DEBUG_PROVIDERS,
120 CONF_DEBUG_RELOAD,
121 ):
122 assert key in entries, f"missing {key}"
123 assert entries[key].default_value is False, f"{key} must be off by default"
124 assert entries[key].category == "debug"
125
126 cap = entries[CONF_DEBUG_EVENT_BUFFER_CAPACITY]
127 assert cap.default_value == 500
128 assert cap.range == (50, 5000)
129
130
131def test_config_entries_present_with_off_defaults(mock_mass: MagicMock) -> None:
132 """Config ConfigEntries are present and default to off (least privilege)."""
133 entries = {e.key: e for e in build_config_entries(mock_mass, DEFAULT_MOUNT_PATH)}
134 for key in (
135 CONF_CONFIG_READ,
136 CONF_CONFIG_WRITE_PROVIDER,
137 CONF_CONFIG_WRITE_CORE,
138 CONF_CONFIG_WRITE_PLAYER,
139 CONF_CONFIG_WRITE_SECRET,
140 ):
141 assert key in entries, f"missing {key}"
142 assert entries[key].default_value is False, f"{key} must be off by default"
143 assert entries[key].category == "mcp_config"
144