/
/
/
1"""Unit tests for the KION Music API client."""
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.kion_music.api_client import KION_BASE_URL, KionMusicClient
12
13
14@pytest.fixture
15def client() -> KionMusicClient:
16 """Return a KionMusicClient with a fake token."""
17 return KionMusicClient("fake_token")
18
19
20async def test_connect_sets_base_url(client: KionMusicClient) -> None:
21 """Verify connect() passes KION_BASE_URL to ClientAsync."""
22 with mock.patch("music_assistant.providers.kion_music.api_client.ClientAsync") as mock_cls:
23 mock_instance = mock.AsyncMock()
24 mock_instance.me = type("Me", (), {"account": type("Account", (), {"uid": 42})()})()
25 mock_instance.init = mock.AsyncMock(return_value=mock_instance)
26 mock_cls.return_value = mock_instance
27
28 result = await client.connect()
29
30 assert result is True
31 mock_cls.assert_called_once_with("fake_token", base_url=KION_BASE_URL)
32
33
34async def test_get_liked_albums_batching(client: KionMusicClient) -> None:
35 """Test that liked albums are fetched in batches of 50."""
36 mock_client = mock.AsyncMock()
37 client._client = mock_client
38 client._user_id = 1
39
40 # Create 60 likes so we get 2 batches
41 likes = []
42 for i in range(60):
43 like = type("Like", (), {"album": type("Album", (), {"id": i + 1})()})()
44 likes.append(like)
45
46 mock_client.users_likes_albums = mock.AsyncMock(return_value=likes)
47
48 batch1 = [type("Album", (), {"id": i + 1})() for i in range(50)]
49 batch2 = [type("Album", (), {"id": i + 51})() for i in range(10)]
50 mock_client.albums = mock.AsyncMock(side_effect=[batch1, batch2])
51
52 result = await client.get_liked_albums()
53
54 assert len(result) == 60
55 assert mock_client.albums.call_count == 2
56
57
58async def test_get_liked_albums_batch_fallback_on_network_error(
59 client: KionMusicClient,
60) -> None:
61 """Test fallback to minimal data when batch fetch fails."""
62 mock_client = mock.AsyncMock()
63 client._client = mock_client
64 client._user_id = 1
65
66 album_obj = type("Album", (), {"id": 1})()
67 likes = [type("Like", (), {"album": album_obj})()]
68
69 mock_client.users_likes_albums = mock.AsyncMock(return_value=likes)
70 mock_client.albums = mock.AsyncMock(side_effect=NetworkError("timeout"))
71
72 result = await client.get_liked_albums()
73
74 assert len(result) == 1
75 assert result[0].id == 1
76
77
78async def test_get_tracks_retry_on_network_error_then_success(
79 client: KionMusicClient,
80) -> None:
81 """Test that get_tracks retries once on NetworkError and succeeds."""
82 mock_client = mock.AsyncMock()
83 client._client = mock_client
84 client._user_id = 1
85
86 track = type("Track", (), {"id": 1})()
87 mock_client.tracks = mock.AsyncMock(side_effect=[NetworkError("timeout"), [track]])
88
89 result = await client.get_tracks(["1"])
90
91 assert len(result) == 1
92 assert mock_client.tracks.call_count == 2
93
94
95async def test_get_tracks_retry_on_network_error_both_fail(
96 client: KionMusicClient,
97) -> None:
98 """Test that get_tracks raises ResourceTemporarilyUnavailable when retry fails."""
99 mock_client = mock.AsyncMock()
100 client._client = mock_client
101 client._user_id = 1
102
103 mock_client.tracks = mock.AsyncMock(side_effect=NetworkError("timeout"))
104
105 with pytest.raises(ResourceTemporarilyUnavailable):
106 await client.get_tracks(["1"])
107
108 assert mock_client.tracks.call_count == 2
109