/
/
/
1"""Test Tidal Provider integration."""
2
3from collections.abc import AsyncGenerator
4from typing import Any
5from unittest.mock import AsyncMock, Mock, patch
6
7import pytest
8from music_assistant_models.enums import MediaType
9from music_assistant_models.errors import LoginFailed
10from music_assistant_models.media_items import Album, Artist, Playlist, Track
11
12from music_assistant.providers.tidal.provider import TidalProvider
13
14
15@pytest.fixture
16def mass_mock() -> Mock:
17 """Return a mock MusicAssistant instance."""
18 mass = Mock()
19 mass.http_session = AsyncMock()
20 mass.metadata.locale = "en_US"
21 mass.cache.get = AsyncMock(return_value=None)
22 mass.cache.set = AsyncMock()
23 mass.cache.delete = AsyncMock()
24 return mass
25
26
27@pytest.fixture
28def manifest_mock() -> Mock:
29 """Return a mock provider manifest."""
30 manifest = Mock()
31 manifest.domain = "tidal"
32 return manifest
33
34
35@pytest.fixture
36def config_mock() -> Mock:
37 """Return a mock provider config."""
38 config = Mock()
39 config.name = "Tidal Test"
40 config.instance_id = "tidal_test"
41 config.enabled = True
42 config.get_value.side_effect = lambda key: {
43 "auth_token": "mock_access_token",
44 "refresh_token": "mock_refresh_token",
45 "expiry_time": 1234567890,
46 "user_id": "12345",
47 "log_level": "INFO",
48 }.get(key, "INFO" if "log" in key else None)
49 return config
50
51
52@pytest.fixture
53def provider(mass_mock: Mock, manifest_mock: Mock, config_mock: Mock) -> TidalProvider:
54 """Return a TidalProvider instance."""
55 return TidalProvider(mass_mock, manifest_mock, config_mock)
56
57
58async def test_provider_initialization(
59 mass_mock: Mock, manifest_mock: Mock, config_mock: Mock
60) -> None:
61 """Test provider initialization creates all managers."""
62 provider = TidalProvider(mass_mock, manifest_mock, config_mock)
63
64 assert provider.auth is not None
65 assert provider.api is not None
66 assert provider.library is not None
67 assert provider.media is not None
68 assert provider.playlists is not None
69 assert provider.recommendations_manager is not None
70 assert provider.streaming is not None
71
72
73async def test_handle_async_init_success(provider: TidalProvider) -> None:
74 """Test successful async initialization."""
75 with (
76 patch.object(provider.auth, "initialize", new_callable=AsyncMock) as mock_init,
77 patch.object(provider.api, "get", new_callable=AsyncMock) as mock_get,
78 patch.object(provider, "get_user", new_callable=AsyncMock) as mock_get_user,
79 patch.object(provider.auth, "update_user_info", new_callable=AsyncMock),
80 ):
81 mock_init.return_value = True
82 mock_get.return_value = ({"userId": "12345", "sessionId": "session_123"}, None)
83 mock_get_user.return_value = {"id": "12345", "username": "testuser"}
84
85 await provider.handle_async_init()
86
87 mock_init.assert_called_once()
88 mock_get.assert_called_with("sessions")
89
90
91async def test_handle_async_init_missing_auth() -> None:
92 """Test async initialization fails with missing auth."""
93 mass = Mock()
94 mass.http_session = AsyncMock()
95 mass.metadata.locale = "en_US"
96
97 manifest = Mock()
98 manifest.domain = "tidal"
99
100 config = Mock()
101 config.name = "Tidal Test"
102 config.instance_id = "tidal_test"
103 config.enabled = True
104 config.get_value.side_effect = lambda key: "INFO" if "log" in key else None # Missing auth data
105
106 provider = TidalProvider(mass, manifest, config)
107
108 with pytest.raises(LoginFailed, match="Missing authentication data"):
109 await provider.handle_async_init()
110
111
112async def test_handle_async_init_auth_failed(provider: TidalProvider) -> None:
113 """Test async initialization fails when auth initialize fails."""
114 with patch.object(provider.auth, "initialize", new_callable=AsyncMock) as mock_init:
115 mock_init.return_value = False
116
117 with pytest.raises(LoginFailed, match="Failed to authenticate with Tidal"):
118 await provider.handle_async_init()
119
120
121async def test_search_delegates_to_media(provider: TidalProvider) -> None:
122 """Test search delegates to media manager."""
123 with patch.object(provider.media, "search", new_callable=AsyncMock) as mock_search:
124 mock_search.return_value = Mock()
125
126 await provider.search("test query", [MediaType.ARTIST], limit=10)
127
128 mock_search.assert_called_with("test query", [MediaType.ARTIST], 10)
129
130
131async def test_get_similar_tracks_delegates_to_media(provider: TidalProvider) -> None:
132 """Test get_similar_tracks delegates to media manager."""
133 with patch.object(provider.media, "get_similar_tracks", new_callable=AsyncMock) as mock_get:
134 mock_get.return_value = []
135
136 result = await provider.get_similar_tracks("123", limit=30)
137
138 mock_get.assert_called_with("123", 30)
139 assert result == []
140
141
142async def test_get_artist_delegates_to_media(provider: TidalProvider) -> None:
143 """Test get_artist delegates to media manager."""
144 with patch.object(provider.media, "get_artist", new_callable=AsyncMock) as mock_get:
145 mock_get.return_value = Mock(spec=Artist)
146
147 result = await provider.get_artist("123")
148
149 mock_get.assert_called_with("123")
150 assert result is not None
151
152
153async def test_get_album_delegates_to_media(provider: TidalProvider) -> None:
154 """Test get_album delegates to media manager."""
155 with patch.object(provider.media, "get_album", new_callable=AsyncMock) as mock_get:
156 mock_get.return_value = Mock(spec=Album)
157
158 result = await provider.get_album("123")
159
160 mock_get.assert_called_with("123")
161 assert result is not None
162
163
164async def test_get_track_delegates_to_media(provider: TidalProvider) -> None:
165 """Test get_track delegates to media manager."""
166 with patch.object(provider.media, "get_track", new_callable=AsyncMock) as mock_get:
167 mock_get.return_value = Mock(spec=Track)
168
169 result = await provider.get_track("123")
170
171 mock_get.assert_called_with("123")
172 assert result is not None
173
174
175async def test_get_playlist_delegates_to_media(provider: TidalProvider) -> None:
176 """Test get_playlist delegates to media manager."""
177 with patch.object(provider.media, "get_playlist", new_callable=AsyncMock) as mock_get:
178 mock_get.return_value = Mock(spec=Playlist)
179
180 result = await provider.get_playlist("123")
181
182 mock_get.assert_called_with("123")
183 assert result is not None
184
185
186async def test_get_album_tracks_delegates_to_media(provider: TidalProvider) -> None:
187 """Test get_album_tracks delegates to media manager."""
188 with patch.object(provider.media, "get_album_tracks", new_callable=AsyncMock) as mock_get:
189 mock_get.return_value = []
190
191 result = await provider.get_album_tracks("123")
192
193 mock_get.assert_called_with("123")
194 assert result == []
195
196
197async def test_get_artist_albums_delegates_to_media(provider: TidalProvider) -> None:
198 """Test get_artist_albums delegates to media manager."""
199 with patch.object(provider.media, "get_artist_albums", new_callable=AsyncMock) as mock_get:
200 mock_get.return_value = []
201
202 result = await provider.get_artist_albums("123")
203
204 mock_get.assert_called_with("123")
205 assert result == []
206
207
208async def test_get_artist_toptracks_delegates_to_media(provider: TidalProvider) -> None:
209 """Test get_artist_toptracks delegates to media manager."""
210 with patch.object(provider.media, "get_artist_toptracks", new_callable=AsyncMock) as mock_get:
211 mock_get.return_value = []
212
213 await provider.get_artist_toptracks("123")
214
215 mock_get.assert_called_with("123")
216
217
218async def test_get_playlist_tracks_delegates_to_media(provider: TidalProvider) -> None:
219 """Test get_playlist_tracks delegates to media manager."""
220 with patch.object(provider.media, "get_playlist_tracks", new_callable=AsyncMock) as mock_get:
221 mock_get.return_value = []
222
223 await provider.get_playlist_tracks("123", page=2)
224
225 mock_get.assert_called_with("123", 2)
226
227
228async def test_get_stream_details_delegates_to_streaming(provider: TidalProvider) -> None:
229 """Test get_stream_details delegates to streaming manager."""
230 with patch.object(provider.streaming, "get_stream_details", new_callable=AsyncMock) as mock_get:
231 mock_get.return_value = Mock()
232
233 result = await provider.get_stream_details("123")
234
235 mock_get.assert_called_with("123")
236 assert result is not None
237
238
239async def test_get_item_mapping(provider: TidalProvider) -> None:
240 """Test get_item_mapping creates correct ItemMapping."""
241 mapping = provider.get_item_mapping(MediaType.ARTIST, "123", "Test Artist")
242
243 assert mapping.media_type == MediaType.ARTIST
244 assert mapping.item_id == "123"
245 assert mapping.provider == provider.instance_id
246 assert mapping.name == "Test Artist"
247
248
249async def test_get_library_artists_delegates_to_library(provider: TidalProvider) -> None:
250 """Test get_library_artists delegates to library manager."""
251
252 async def mock_generator() -> AsyncGenerator[Any, None]:
253 yield Mock(spec=Artist)
254 yield Mock(spec=Artist)
255
256 with patch.object(provider.library, "get_artists", return_value=mock_generator()):
257 artists = []
258 async for artist in provider.get_library_artists():
259 artists.append(artist)
260
261 assert len(artists) == 2
262
263
264async def test_get_library_albums_delegates_to_library(provider: TidalProvider) -> None:
265 """Test get_library_albums delegates to library manager."""
266
267 async def mock_generator() -> AsyncGenerator[Any, None]:
268 yield Mock(spec=Album)
269
270 with patch.object(provider.library, "get_albums", return_value=mock_generator()):
271 albums = []
272 async for album in provider.get_library_albums():
273 albums.append(album)
274
275 assert len(albums) == 1
276
277
278async def test_get_library_tracks_delegates_to_library(provider: TidalProvider) -> None:
279 """Test get_library_tracks delegates to library manager."""
280
281 async def mock_generator() -> AsyncGenerator[Any, None]:
282 yield Mock(spec=Track)
283 yield Mock(spec=Track)
284 yield Mock(spec=Track)
285
286 with patch.object(provider.library, "get_tracks", return_value=mock_generator()):
287 tracks = []
288 async for track in provider.get_library_tracks():
289 tracks.append(track)
290
291 assert len(tracks) == 3
292
293
294async def test_get_library_playlists_delegates_to_library(provider: TidalProvider) -> None:
295 """Test get_library_playlists delegates to library manager."""
296
297 async def mock_generator() -> AsyncGenerator[Any, None]:
298 yield Mock(spec=Playlist)
299
300 with patch.object(provider.library, "get_playlists", return_value=mock_generator()):
301 playlists = []
302 async for playlist in provider.get_library_playlists():
303 playlists.append(playlist)
304
305 assert len(playlists) == 1
306
307
308async def test_library_add_delegates_to_library(provider: TidalProvider) -> None:
309 """Test library_add delegates to library manager."""
310 with patch.object(provider.library, "add_item", new_callable=AsyncMock) as mock_add:
311 mock_add.return_value = True
312 item = Mock()
313
314 result = await provider.library_add(item)
315
316 assert result is True
317 mock_add.assert_called_with(item)
318
319
320async def test_library_remove_delegates_to_library(provider: TidalProvider) -> None:
321 """Test library_remove delegates to library manager."""
322 with patch.object(provider.library, "remove_item", new_callable=AsyncMock) as mock_remove:
323 mock_remove.return_value = True
324
325 result = await provider.library_remove("123", MediaType.TRACK)
326
327 assert result is True
328 mock_remove.assert_called_with("123", MediaType.TRACK)
329
330
331async def test_create_playlist_delegates_to_playlists(provider: TidalProvider) -> None:
332 """Test create_playlist delegates to playlist manager."""
333 with patch.object(provider.playlists, "create", new_callable=AsyncMock) as mock_create:
334 mock_create.return_value = Mock(spec=Playlist)
335
336 await provider.create_playlist("New Playlist")
337
338 mock_create.assert_called_with("New Playlist")
339
340
341async def test_add_playlist_tracks_delegates_to_playlists(provider: TidalProvider) -> None:
342 """Test add_playlist_tracks delegates to playlist manager."""
343 with patch.object(provider.playlists, "add_tracks", new_callable=AsyncMock) as mock_add:
344 await provider.add_playlist_tracks("123", ["track1", "track2"])
345
346 mock_add.assert_called_with("123", ["track1", "track2"])
347
348
349async def test_remove_playlist_tracks_delegates_to_playlists(provider: TidalProvider) -> None:
350 """Test remove_playlist_tracks delegates to playlist manager."""
351 with patch.object(provider.playlists, "remove_tracks", new_callable=AsyncMock) as mock_remove:
352 await provider.remove_playlist_tracks("123", (1, 2, 3))
353
354 mock_remove.assert_called_with("123", (1, 2, 3))
355
356
357async def test_recommendations_delegates_to_recommendations_manager(
358 provider: TidalProvider,
359) -> None:
360 """Test recommendations delegates to recommendations manager."""
361 with patch.object(
362 provider.recommendations_manager, "get_recommendations", new_callable=AsyncMock
363 ) as mock_get:
364 mock_get.return_value = []
365
366 await provider.recommendations()
367
368 mock_get.assert_called_once()
369
370
371async def test_get_user(provider: TidalProvider) -> None:
372 """Test get_user fetches user data."""
373 with patch.object(provider.api, "get_data", new_callable=AsyncMock) as mock_get:
374 mock_get.return_value = {"id": "123", "username": "testuser"}
375
376 user = await provider.get_user("123")
377
378 assert user["id"] == "123"
379 mock_get.assert_called_with("users/123")
380