/
/
/
1"""Tests for the Plex setup-flow library selection (claim exclusion + smart defaults)."""
2
3from __future__ import annotations
4
5from typing import Any
6from unittest.mock import MagicMock
7
8from music_assistant.providers.plex.constants import CONF_LIBRARY_ID
9from music_assistant.providers.plex.helpers import (
10 CONF_LIBRARY_TYPE,
11 LIBRARY_TYPE_AUDIOBOOKS,
12 LIBRARY_TYPE_MUSIC,
13 LIBRARY_TYPE_PODCASTS,
14 PlexSectionInfo,
15)
16from music_assistant.providers.plex.setup_flow import (
17 _claimed_libraries,
18 _default_library_selection,
19)
20
21
22def _section(name: str, tracking: bool) -> PlexSectionInfo:
23 """Build a PlexSectionInfo for the given display name and tracking flag."""
24 return PlexSectionInfo(
25 display_name=name,
26 section_title=name.rsplit(" / ", maxsplit=1)[-1],
27 server_name="Server",
28 section_type="artist",
29 is_tracking_progress=tracking,
30 )
31
32
33def _make_mock_mass(providers_config: dict[str, Any] | None = None) -> MagicMock:
34 """Build a MusicAssistant stub whose config.get('providers') returns the raw config."""
35 mass = MagicMock()
36 mass.config.get = MagicMock(return_value=providers_config or {})
37 return mass
38
39
40def test_claimed_libraries_reads_raw_config_without_recursion() -> None:
41 """
42 Claimed libraries are read from the raw stored config, not via get_provider_configs.
43
44 Regression: using get_provider_configs(include_values=True) recursed because each
45 call triggered get_config_entries for every other plex instance.
46 """
47 raw_config = {
48 "plex-1": {
49 "domain": "plex",
50 "values": {
51 CONF_LIBRARY_ID: "Server / Audiobooks",
52 CONF_LIBRARY_TYPE: LIBRARY_TYPE_AUDIOBOOKS,
53 },
54 },
55 "plex-2": {
56 "domain": "plex",
57 "setup_data": {
58 CONF_LIBRARY_ID: "Server / Music",
59 CONF_LIBRARY_TYPE: LIBRARY_TYPE_MUSIC,
60 },
61 },
62 "other": {"domain": "spotify", "values": {CONF_LIBRARY_ID: "ignored"}},
63 }
64 mass = _make_mock_mass(raw_config)
65
66 used, has_audiobook_provider = _claimed_libraries(mass, instance_id=None)
67
68 assert used == {"Server / Audiobooks", "Server / Music"}
69 assert has_audiobook_provider is True
70 # the raw provider config is read directly; the recursive helper is never used
71 mass.config.get.assert_called_once_with("providers", {})
72 mass.config.get_provider_configs.assert_not_called()
73
74
75def test_default_selection_prefers_unclaimed_non_tracking_library() -> None:
76 """The default library is the first non-tracking (music) library, A-Z."""
77 sections = [
78 _section("Server / Audiobooks", tracking=True),
79 _section("Server / Music", tracking=False),
80 _section("Server / Podcasts", tracking=True),
81 ]
82 library, library_type = _default_library_selection(
83 sections,
84 used_libraries=set(),
85 has_audiobook_provider=False,
86 prefilled_library=None,
87 )
88 # the non-tracking (music) library sorts ahead of the tracking ones
89 assert library == "Server / Music"
90 assert library_type == LIBRARY_TYPE_MUSIC
91
92
93def test_default_selection_skips_claimed_libraries() -> None:
94 """Claimed libraries are excluded, so the default falls to the remaining one."""
95 sections = [
96 _section("Server / Music", tracking=False),
97 _section("Server / Podcasts", tracking=True),
98 ]
99 # Music is claimed and an audiobook provider already exists
100 library, library_type = _default_library_selection(
101 sections,
102 used_libraries={"Server / Music"},
103 has_audiobook_provider=True,
104 prefilled_library=None,
105 )
106 # only Podcasts is unclaimed; it is a tracking library and an audiobook provider
107 # already exists, so the type resolves to Podcasts
108 assert library == "Server / Podcasts"
109 assert library_type == LIBRARY_TYPE_PODCASTS
110
111
112def test_default_selection_tracking_library_without_audiobook_provider() -> None:
113 """A tracking default library maps to the Audiobooks type when none exists yet."""
114 sections = [_section("Server / Audiobooks", tracking=True)]
115 library, library_type = _default_library_selection(
116 sections,
117 used_libraries=set(),
118 has_audiobook_provider=False,
119 prefilled_library=None,
120 )
121 assert library == "Server / Audiobooks"
122 assert library_type == LIBRARY_TYPE_AUDIOBOOKS
123
124
125def test_default_selection_preserves_prefilled_choice() -> None:
126 """A previously selected library (reconfigure) is kept as the default."""
127 sections = [
128 _section("Server / Music", tracking=False),
129 _section("Server / Audiobooks", tracking=True),
130 ]
131 library, library_type = _default_library_selection(
132 sections,
133 used_libraries=set(),
134 has_audiobook_provider=False,
135 prefilled_library="Server / Music",
136 )
137 assert library == "Server / Music"
138 assert library_type == LIBRARY_TYPE_MUSIC
139