/
/
/
1"""Unit tests for YandexMusicClient (api_client.py)."""
2
3from __future__ import annotations
4
5from unittest import mock
6
7import pytest
8from music_assistant_models.errors import ResourceTemporarilyUnavailable
9from yandex_music.exceptions import NetworkError
10
11from music_assistant.providers.yandex_music.api_client import YandexMusicClient
12
13
14def _make_client() -> tuple[YandexMusicClient, mock.AsyncMock]:
15 """Create a YandexMusicClient with a mocked underlying ClientAsync.
16
17 :return: Tuple of (YandexMusicClient, mock_underlying_client).
18 """
19 client = YandexMusicClient(token="fake_token")
20 mock_underlying = mock.AsyncMock()
21 client._client = mock_underlying
22 client._user_id = 12345
23 return client, mock_underlying
24
25
26# -- get_liked_albums: batching -------------------------------------------------
27
28
29async def test_get_liked_albums_batching() -> None:
30 """Albums are fetched in batch via client.albums() for full metadata."""
31 client, underlying = _make_client()
32
33 # Build 3 minimal "like" objects with album stubs (no cover_uri)
34 likes = []
35 for album_id in (1, 2, 3):
36 album_stub = type("Album", (), {"id": album_id, "cover_uri": None})()
37 like = type("Like", (), {"album": album_stub})()
38 likes.append(like)
39
40 # Full album objects returned by client.albums()
41 full_albums = [
42 type("Album", (), {"id": aid, "cover_uri": f"cover_{aid}"})() for aid in (1, 2, 3)
43 ]
44
45 underlying.users_likes_albums = mock.AsyncMock(return_value=likes)
46 underlying.albums = mock.AsyncMock(return_value=full_albums)
47
48 result = await client.get_liked_albums()
49
50 underlying.albums.assert_awaited_once_with(["1", "2", "3"])
51 assert result == full_albums
52 assert all(a.cover_uri is not None for a in result)
53
54
55async def test_get_liked_albums_batch_fallback_on_network_error() -> None:
56 """When client.albums() fails, fallback returns minimal album data from likes."""
57 client, underlying = _make_client()
58
59 album_stub_1 = type("Album", (), {"id": 10, "cover_uri": None})()
60 album_stub_2 = type("Album", (), {"id": 20, "cover_uri": None})()
61 likes = [
62 type("Like", (), {"album": album_stub_1})(),
63 type("Like", (), {"album": album_stub_2})(),
64 ]
65
66 underlying.users_likes_albums = mock.AsyncMock(return_value=likes)
67 underlying.albums = mock.AsyncMock(side_effect=NetworkError("timeout"))
68
69 result = await client.get_liked_albums()
70
71 # Should fall back to the minimal album objects from likes
72 assert len(result) == 2
73 assert {a.id for a in result} == {10, 20}
74
75
76# -- get_tracks: retry on NetworkError -------------------------------------------
77
78
79async def test_get_tracks_retry_on_network_error_then_success() -> None:
80 """First call fails with NetworkError; retry succeeds."""
81 client, underlying = _make_client()
82
83 track = type("Track", (), {"id": 400, "title": "Test Track"})()
84 underlying.tracks = mock.AsyncMock(side_effect=[NetworkError("timeout"), [track]])
85
86 result = await client.get_tracks(["400"])
87
88 assert result == [track]
89 assert underlying.tracks.await_count == 2
90
91
92async def test_get_tracks_retry_on_network_error_both_fail() -> None:
93 """Both attempts fail with NetworkError â ResourceTemporarilyUnavailable."""
94 client, underlying = _make_client()
95
96 underlying.tracks = mock.AsyncMock(
97 side_effect=[NetworkError("timeout"), NetworkError("timeout again")]
98 )
99
100 with pytest.raises(ResourceTemporarilyUnavailable):
101 await client.get_tracks(["400"])
102
103 assert underlying.tracks.await_count == 2
104