/
/
/
1"""Test Yandex Music Recommendations."""
2
3from __future__ import annotations
4
5from typing import Any
6from unittest.mock import AsyncMock, Mock, patch
7
8import pytest
9from music_assistant_models.errors import InvalidDataError
10from music_assistant_models.media_items import Album, Playlist, RecommendationFolder, Track
11
12from music_assistant.providers.yandex_music.constants import (
13 BROWSE_NAMES_EN,
14 MY_WAVE_PLAYLIST_ID,
15 RADIO_TRACK_ID_SEP,
16 ROTOR_STATION_MY_WAVE,
17)
18from music_assistant.providers.yandex_music.provider import YandexMusicProvider
19
20
21@pytest.fixture
22def provider_mock() -> Mock:
23 """Return a mock Yandex Music provider."""
24 provider = Mock(spec=YandexMusicProvider)
25 provider.domain = "yandex_music"
26 provider.instance_id = "yandex_music_instance"
27 provider.logger = Mock()
28
29 # Mock client
30 provider.client = AsyncMock()
31 provider.client.user_id = 12345
32
33 # Mock config
34 provider.config = Mock()
35 provider.config.get_value = Mock(side_effect=lambda key: 150 if "max_tracks" in key else None)
36
37 # Mock mass with cache
38 provider.mass = Mock()
39 provider.mass.metadata = Mock()
40 provider.mass.metadata.locale = "en_US"
41 provider.mass.cache = AsyncMock()
42 provider.mass.cache.get = AsyncMock(return_value=None) # Cache always misses
43 provider.mass.cache.set = AsyncMock()
44
45 # Mock _get_browse_names to return EN names
46 provider._get_browse_names = Mock(return_value=BROWSE_NAMES_EN)
47
48 return provider
49
50
51@pytest.mark.asyncio
52async def test_get_my_wave_recommendations_success(provider_mock: Mock) -> None:
53 """Test _get_my_wave_recommendations returns data when API provides tracks."""
54 # Create mock track with required attributes
55 mock_track = Mock()
56 mock_track.id = "12345"
57 mock_track.track_id = "12345"
58
59 # Mock get_my_wave_tracks to return tracks
60 provider_mock.client.get_my_wave_tracks = AsyncMock(return_value=([mock_track], None))
61
62 # Mock _parse_my_wave_track to return a Track object with composite item_id
63 mock_parsed_track = Mock(spec=Track)
64 mock_parsed_track.item_id = f"12345{RADIO_TRACK_ID_SEP}{ROTOR_STATION_MY_WAVE}"
65 mock_parsed_track.name = "Test Track"
66 mock_parsed_track.provider_mappings = []
67 provider_mock._parse_my_wave_track = Mock(return_value=mock_parsed_track)
68
69 result = await YandexMusicProvider._get_my_wave_recommendations(provider_mock)
70
71 assert result is not None
72 assert isinstance(result, RecommendationFolder)
73 assert result.item_id == MY_WAVE_PLAYLIST_ID
74 assert result.provider == provider_mock.instance_id
75 assert result.name == BROWSE_NAMES_EN[MY_WAVE_PLAYLIST_ID]
76 assert result.icon == "mdi-waveform"
77 assert len(result.items) > 0
78
79
80@pytest.mark.asyncio
81async def test_get_my_wave_recommendations_empty(provider_mock: Mock) -> None:
82 """Test _get_my_wave_recommendations returns None when API returns no tracks."""
83 provider_mock.client.get_my_wave_tracks = AsyncMock(return_value=([], None))
84
85 result = await YandexMusicProvider._get_my_wave_recommendations(provider_mock)
86
87 assert result is None
88
89
90@pytest.mark.asyncio
91async def test_get_my_wave_recommendations_duplicate_filtering(provider_mock: Mock) -> None:
92 """Test _get_my_wave_recommendations filters duplicate tracks."""
93 # Create mock tracks with same ID
94 mock_track1 = Mock()
95 mock_track1.id = "12345"
96 mock_track1.track_id = "12345"
97
98 mock_track2 = Mock()
99 mock_track2.id = "12345" # Same ID
100 mock_track2.track_id = "12345"
101
102 # First call returns track1, second call returns track2 (duplicate)
103 provider_mock.client.get_my_wave_tracks = AsyncMock(
104 side_effect=[
105 ([mock_track1], None),
106 ([mock_track2], None),
107 ]
108 )
109
110 mock_parsed_track = Mock(spec=Track)
111 mock_parsed_track.item_id = f"12345{RADIO_TRACK_ID_SEP}{ROTOR_STATION_MY_WAVE}"
112 mock_parsed_track.name = "Test Track"
113 mock_parsed_track.provider_mappings = []
114
115 # _parse_my_wave_track returns track on first call, None on duplicate
116 provider_mock._parse_my_wave_track = Mock(side_effect=[mock_parsed_track, None])
117
118 result = await YandexMusicProvider._get_my_wave_recommendations(provider_mock)
119
120 assert result is not None
121 # Should only have 1 track despite 2 API calls (duplicate filtered)
122 assert len(result.items) == 1
123
124
125@pytest.mark.asyncio
126async def test_get_my_wave_recommendations_invalid_data_error(provider_mock: Mock) -> None:
127 """Test _get_my_wave_recommendations handles InvalidDataError gracefully."""
128 mock_track = Mock()
129 mock_track.id = "12345"
130 mock_track.track_id = "12345"
131
132 provider_mock.client.get_my_wave_tracks = AsyncMock(return_value=([mock_track], None))
133
134 # _parse_my_wave_track returns None (simulates parse error handled internally)
135 provider_mock._parse_my_wave_track = Mock(return_value=None)
136
137 result = await YandexMusicProvider._get_my_wave_recommendations(provider_mock)
138
139 # Should return None as no valid tracks were parsed
140 assert result is None
141
142
143@pytest.mark.asyncio
144async def test_get_feed_recommendations_success(provider_mock: Mock) -> None:
145 """Test _get_feed_recommendations returns data when API provides feed."""
146 # Mock feed with generated playlists
147 mock_gen_playlist = Mock()
148 mock_gen_playlist.ready = True
149 mock_gen_playlist.data = Mock() # Playlist data
150
151 mock_feed = Mock()
152 mock_feed.generated_playlists = [mock_gen_playlist]
153
154 provider_mock.client.get_feed = AsyncMock(return_value=mock_feed)
155
156 # Mock parse_playlist
157 mock_parsed_playlist = Mock(spec=Playlist)
158 mock_parsed_playlist.item_id = "playlist_1"
159 mock_parsed_playlist.name = "Playlist of the Day"
160
161 with patch(
162 "music_assistant.providers.yandex_music.provider.parse_playlist",
163 return_value=mock_parsed_playlist,
164 ):
165 result = await YandexMusicProvider._get_feed_recommendations(provider_mock)
166
167 assert result is not None
168 assert isinstance(result, RecommendationFolder)
169 assert result.item_id == "feed"
170 assert result.provider == provider_mock.instance_id
171 assert result.name == BROWSE_NAMES_EN["feed"]
172 assert result.icon == "mdi-account-music"
173 assert len(result.items) > 0
174
175
176@pytest.mark.asyncio
177async def test_get_feed_recommendations_empty(provider_mock: Mock) -> None:
178 """Test _get_feed_recommendations returns None when feed is empty."""
179 provider_mock.client.get_feed = AsyncMock(return_value=None)
180
181 result = await YandexMusicProvider._get_feed_recommendations(provider_mock)
182
183 assert result is None
184
185
186@pytest.mark.asyncio
187async def test_get_feed_recommendations_no_generated_playlists(provider_mock: Mock) -> None:
188 """Test _get_feed_recommendations returns None when no generated playlists."""
189 mock_feed = Mock()
190 mock_feed.generated_playlists = []
191
192 provider_mock.client.get_feed = AsyncMock(return_value=mock_feed)
193
194 result = await YandexMusicProvider._get_feed_recommendations(provider_mock)
195
196 assert result is None
197
198
199@pytest.mark.asyncio
200async def test_get_feed_recommendations_invalid_data_error(provider_mock: Mock) -> None:
201 """Test _get_feed_recommendations handles InvalidDataError gracefully."""
202 mock_gen_playlist = Mock()
203 mock_gen_playlist.ready = True
204 mock_gen_playlist.data = Mock()
205
206 mock_feed = Mock()
207 mock_feed.generated_playlists = [mock_gen_playlist]
208
209 provider_mock.client.get_feed = AsyncMock(return_value=mock_feed)
210
211 with patch(
212 "music_assistant.providers.yandex_music.provider.parse_playlist",
213 side_effect=InvalidDataError("Parse error"),
214 ):
215 result = await YandexMusicProvider._get_feed_recommendations(provider_mock)
216
217 assert result is None
218 provider_mock.logger.debug.assert_called()
219
220
221@pytest.mark.asyncio
222async def test_get_chart_recommendations_success(provider_mock: Mock) -> None:
223 """Test _get_chart_recommendations returns data when API provides chart."""
224 # Mock TrackShort with .track attribute
225 mock_track_short = Mock()
226 mock_track_obj = Mock() # The actual Track object
227 mock_track_short.track = mock_track_obj
228
229 mock_chart = Mock()
230 mock_chart.tracks = [mock_track_short]
231
232 mock_chart_info = Mock()
233 mock_chart_info.chart = mock_chart
234
235 provider_mock.client.get_chart = AsyncMock(return_value=mock_chart_info)
236
237 # Mock parse_track
238 mock_parsed_track = Mock(spec=Track)
239 mock_parsed_track.item_id = "track_1"
240 mock_parsed_track.name = "Chart Track 1"
241
242 with patch(
243 "music_assistant.providers.yandex_music.provider.parse_track",
244 return_value=mock_parsed_track,
245 ):
246 result = await YandexMusicProvider._get_chart_recommendations(provider_mock)
247
248 assert result is not None
249 assert isinstance(result, RecommendationFolder)
250 assert result.item_id == "chart"
251 assert result.provider == provider_mock.instance_id
252 assert result.name == BROWSE_NAMES_EN["chart"]
253 assert result.icon == "mdi-chart-line"
254 assert len(result.items) > 0
255
256
257@pytest.mark.asyncio
258async def test_get_chart_recommendations_empty(provider_mock: Mock) -> None:
259 """Test _get_chart_recommendations returns None when chart is empty."""
260 provider_mock.client.get_chart = AsyncMock(return_value=None)
261
262 result = await YandexMusicProvider._get_chart_recommendations(provider_mock)
263
264 assert result is None
265
266
267@pytest.mark.asyncio
268async def test_get_chart_recommendations_no_tracks(provider_mock: Mock) -> None:
269 """Test _get_chart_recommendations returns None when chart has no tracks."""
270 mock_chart = Mock()
271 mock_chart.tracks = []
272
273 mock_chart_info = Mock()
274 mock_chart_info.chart = mock_chart
275
276 provider_mock.client.get_chart = AsyncMock(return_value=mock_chart_info)
277
278 result = await YandexMusicProvider._get_chart_recommendations(provider_mock)
279
280 assert result is None
281
282
283@pytest.mark.asyncio
284async def test_get_chart_recommendations_invalid_data_error(provider_mock: Mock) -> None:
285 """Test _get_chart_recommendations handles InvalidDataError gracefully."""
286 mock_track_short = Mock()
287 mock_track_obj = Mock()
288 mock_track_short.track = mock_track_obj
289
290 mock_chart = Mock()
291 mock_chart.tracks = [mock_track_short]
292
293 mock_chart_info = Mock()
294 mock_chart_info.chart = mock_chart
295
296 provider_mock.client.get_chart = AsyncMock(return_value=mock_chart_info)
297
298 with patch(
299 "music_assistant.providers.yandex_music.provider.parse_track",
300 side_effect=InvalidDataError("Parse error"),
301 ):
302 result = await YandexMusicProvider._get_chart_recommendations(provider_mock)
303
304 assert result is None
305 provider_mock.logger.debug.assert_called()
306
307
308@pytest.mark.asyncio
309async def test_get_new_releases_recommendations_success(provider_mock: Mock) -> None:
310 """Test _get_new_releases_recommendations returns data when API provides releases."""
311 # Mock releases with album IDs
312 mock_releases = Mock()
313 mock_releases.new_releases = [123, 456, 789]
314
315 provider_mock.client.get_new_releases = AsyncMock(return_value=mock_releases)
316
317 # Mock get_albums to return album objects
318 mock_album = Mock()
319 provider_mock.client.get_albums = AsyncMock(return_value=[mock_album])
320
321 # Mock parse_album
322 mock_parsed_album = Mock(spec=Album)
323 mock_parsed_album.item_id = "album_1"
324 mock_parsed_album.name = "New Album"
325
326 with patch(
327 "music_assistant.providers.yandex_music.provider.parse_album",
328 return_value=mock_parsed_album,
329 ):
330 result = await YandexMusicProvider._get_new_releases_recommendations(provider_mock)
331
332 assert result is not None
333 assert isinstance(result, RecommendationFolder)
334 assert result.item_id == "new_releases"
335 assert result.provider == provider_mock.instance_id
336 assert result.name == BROWSE_NAMES_EN["new_releases"]
337 assert result.icon == "mdi-new-box"
338 assert len(result.items) > 0
339
340
341@pytest.mark.asyncio
342async def test_get_new_releases_recommendations_empty(provider_mock: Mock) -> None:
343 """Test _get_new_releases_recommendations returns None when releases are empty."""
344 provider_mock.client.get_new_releases = AsyncMock(return_value=None)
345
346 result = await YandexMusicProvider._get_new_releases_recommendations(provider_mock)
347
348 assert result is None
349
350
351@pytest.mark.asyncio
352async def test_get_new_releases_recommendations_no_releases(provider_mock: Mock) -> None:
353 """Test _get_new_releases_recommendations returns None when no releases."""
354 mock_releases = Mock()
355 mock_releases.new_releases = []
356
357 provider_mock.client.get_new_releases = AsyncMock(return_value=mock_releases)
358
359 result = await YandexMusicProvider._get_new_releases_recommendations(provider_mock)
360
361 assert result is None
362
363
364@pytest.mark.asyncio
365async def test_get_new_releases_recommendations_invalid_data_error(provider_mock: Mock) -> None:
366 """Test _get_new_releases_recommendations handles InvalidDataError gracefully."""
367 mock_releases = Mock()
368 mock_releases.new_releases = [123]
369
370 provider_mock.client.get_new_releases = AsyncMock(return_value=mock_releases)
371 provider_mock.client.get_albums = AsyncMock(return_value=[Mock()])
372
373 with patch(
374 "music_assistant.providers.yandex_music.provider.parse_album",
375 side_effect=InvalidDataError("Parse error"),
376 ):
377 result = await YandexMusicProvider._get_new_releases_recommendations(provider_mock)
378
379 assert result is None
380 provider_mock.logger.debug.assert_called()
381
382
383@pytest.mark.asyncio
384async def test_get_new_playlists_recommendations_success(provider_mock: Mock) -> None:
385 """Test _get_new_playlists_recommendations returns data when API provides playlists."""
386 # Mock playlist ID object
387 mock_playlist_id = Mock()
388 mock_playlist_id.uid = "user123"
389 mock_playlist_id.kind = "456"
390
391 mock_result = Mock()
392 mock_result.new_playlists = [mock_playlist_id]
393
394 provider_mock.client.get_new_playlists = AsyncMock(return_value=mock_result)
395
396 # Mock get_playlists to return playlist objects
397 mock_playlist = Mock()
398 provider_mock.client.get_playlists = AsyncMock(return_value=[mock_playlist])
399
400 # Mock parse_playlist
401 mock_parsed_playlist = Mock(spec=Playlist)
402 mock_parsed_playlist.item_id = "playlist_1"
403 mock_parsed_playlist.name = "New Playlist"
404
405 with patch(
406 "music_assistant.providers.yandex_music.provider.parse_playlist",
407 return_value=mock_parsed_playlist,
408 ):
409 result = await YandexMusicProvider._get_new_playlists_recommendations(provider_mock)
410
411 assert result is not None
412 assert isinstance(result, RecommendationFolder)
413 assert result.item_id == "new_playlists"
414 assert result.provider == provider_mock.instance_id
415 assert result.name == BROWSE_NAMES_EN["new_playlists"]
416 assert result.icon == "mdi-playlist-star"
417 assert len(result.items) > 0
418
419
420@pytest.mark.asyncio
421async def test_get_new_playlists_recommendations_empty(provider_mock: Mock) -> None:
422 """Test _get_new_playlists_recommendations returns None when result is empty."""
423 provider_mock.client.get_new_playlists = AsyncMock(return_value=None)
424
425 result = await YandexMusicProvider._get_new_playlists_recommendations(provider_mock)
426
427 assert result is None
428
429
430@pytest.mark.asyncio
431async def test_get_new_playlists_recommendations_no_playlists(provider_mock: Mock) -> None:
432 """Test _get_new_playlists_recommendations returns None when no playlists."""
433 mock_result = Mock()
434 mock_result.new_playlists = []
435
436 provider_mock.client.get_new_playlists = AsyncMock(return_value=mock_result)
437
438 result = await YandexMusicProvider._get_new_playlists_recommendations(provider_mock)
439
440 assert result is None
441
442
443@pytest.mark.asyncio
444async def test_get_new_playlists_recommendations_invalid_data_error(provider_mock: Mock) -> None:
445 """Test _get_new_playlists_recommendations handles InvalidDataError gracefully."""
446 mock_playlist_id = Mock()
447 mock_playlist_id.uid = "user123"
448 mock_playlist_id.kind = "456"
449
450 mock_result = Mock()
451 mock_result.new_playlists = [mock_playlist_id]
452
453 provider_mock.client.get_new_playlists = AsyncMock(return_value=mock_result)
454 provider_mock.client.get_playlists = AsyncMock(return_value=[Mock()])
455
456 with patch(
457 "music_assistant.providers.yandex_music.provider.parse_playlist",
458 side_effect=InvalidDataError("Parse error"),
459 ):
460 result = await YandexMusicProvider._get_new_playlists_recommendations(provider_mock)
461
462 assert result is None
463 provider_mock.logger.debug.assert_called()
464
465
466@pytest.mark.asyncio
467async def test_get_top_picks_recommendations_success(provider_mock: Mock) -> None:
468 """Test _get_top_picks_recommendations returns data when API provides playlists."""
469 mock_playlist = Mock()
470 provider_mock.client.get_tag_playlists = AsyncMock(return_value=[mock_playlist])
471
472 # Mock parse_playlist
473 mock_parsed_playlist = Mock(spec=Playlist)
474 mock_parsed_playlist.item_id = "playlist_1"
475 mock_parsed_playlist.name = "Top Pick"
476
477 with patch(
478 "music_assistant.providers.yandex_music.provider.parse_playlist",
479 return_value=mock_parsed_playlist,
480 ):
481 result = await YandexMusicProvider._get_top_picks_recommendations(provider_mock)
482
483 assert result is not None
484 assert isinstance(result, RecommendationFolder)
485 assert result.item_id == "top_picks"
486 assert result.provider == provider_mock.instance_id
487 assert result.name == BROWSE_NAMES_EN["top_picks"]
488 assert result.icon == "mdi-star"
489 assert len(result.items) > 0
490 # Verify it called with "top" tag
491 provider_mock.client.get_tag_playlists.assert_called_once_with("top")
492
493
494@pytest.mark.asyncio
495async def test_get_top_picks_recommendations_empty(provider_mock: Mock) -> None:
496 """Test _get_top_picks_recommendations returns None when API returns empty."""
497 provider_mock.client.get_tag_playlists = AsyncMock(return_value=[])
498
499 result = await YandexMusicProvider._get_top_picks_recommendations(provider_mock)
500
501 assert result is None
502
503
504@pytest.mark.asyncio
505async def test_get_top_picks_recommendations_invalid_data_error(provider_mock: Mock) -> None:
506 """Test _get_top_picks_recommendations handles InvalidDataError gracefully."""
507 provider_mock.client.get_tag_playlists = AsyncMock(return_value=[Mock()])
508
509 with patch(
510 "music_assistant.providers.yandex_music.provider.parse_playlist",
511 side_effect=InvalidDataError("Parse error"),
512 ):
513 result = await YandexMusicProvider._get_top_picks_recommendations(provider_mock)
514
515 assert result is None
516 provider_mock.logger.debug.assert_called()
517
518
519@pytest.mark.asyncio
520async def test_get_mood_mix_recommendations_success(provider_mock: Mock) -> None:
521 """Test _get_mood_mix_recommendations returns data with deterministic random choice."""
522 mock_playlist = Mock()
523 provider_mock.client.get_tag_playlists = AsyncMock(return_value=[mock_playlist])
524
525 # Mock parse_playlist
526 mock_parsed_playlist = Mock(spec=Playlist)
527 mock_parsed_playlist.item_id = "playlist_1"
528 mock_parsed_playlist.name = "Chill Playlist"
529
530 # No need to patch random.choice - tag is now passed as argument
531 with patch(
532 "music_assistant.providers.yandex_music.provider.parse_playlist",
533 return_value=mock_parsed_playlist,
534 ):
535 result = await YandexMusicProvider._get_mood_mix_recommendations(provider_mock, "chill")
536
537 assert result is not None
538 assert isinstance(result, RecommendationFolder)
539 assert result.item_id == "mood_mix"
540 assert result.provider == provider_mock.instance_id
541 # Name should include the mood tag
542 assert "Chill" in result.name or "chill" in result.name.lower()
543 assert result.icon == "mdi-emoticon-outline"
544 assert len(result.items) > 0
545 # Verify it called with mood tag
546 provider_mock.client.get_tag_playlists.assert_called_once_with("chill")
547
548
549@pytest.mark.asyncio
550async def test_get_mood_mix_recommendations_empty(provider_mock: Mock) -> None:
551 """Test _get_mood_mix_recommendations returns None when API returns empty."""
552 provider_mock.client.get_tag_playlists = AsyncMock(return_value=[])
553
554 result = await YandexMusicProvider._get_mood_mix_recommendations(provider_mock, "sad")
555
556 assert result is None
557
558
559@pytest.mark.asyncio
560async def test_get_mood_mix_recommendations_invalid_data_error(provider_mock: Mock) -> None:
561 """Test _get_mood_mix_recommendations handles InvalidDataError gracefully."""
562 provider_mock.client.get_tag_playlists = AsyncMock(return_value=[Mock()])
563
564 with patch(
565 "music_assistant.providers.yandex_music.provider.parse_playlist",
566 side_effect=InvalidDataError("Parse error"),
567 ):
568 result = await YandexMusicProvider._get_mood_mix_recommendations(provider_mock, "romantic")
569
570 assert result is None
571 provider_mock.logger.debug.assert_called()
572
573
574@pytest.mark.asyncio
575async def test_get_activity_mix_recommendations_success(provider_mock: Mock) -> None:
576 """Test _get_activity_mix_recommendations returns data with deterministic random choice."""
577 mock_playlist = Mock()
578 provider_mock.client.get_tag_playlists = AsyncMock(return_value=[mock_playlist])
579
580 # Mock parse_playlist
581 mock_parsed_playlist = Mock(spec=Playlist)
582 mock_parsed_playlist.item_id = "playlist_1"
583 mock_parsed_playlist.name = "Workout Playlist"
584
585 # No need to patch random.choice - tag is now passed as argument
586 with patch(
587 "music_assistant.providers.yandex_music.provider.parse_playlist",
588 return_value=mock_parsed_playlist,
589 ):
590 result = await YandexMusicProvider._get_activity_mix_recommendations(
591 provider_mock, "workout"
592 )
593
594 assert result is not None
595 assert isinstance(result, RecommendationFolder)
596 assert result.item_id == "activity_mix"
597 assert result.provider == provider_mock.instance_id
598 # Name should include the activity tag
599 assert "Workout" in result.name or "workout" in result.name.lower()
600 assert result.icon == "mdi-run"
601 assert len(result.items) > 0
602 # Verify it called with activity tag
603 provider_mock.client.get_tag_playlists.assert_called_once_with("workout")
604
605
606@pytest.mark.asyncio
607async def test_get_activity_mix_recommendations_empty(provider_mock: Mock) -> None:
608 """Test _get_activity_mix_recommendations returns None when API returns empty."""
609 provider_mock.client.get_tag_playlists = AsyncMock(return_value=[])
610
611 result = await YandexMusicProvider._get_activity_mix_recommendations(provider_mock, "focus")
612
613 assert result is None
614
615
616@pytest.mark.asyncio
617async def test_get_activity_mix_recommendations_invalid_data_error(provider_mock: Mock) -> None:
618 """Test _get_activity_mix_recommendations handles InvalidDataError gracefully."""
619 provider_mock.client.get_tag_playlists = AsyncMock(return_value=[Mock()])
620
621 with patch(
622 "music_assistant.providers.yandex_music.provider.parse_playlist",
623 side_effect=InvalidDataError("Parse error"),
624 ):
625 result = await YandexMusicProvider._get_activity_mix_recommendations(
626 provider_mock, "morning"
627 )
628
629 assert result is None
630 provider_mock.logger.debug.assert_called()
631
632
633@pytest.mark.asyncio
634async def test_get_seasonal_mix_recommendations_winter(provider_mock: Mock) -> None:
635 """Test _get_seasonal_mix_recommendations returns winter playlists in January."""
636 mock_playlist = Mock()
637 provider_mock.client.get_tag_playlists = AsyncMock(return_value=[mock_playlist])
638
639 # Mock parse_playlist
640 mock_parsed_playlist = Mock(spec=Playlist)
641 mock_parsed_playlist.item_id = "playlist_1"
642 mock_parsed_playlist.name = "Winter Playlist"
643
644 # Patch datetime to return January (month 1)
645 mock_datetime = Mock()
646 mock_datetime.now.return_value.month = 1
647
648 with (
649 patch("music_assistant.providers.yandex_music.provider.datetime", mock_datetime),
650 patch(
651 "music_assistant.providers.yandex_music.provider.parse_playlist",
652 return_value=mock_parsed_playlist,
653 ),
654 ):
655 result = await YandexMusicProvider._get_seasonal_mix_recommendations(provider_mock)
656
657 assert result is not None
658 assert isinstance(result, RecommendationFolder)
659 assert result.item_id == "seasonal_mix"
660 assert result.provider == provider_mock.instance_id
661 # Name should include winter
662 assert "Winter" in result.name or "winter" in result.name.lower()
663 assert result.icon == "mdi-weather-sunny"
664 assert len(result.items) > 0
665 # Verify it called with winter tag
666 provider_mock.client.get_tag_playlists.assert_called_once_with("winter")
667
668
669@pytest.mark.asyncio
670async def test_get_seasonal_mix_recommendations_summer(provider_mock: Mock) -> None:
671 """Test _get_seasonal_mix_recommendations returns summer playlists in July."""
672 mock_playlist = Mock()
673 provider_mock.client.get_tag_playlists = AsyncMock(return_value=[mock_playlist])
674
675 mock_parsed_playlist = Mock(spec=Playlist)
676 mock_parsed_playlist.item_id = "playlist_1"
677 mock_parsed_playlist.name = "Summer Playlist"
678
679 # Patch datetime to return July (month 7)
680 mock_datetime = Mock()
681 mock_datetime.now.return_value.month = 7
682
683 with (
684 patch("music_assistant.providers.yandex_music.provider.datetime", mock_datetime),
685 patch(
686 "music_assistant.providers.yandex_music.provider.parse_playlist",
687 return_value=mock_parsed_playlist,
688 ),
689 ):
690 result = await YandexMusicProvider._get_seasonal_mix_recommendations(provider_mock)
691
692 assert result is not None
693 # Verify it called with summer tag
694 provider_mock.client.get_tag_playlists.assert_called_once_with("summer")
695
696
697@pytest.mark.asyncio
698async def test_get_seasonal_mix_recommendations_spring_fallback(provider_mock: Mock) -> None:
699 """Test _get_seasonal_mix_recommendations falls back to autumn for spring months."""
700 mock_playlist = Mock()
701 provider_mock.client.get_tag_playlists = AsyncMock(return_value=[mock_playlist])
702
703 mock_parsed_playlist = Mock(spec=Playlist)
704 mock_parsed_playlist.item_id = "playlist_1"
705 mock_parsed_playlist.name = "Autumn Playlist"
706
707 # Patch datetime to return March (month 3 - spring)
708 mock_datetime = Mock()
709 mock_datetime.now.return_value.month = 3
710
711 # _validate_tag returns False for spring, triggering fallback to autumn
712 provider_mock._validate_tag = AsyncMock(return_value=False)
713
714 with (
715 patch("music_assistant.providers.yandex_music.provider.datetime", mock_datetime),
716 patch(
717 "music_assistant.providers.yandex_music.provider.parse_playlist",
718 return_value=mock_parsed_playlist,
719 ),
720 ):
721 result = await YandexMusicProvider._get_seasonal_mix_recommendations(provider_mock)
722
723 assert result is not None
724 # Verify it called with autumn tag (spring fallback)
725 provider_mock.client.get_tag_playlists.assert_called_once_with("autumn")
726
727
728@pytest.mark.asyncio
729async def test_get_seasonal_mix_recommendations_empty(provider_mock: Mock) -> None:
730 """Test _get_seasonal_mix_recommendations returns None when API returns empty."""
731 provider_mock.client.get_tag_playlists = AsyncMock(return_value=[])
732
733 mock_datetime = Mock()
734 mock_datetime.now.return_value.month = 6
735
736 with patch("music_assistant.providers.yandex_music.provider.datetime", mock_datetime):
737 result = await YandexMusicProvider._get_seasonal_mix_recommendations(provider_mock)
738
739 assert result is None
740
741
742@pytest.mark.asyncio
743async def test_get_seasonal_mix_recommendations_invalid_data_error(provider_mock: Mock) -> None:
744 """Test _get_seasonal_mix_recommendations handles InvalidDataError gracefully."""
745 provider_mock.client.get_tag_playlists = AsyncMock(return_value=[Mock()])
746
747 mock_datetime = Mock()
748 mock_datetime.now.return_value.month = 9
749
750 with (
751 patch("music_assistant.providers.yandex_music.provider.datetime", mock_datetime),
752 patch(
753 "music_assistant.providers.yandex_music.provider.parse_playlist",
754 side_effect=InvalidDataError("Parse error"),
755 ),
756 ):
757 result = await YandexMusicProvider._get_seasonal_mix_recommendations(provider_mock)
758
759 assert result is None
760 provider_mock.logger.debug.assert_called()
761
762
763@pytest.mark.asyncio
764async def test_recommendations_aggregates_all_folders(provider_mock: Mock) -> None:
765 """Test recommendations() aggregates all recommendation folders."""
766 # Mock all individual recommendation methods to return folders
767 mock_folder = Mock(spec=RecommendationFolder)
768 mock_folder.item_id = "test_folder"
769 mock_folder.name = "Test Folder"
770
771 async def return_folder(*_args: Any, **_kwargs: Any) -> RecommendationFolder:
772 return mock_folder
773
774 async def return_tag(_category: str) -> str:
775 return "test_tag"
776
777 # Set the methods directly on the provider mock instance
778 provider_mock._get_my_wave_recommendations = return_folder
779 provider_mock._get_feed_recommendations = return_folder
780 provider_mock._get_chart_recommendations = return_folder
781 provider_mock._get_new_releases_recommendations = return_folder
782 provider_mock._get_new_playlists_recommendations = return_folder
783 provider_mock._get_top_picks_recommendations = return_folder
784 provider_mock._get_mood_mix_recommendations = return_folder
785 provider_mock._get_activity_mix_recommendations = return_folder
786 provider_mock._get_seasonal_mix_recommendations = return_folder
787 provider_mock._pick_random_tag_for_category = return_tag
788
789 result = await YandexMusicProvider.recommendations(provider_mock)
790
791 assert len(result) == 9 # All 9 methods returned folders
792
793
794@pytest.mark.asyncio
795async def test_recommendations_filters_none_folders(provider_mock: Mock) -> None:
796 """Test recommendations() filters out None results from individual methods."""
797 mock_folder = Mock(spec=RecommendationFolder)
798 mock_folder.item_id = "test_folder"
799 mock_folder.name = "Test Folder"
800
801 # Create async functions that return the desired values
802 async def return_folder(*_args: Any, **_kwargs: Any) -> RecommendationFolder:
803 return mock_folder
804
805 async def return_none(*_args: Any, **_kwargs: Any) -> None:
806 return None
807
808 async def return_tag(_category: str) -> str:
809 return "test_tag"
810
811 # Set the methods directly on the provider mock instance
812 provider_mock._get_my_wave_recommendations = return_folder
813 provider_mock._get_feed_recommendations = return_none
814 provider_mock._get_chart_recommendations = return_folder
815 provider_mock._get_new_releases_recommendations = return_none
816 provider_mock._get_new_playlists_recommendations = return_folder
817 provider_mock._get_top_picks_recommendations = return_none
818 provider_mock._get_mood_mix_recommendations = return_folder
819 provider_mock._get_activity_mix_recommendations = return_none
820 provider_mock._get_seasonal_mix_recommendations = return_folder
821 provider_mock._pick_random_tag_for_category = return_tag
822
823 result = await YandexMusicProvider.recommendations(provider_mock)
824
825 # Should only return 5 folders (4 None were filtered out)
826 assert len(result) == 5
827
828
829@pytest.mark.asyncio
830async def test_recommendations_returns_empty_list_when_all_none(provider_mock: Mock) -> None:
831 """Test recommendations() returns empty list when all methods return None."""
832
833 async def return_none(*_args: Any, **_kwargs: Any) -> None:
834 return None
835
836 async def return_no_tag(_category: str) -> None:
837 return None
838
839 # Set the methods directly on the provider mock instance
840 provider_mock._get_my_wave_recommendations = return_none
841 provider_mock._get_feed_recommendations = return_none
842 provider_mock._get_chart_recommendations = return_none
843 provider_mock._get_new_releases_recommendations = return_none
844 provider_mock._get_new_playlists_recommendations = return_none
845 provider_mock._get_top_picks_recommendations = return_none
846 provider_mock._get_mood_mix_recommendations = return_none
847 provider_mock._get_activity_mix_recommendations = return_none
848 provider_mock._get_seasonal_mix_recommendations = return_none
849 provider_mock._pick_random_tag_for_category = return_no_tag
850
851 result = await YandexMusicProvider.recommendations(provider_mock)
852
853 assert result == []
854