/
/
/
1"""Tests for the `reachable_via` provider-mapping filter on library_items()."""
2
3from __future__ import annotations
4
5from typing import TYPE_CHECKING
6from unittest.mock import Mock, patch
7
8from music_assistant_models.media_items import Artist, ProviderMapping, Track, UniqueList
9
10from music_assistant.mass import MusicAssistant
11
12if TYPE_CHECKING:
13 import pytest
14
15
16def _mapping(
17 provider_instance: str,
18 provider_domain: str,
19 item_id: str,
20 *,
21 in_library: bool = True,
22 available: bool = True,
23) -> ProviderMapping:
24 return ProviderMapping(
25 item_id=item_id,
26 provider_domain=provider_domain,
27 provider_instance=provider_instance,
28 in_library=in_library,
29 available=available,
30 )
31
32
33async def _add_track(mass: MusicAssistant, name: str, mappings: set[ProviderMapping]) -> Track:
34 """Add a library track (with a dummy library artist) with the given provider mappings."""
35 artist = Artist(
36 item_id="0",
37 provider="library",
38 name=f"{name} Artist",
39 provider_mappings={_mapping("local_1", "local", f"{name}-artist")},
40 )
41 db_artist = await mass.music.artists.add_item_to_library(artist)
42 track = Track(
43 item_id="0",
44 provider="library",
45 name=name,
46 provider_mappings=mappings,
47 artists=UniqueList([db_artist]),
48 disc_number=1,
49 track_number=1,
50 )
51 return await mass.music.tracks.add_item_to_library(track)
52
53
54async def test_reachable_via_includes_item_not_in_library_on_requested_provider(
55 mass: MusicAssistant, monkeypatch: pytest.MonkeyPatch
56) -> None:
57 """
58 A library item playable via Spotify (but not favorited there) must be included.
59
60 The item is a library item because of its Local mapping (in_library=True); its
61 Spotify mapping is not in Spotify's own library (in_library=False). Reachability
62 must not require the *matched* mapping to be in-library, unlike the existing
63 `provider` filter.
64 """
65 monkeypatch.setattr(
66 mass.music, "get_active_provider_instances", lambda: ["local_1", "spotify_1"]
67 )
68 await _add_track(
69 mass,
70 "Track A",
71 {
72 _mapping("local_1", "local", "track-a-local"),
73 _mapping("spotify_1", "spotify", "track-a-spotify", in_library=False),
74 },
75 )
76
77 result = await mass.music.tracks.library_items(reachable_via=["spotify_1"])
78
79 assert {t.name for t in result} == {"Track A"}
80
81
82async def test_reachable_via_excludes_item_reachable_only_via_unloaded_provider(
83 mass: MusicAssistant, monkeypatch: pytest.MonkeyPatch
84) -> None:
85 """A stale mapping to a provider that is no longer loaded cannot satisfy the filter."""
86 monkeypatch.setattr(mass.music, "get_active_provider_instances", lambda: ["local_1"])
87 await _add_track(
88 mass,
89 "Track B",
90 {
91 _mapping("local_1", "local", "track-b-local"),
92 _mapping("spotify_1", "spotify", "track-b-spotify", in_library=False),
93 },
94 )
95
96 result = await mass.music.tracks.library_items(reachable_via=["spotify_1"])
97
98 assert result == []
99
100
101async def test_reachable_via_excludes_item_with_unavailable_mapping(
102 mass: MusicAssistant, monkeypatch: pytest.MonkeyPatch
103) -> None:
104 """A mapping that is no longer available on the requested provider cannot match."""
105 monkeypatch.setattr(
106 mass.music, "get_active_provider_instances", lambda: ["local_1", "spotify_1"]
107 )
108 await _add_track(
109 mass,
110 "Track H",
111 {
112 _mapping("local_1", "local", "track-h-local"),
113 _mapping("spotify_1", "spotify", "track-h-spotify", available=False),
114 },
115 )
116
117 result = await mass.music.tracks.library_items(reachable_via=["spotify_1"])
118
119 assert result == []
120
121
122async def test_reachable_via_includes_item_despite_restricted_user_provider_filter(
123 mass: MusicAssistant, monkeypatch: pytest.MonkeyPatch
124) -> None:
125 """
126 A restricted user can still reach an item through an allowed provider.
127
128 The item's only in-library mapping is on Local, which isn't one of the user's
129 allowed providers; its Spotify mapping is available but not itself favorited on
130 Spotify. Since the user is allowed to use Spotify and the item is reachable
131 through it, the implicit user-provider-filter injection (which would otherwise
132 also require the in-library mapping itself to be on an allowed provider) must
133 not additionally exclude it.
134 """
135 monkeypatch.setattr(
136 mass.music, "get_active_provider_instances", lambda: ["local_1", "spotify_1"]
137 )
138 await _add_track(
139 mass,
140 "Track I",
141 {
142 _mapping("local_1", "local", "track-i-local"),
143 _mapping("spotify_1", "spotify", "track-i-spotify", in_library=False),
144 },
145 )
146
147 with patch(
148 "music_assistant.controllers.music.media.base.get_current_user",
149 return_value=Mock(provider_filter=["spotify_1"]),
150 ):
151 result = await mass.music.tracks.library_items(reachable_via=["spotify_1"])
152
153 assert {t.name for t in result} == {"Track I"}
154
155
156async def test_reachable_via_excludes_item_for_provider_outside_user_access(
157 mass: MusicAssistant, monkeypatch: pytest.MonkeyPatch
158) -> None:
159 """
160 A provider outside the current user's access (per get_active_provider_instances) is ignored.
161
162 `get_active_provider_instances` already applies the current user's provider_filter, so a
163 requested instance that it omits (whether unloaded or simply not allowed for this
164 user) must not make the item reachable, even though a matching mapping exists.
165 """
166 monkeypatch.setattr(mass.music, "get_active_provider_instances", lambda: ["local_1"])
167 await _add_track(
168 mass,
169 "Track C",
170 {
171 _mapping("local_1", "local", "track-c-local"),
172 _mapping("spotify_1", "spotify", "track-c-spotify"),
173 },
174 )
175
176 result = await mass.music.tracks.library_items(reachable_via=["local_1", "spotify_1"])
177
178 assert {t.name for t in result} == {"Track C"}
179 result = await mass.music.tracks.library_items(reachable_via=["spotify_1"])
180 assert result == []
181
182
183async def test_reachable_via_none_applies_no_filter(
184 mass: MusicAssistant, monkeypatch: pytest.MonkeyPatch
185) -> None:
186 """Omitting the filter (None) keeps existing unfiltered behavior."""
187 monkeypatch.setattr(mass.music, "get_active_provider_instances", list)
188 await _add_track(mass, "Track D", {_mapping("local_1", "local", "track-d-local")})
189
190 result = await mass.music.tracks.library_items(reachable_via=None)
191
192 assert {t.name for t in result} == {"Track D"}
193
194
195async def test_reachable_via_explicit_empty_list_returns_no_items(
196 mass: MusicAssistant, monkeypatch: pytest.MonkeyPatch
197) -> None:
198 """An explicit empty list returns no items, distinct from omitting the filter."""
199 monkeypatch.setattr(mass.music, "get_active_provider_instances", lambda: ["local_1"])
200 await _add_track(mass, "Track E", {_mapping("local_1", "local", "track-e-local")})
201
202 result = await mass.music.tracks.library_items(reachable_via=[])
203
204 assert result == []
205
206
207async def test_reachable_via_applies_to_random_order_by_subquery(
208 mass: MusicAssistant, monkeypatch: pytest.MonkeyPatch
209) -> None:
210 """The reachability filter is also applied when using the fast random subquery path."""
211 monkeypatch.setattr(
212 mass.music, "get_active_provider_instances", lambda: ["local_1", "spotify_1"]
213 )
214 await _add_track(
215 mass,
216 "Track F",
217 {
218 _mapping("local_1", "local", "track-f-local"),
219 _mapping("spotify_1", "spotify", "track-f-spotify", in_library=False),
220 },
221 )
222 await _add_track(mass, "Track G", {_mapping("local_1", "local", "track-g-local")})
223
224 result = await mass.music.tracks.library_items(
225 order_by="random_play_count", reachable_via=["spotify_1"]
226 )
227
228 assert {t.name for t in result} == {"Track F"}
229