/
/
/
1"""Unit tests for the Plex Music Provider audiobook helpers."""
2
3from __future__ import annotations
4
5from typing import Any
6
7import pytest
8
9from music_assistant.providers.plex.helpers import (
10 PlexSectionInfo,
11 _library_tracks_progress,
12 extract_library_name,
13)
14
15
16class TestPlexSectionInfo:
17 """Tests for PlexSectionInfo dataclass."""
18
19 def test_from_dict_kwargs_reconstruction(self) -> None:
20 """PlexSectionInfo can be reconstructed from a dict via **kwargs."""
21 data: dict[str, Any] = {
22 "display_name": "My Server / Audiobooks",
23 "section_title": "Audiobooks",
24 "server_name": "My Server",
25 "section_type": "artist",
26 "is_tracking_progress": True,
27 }
28 info = PlexSectionInfo(**data)
29 assert info.display_name == "My Server / Audiobooks"
30 assert info.section_title == "Audiobooks"
31 assert info.is_tracking_progress is True
32
33
34class TestExtractLibraryName:
35 """Tests for extract_library_name() config value parser."""
36
37 @pytest.mark.parametrize(
38 ("input_value", "expected"),
39 [
40 ("My Server / Music", "Music"),
41 ("My Server / Audiobooks", "Audiobooks"),
42 (" My Server / Audiobooks ", "Audiobooks"),
43 ("Audiobooks", "Audiobooks"),
44 (" Audiobooks ", "Audiobooks"),
45 ],
46 )
47 def test_extract_library_name(self, input_value: str, expected: str) -> None:
48 """Extract library name handles both 'server / name' and plain 'name' formats."""
49 assert extract_library_name(input_value) == expected
50
51 def test_empty_string_returns_empty(self) -> None:
52 """Empty string should be returned as-is (not an error)."""
53 assert extract_library_name("") == ""
54
55
56class TestLibraryUsesTrackProgress:
57 """Tests for _library_tracks_progress() using storeTrackProgress preference."""
58
59 class FakeSetting:
60 """Minimal Setting stub for unit tests."""
61
62 def __init__(self, setting_id: str, value: bool) -> None:
63 """Initialize fake setting."""
64 self.id = setting_id
65 self.value = value
66
67 class FakeSection:
68 """Minimal LibrarySection stub for unit tests."""
69
70 def __init__(self, title: str, settings_data: list[tuple[str, bool]]) -> None:
71 """Initialize fake section with given title and settings."""
72 self.title = title
73 self._settings = [
74 TestLibraryUsesTrackProgress.FakeSetting(setting_id, value)
75 for setting_id, value in settings_data
76 ]
77
78 def settings(self) -> list[TestLibraryUsesTrackProgress.FakeSetting]:
79 """Return fake settings list."""
80 return self._settings
81
82 def test_enable_track_offsets_enabled(self) -> None:
83 """Section with enableTrackOffsets=True should be flagged as resumable."""
84 section = self.FakeSection("Audiobooks", [("enableTrackOffsets", True)])
85 assert _library_tracks_progress(section) is True
86
87 def test_enable_track_offsets_disabled(self) -> None:
88 """Section with enableTrackOffsets=False should not be flagged."""
89 section = self.FakeSection("Music", [("enableTrackOffsets", False)])
90 assert _library_tracks_progress(section) is False
91
92 def test_setting_absent(self) -> None:
93 """Section without enableTrackOffsets should not be flagged."""
94 section = self.FakeSection("Music", [("someOtherSetting", True)])
95 assert _library_tracks_progress(section) is False
96
97 def test_empty_settings(self) -> None:
98 """Section with no settings should not raise."""
99 section = self.FakeSection("Music", [])
100 assert _library_tracks_progress(section) is False
101
102 def test_settings_call_raises(self) -> None:
103 """If settings() raises, should gracefully fall back to False."""
104
105 class BrokenSection:
106 """Stub that raises on settings() call."""
107
108 title = "Broken"
109
110 def settings(self) -> list[Any]:
111 """Simulate a failing settings call."""
112 raise RuntimeError("Network error")
113
114 assert _library_tracks_progress(BrokenSection()) is False
115
116
117class TestGetSectionInfo:
118 """Tests for get_section_info audiobook-first behaviour."""
119
120 class FakePlexServer:
121 """Minimal PlexServer stub."""
122
123 def __init__(self, sections: list[Any]) -> None:
124 """Initialize fake server with given sections."""
125 self.friendlyName = "Test Server"
126 self._sections = sections
127
128 def library(self) -> TestGetSectionInfo.FakeLibrary:
129 """Return fake library."""
130 return TestGetSectionInfo.FakeLibrary(self._sections)
131
132 class FakeLibrary:
133 """Minimal Library stub."""
134
135 def __init__(self, sections: list[Any]) -> None:
136 """Initialize fake library with given sections."""
137 self._sections = sections
138
139 def sections(self) -> list[Any]:
140 """Return contained sections."""
141 return self._sections
142
143 class FakeMusicSection:
144 """Minimal MusicSection stub."""
145
146 TYPE = "artist"
147
148 def __init__(
149 self, title: str, enable_track_offsets: bool = False, section_type: str = "artist"
150 ) -> None:
151 """Initialize fake music section."""
152 self.title = title
153 self.type = section_type
154 self._enable_track_offsets = enable_track_offsets
155
156 def settings(self) -> list[TestLibraryUsesTrackProgress.FakeSetting]:
157 """Return fake settings for this section."""
158 if self._enable_track_offsets:
159 return [TestLibraryUsesTrackProgress.FakeSetting("enableTrackOffsets", True)]
160 return []
161
162 def test_all_music_sections_with_flag_are_resume_content(self) -> None:
163 """All music sections with enableTrackOffsets=True should be flagged as resumable."""
164 sections = [
165 self.FakeMusicSection("Music (No Resume)"),
166 self.FakeMusicSection("Audiobooks", enable_track_offsets=True),
167 self.FakeMusicSection("More Audios", enable_track_offsets=True),
168 ]
169
170 # Build PlexSectionInfo manually to simulate get_section_info logic
171 results: list[PlexSectionInfo] = []
172 for section in sections:
173 if section.type != self.FakeMusicSection.TYPE:
174 continue
175 results.append(
176 PlexSectionInfo(
177 display_name=f"Test Server / {section.title}",
178 section_title=section.title,
179 server_name="Test Server",
180 section_type=section.type,
181 is_tracking_progress=_library_tracks_progress(section),
182 )
183 )
184
185 assert len(results) == 3
186 assert results[0].is_tracking_progress is False
187 assert results[1].is_tracking_progress is True
188 assert results[2].is_tracking_progress is True
189
190
191class TestGetSectionInfoLegacyFallback:
192 """Tests ensuring library-type filtering is still respected."""
193
194 class FakeMovieSection:
195 """Minimal non-music section stub."""
196
197 TYPE = "movie"
198
199 def __init__(self, title: str) -> None:
200 """Initialize fake movie section."""
201 self.title = title
202 self.type = "movie"
203
204 def settings(self) -> list[Any]:
205 """Return empty settings."""
206 return []
207
208 def test_non_music_sections_ignored(self) -> None:
209 """Non-music sections should be skipped entirely."""
210 movie_section = self.FakeMovieSection("Movies")
211 assert movie_section.type != "artist"
212 # Simulating the loop filter
213 results: list[bool] = []
214 if movie_section.type == "artist":
215 results.append(True)
216 assert len(results) == 0
217