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