/
/
/
1"""Test Tidal Library Manager."""
2
3from collections.abc import AsyncGenerator
4from typing import Any
5from unittest.mock import AsyncMock, MagicMock, Mock, patch
6
7import pytest
8from music_assistant_models.enums import MediaType
9from music_assistant_models.media_items import ItemMapping
10
11from music_assistant.providers.tidal.library import TidalLibraryManager
12
13
14@pytest.fixture
15def provider_mock() -> Mock:
16 """Return a mock provider."""
17 provider = Mock()
18 provider.domain = "tidal"
19 provider.instance_id = "tidal_instance"
20 provider.auth.user_id = "12345"
21 provider.api = AsyncMock()
22 provider.api.get_data.return_value = {"items": []}
23 provider.api.paginate = MagicMock()
24
25 # Configure async iterator for paginate
26 async def async_iter(*_args: Any, **_kwargs: Any) -> AsyncGenerator[Any, None]:
27 for item in provider.api.paginate.return_value:
28 yield item
29
30 provider.api.paginate.side_effect = async_iter
31 provider.api.paginate.return_value = []
32
33 provider.logger = Mock()
34
35 def get_item_mapping(media_type: MediaType, key: str, name: str) -> ItemMapping:
36 return ItemMapping(
37 media_type=media_type,
38 item_id=key,
39 provider=provider.instance_id,
40 name=name,
41 )
42
43 provider.get_item_mapping.side_effect = get_item_mapping
44
45 return provider
46
47
48@pytest.fixture
49def library_manager(provider_mock: Mock) -> TidalLibraryManager:
50 """Return a TidalLibraryManager instance."""
51 return TidalLibraryManager(provider_mock)
52
53
54@patch("music_assistant.providers.tidal.library.parse_artist")
55async def test_get_artists(
56 mock_parse_artist: Mock, library_manager: TidalLibraryManager, provider_mock: Mock
57) -> None:
58 """Test get_artists."""
59 provider_mock.api.paginate.return_value = [
60 {"created": "2024-01-01T00:00:00", "item": {"id": 1, "name": "Test Artist"}}
61 ]
62 mock_parse_artist.return_value = Mock(item_id="1")
63
64 artists = [a async for a in library_manager.get_artists()]
65
66 assert len(artists) == 1
67 assert artists[0].item_id == "1"
68 provider_mock.api.paginate.assert_called_with(
69 "users/12345/favorites/artists",
70 )
71 mock_parse_artist.assert_called_once()
72
73
74@patch("music_assistant.providers.tidal.library.parse_album")
75async def test_get_albums(
76 mock_parse_album: Mock, library_manager: TidalLibraryManager, provider_mock: Mock
77) -> None:
78 """Test get_albums."""
79 provider_mock.api.paginate.return_value = [
80 {"created": "2024-01-01T00:00:00", "item": {"id": 1, "title": "Test Album"}}
81 ]
82 mock_parse_album.return_value = Mock(item_id="1")
83
84 albums = [a async for a in library_manager.get_albums()]
85
86 assert len(albums) == 1
87 assert albums[0].item_id == "1"
88 provider_mock.api.paginate.assert_called_with(
89 "users/12345/favorites/albums",
90 )
91 mock_parse_album.assert_called_once()
92
93
94@patch("music_assistant.providers.tidal.library.parse_track")
95async def test_get_tracks(
96 mock_parse_track: Mock, library_manager: TidalLibraryManager, provider_mock: Mock
97) -> None:
98 """Test get_tracks."""
99 provider_mock.api.paginate.return_value = [
100 {"created": "2024-01-01T00:00:00", "item": {"id": 1, "title": "Test Track"}}
101 ]
102 mock_parse_track.return_value = Mock(item_id="1")
103
104 tracks = [t async for t in library_manager.get_tracks()]
105
106 assert len(tracks) == 1
107 assert tracks[0].item_id == "1"
108 provider_mock.api.paginate.assert_called_with(
109 "users/12345/favorites/tracks",
110 )
111 mock_parse_track.assert_called_once()
112
113
114@patch("music_assistant.providers.tidal.library.parse_playlist")
115async def test_get_playlists(
116 mock_parse_playlist: Mock, library_manager: TidalLibraryManager, provider_mock: Mock
117) -> None:
118 """Test get_playlists."""
119 # Mock mixes response
120 mixes_response = [{"id": "mix_1", "title": "Mix 1"}]
121 # Mock playlists response
122 playlists_response = [
123 {
124 "created": "2024-01-01T00:00:00",
125 "playlist": {"uuid": "pl_1", "title": "Playlist 1"},
126 }
127 ]
128
129 # Configure paginate side effect
130 async def paginate_side_effect(
131 endpoint: str, **_kwargs: Any
132 ) -> AsyncGenerator[dict[str, Any], None]:
133 if "mixes" in endpoint:
134 for item in mixes_response:
135 yield item
136 else:
137 # The ignore[assignment] is needed because of the different return types
138 for item in playlists_response: # type: ignore[assignment]
139 yield item
140
141 provider_mock.api.paginate.side_effect = paginate_side_effect
142
143 # Setup mock return values
144 mock_parse_playlist.side_effect = [
145 Mock(item_id="mix_1"),
146 Mock(item_id="pl_1"),
147 ]
148
149 playlists = [p async for p in library_manager.get_playlists()]
150
151 assert len(playlists) == 2
152 assert playlists[0].item_id == "mix_1"
153 assert playlists[1].item_id == "pl_1"
154 assert mock_parse_playlist.call_count == 2
155
156
157async def test_add_item_artist(library_manager: TidalLibraryManager, provider_mock: Mock) -> None:
158 """Test add_item for artist."""
159 item = Mock(item_id="123", media_type=MediaType.ARTIST)
160 await library_manager.add_item(item)
161
162 provider_mock.api.post.assert_called_with(
163 "users/12345/favorites/artists",
164 data={"artistId": "123"},
165 as_form=True,
166 )
167
168
169async def test_add_item_album(library_manager: TidalLibraryManager, provider_mock: Mock) -> None:
170 """Test add_item for album."""
171 item = Mock(item_id="123", media_type=MediaType.ALBUM)
172 await library_manager.add_item(item)
173
174 provider_mock.api.post.assert_called_with(
175 "users/12345/favorites/albums",
176 data={"albumId": "123"},
177 as_form=True,
178 )
179
180
181async def test_add_item_track(library_manager: TidalLibraryManager, provider_mock: Mock) -> None:
182 """Test add_item for track."""
183 item = Mock(item_id="123", media_type=MediaType.TRACK)
184 await library_manager.add_item(item)
185
186 provider_mock.api.post.assert_called_with(
187 "users/12345/favorites/tracks",
188 data={"trackId": "123"},
189 as_form=True,
190 )
191
192
193async def test_add_item_playlist(library_manager: TidalLibraryManager, provider_mock: Mock) -> None:
194 """Test add_item for playlist."""
195 item = Mock(item_id="123", media_type=MediaType.PLAYLIST)
196 await library_manager.add_item(item)
197
198 provider_mock.api.post.assert_called_with(
199 "users/12345/favorites/playlists",
200 data={"uuids": "123"},
201 as_form=True,
202 )
203
204
205async def test_remove_item_artist(
206 library_manager: TidalLibraryManager, provider_mock: Mock
207) -> None:
208 """Test remove_item for artist."""
209 await library_manager.remove_item("123", MediaType.ARTIST)
210
211 provider_mock.api.delete.assert_called_with("users/12345/favorites/artists/123")
212
213
214async def test_remove_item_album(library_manager: TidalLibraryManager, provider_mock: Mock) -> None:
215 """Test remove_item for album."""
216 await library_manager.remove_item("123", MediaType.ALBUM)
217
218 provider_mock.api.delete.assert_called_with("users/12345/favorites/albums/123")
219
220
221async def test_remove_item_track(library_manager: TidalLibraryManager, provider_mock: Mock) -> None:
222 """Test remove_item for track."""
223 await library_manager.remove_item("123", MediaType.TRACK)
224
225 provider_mock.api.delete.assert_called_with("users/12345/favorites/tracks/123")
226
227
228async def test_remove_item_playlist(
229 library_manager: TidalLibraryManager, provider_mock: Mock
230) -> None:
231 """Test remove_item for playlist."""
232 await library_manager.remove_item("123", MediaType.PLAYLIST)
233
234 provider_mock.api.delete.assert_called_with("users/12345/favorites/playlists/123")
235