music-assistant-server

7.3 KBPY
test_api_client.py
7.3 KB218 lines • python
1"""Test Tidal API Client."""
2
3from typing import Any
4from unittest.mock import AsyncMock, MagicMock, Mock, patch
5
6import pytest
7from aiohttp import ClientResponse
8from music_assistant_models.errors import (
9    LoginFailed,
10    MediaNotFoundError,
11    RetriesExhausted,
12)
13
14from music_assistant.providers.tidal.api_client import TidalAPIClient
15
16
17@pytest.fixture
18def provider_mock() -> Mock:
19    """Return a mock provider."""
20    provider = Mock()
21    provider.auth = AsyncMock()
22    provider.auth.ensure_valid_token.return_value = True
23    provider.auth.access_token = "token"
24    provider.auth.session_id = "session"
25    provider.auth.country_code = "US"
26    provider.mass = Mock()
27    provider.mass.http_session = AsyncMock()
28    provider.mass.metadata.locale = "en_US"
29    provider.logger = Mock()
30    return provider
31
32
33@pytest.fixture
34def api_client(provider_mock: Mock) -> TidalAPIClient:
35    """Return a TidalAPIClient instance."""
36    return TidalAPIClient(provider_mock)
37
38
39async def test_get_success(api_client: TidalAPIClient, provider_mock: Mock) -> None:
40    """Test successful GET request."""
41    response = AsyncMock(spec=ClientResponse)
42    response.status = 200
43    response.json.return_value = {"data": "test"}
44
45    # Create a mock that acts as an async context manager
46    request_ctx = AsyncMock()
47    request_ctx.__aenter__.return_value = response
48
49    # The request method itself should be a MagicMock (not AsyncMock)
50    # that returns the context manager
51    provider_mock.mass.http_session.request = MagicMock(return_value=request_ctx)
52
53    result = await api_client.get("test/endpoint")
54    assert result == {"data": "test"}
55
56
57async def test_get_401_error(api_client: TidalAPIClient, provider_mock: Mock) -> None:
58    """Test GET request with 401 error."""
59    response = AsyncMock(spec=ClientResponse)
60    response.status = 401
61
62    request_ctx = AsyncMock()
63    request_ctx.__aenter__.return_value = response
64    provider_mock.mass.http_session.request = MagicMock(return_value=request_ctx)
65
66    with pytest.raises(LoginFailed):
67        await api_client.get("test/endpoint")
68
69
70async def test_get_404_error(api_client: TidalAPIClient, provider_mock: Mock) -> None:
71    """Test GET request with 404 error."""
72    response = AsyncMock(spec=ClientResponse)
73    response.status = 404
74    response.url = "http://test/endpoint"
75
76    request_ctx = AsyncMock()
77    request_ctx.__aenter__.return_value = response
78    provider_mock.mass.http_session.request = MagicMock(return_value=request_ctx)
79
80    with pytest.raises(MediaNotFoundError):
81        await api_client.get("test/endpoint")
82
83
84async def test_get_429_error(api_client: TidalAPIClient, provider_mock: Mock) -> None:
85    """Test GET request with 429 error."""
86    with patch("asyncio.sleep"):
87        response = AsyncMock(spec=ClientResponse)
88    response.status = 429
89    response.headers = {"Retry-After": "10"}
90
91    request_ctx = AsyncMock()
92    request_ctx.__aenter__.return_value = response
93    provider_mock.mass.http_session.request = MagicMock(return_value=request_ctx)
94
95    with pytest.raises(RetriesExhausted):
96        await api_client.get("test/endpoint")
97
98
99async def test_post_success(api_client: TidalAPIClient, provider_mock: Mock) -> None:
100    """Test successful POST request."""
101    response = AsyncMock(spec=ClientResponse)
102    response.status = 200
103    response.json.return_value = {"success": True}
104
105    request_ctx = AsyncMock()
106    request_ctx.__aenter__.return_value = response
107    provider_mock.mass.http_session.request = MagicMock(return_value=request_ctx)
108
109    result = await api_client.post("test/endpoint", data={"key": "value"})
110    assert result == {"success": True}
111
112
113async def test_paginate(api_client: TidalAPIClient, provider_mock: Mock) -> None:
114    """Test pagination."""
115    # Mock first page response
116    response1 = AsyncMock(spec=ClientResponse)
117    response1.status = 200
118    response1.json.return_value = {"items": [{"id": 1}, {"id": 2}], "totalNumberOfItems": 4}
119
120    # Mock second page response
121    response2 = AsyncMock(spec=ClientResponse)
122    response2.status = 200
123    response2.json.return_value = {"items": [{"id": 3}, {"id": 4}], "totalNumberOfItems": 4}
124
125    # Mock empty response to stop iteration
126    response3 = AsyncMock(spec=ClientResponse)
127    response3.status = 200
128    response3.json.return_value = {"items": []}
129
130    ctx1 = AsyncMock()
131    ctx1.__aenter__.return_value = response1
132
133    ctx2 = AsyncMock()
134    ctx2.__aenter__.return_value = response2
135
136    ctx3 = AsyncMock()
137    ctx3.__aenter__.return_value = response3
138
139    provider_mock.mass.http_session.request = MagicMock(side_effect=[ctx1, ctx2, ctx3])
140
141    items: list[dict[str, Any]] = []
142    async for item in api_client.paginate("test/endpoint", limit=2):
143        items.append(item)
144
145    assert len(items) == 4
146    assert items[0]["id"] == 1
147    assert items[3]["id"] == 4
148
149
150async def test_delete_success(api_client: TidalAPIClient, provider_mock: Mock) -> None:
151    """Test successful DELETE request."""
152    response = AsyncMock(spec=ClientResponse)
153    response.status = 204
154
155    request_ctx = AsyncMock()
156    request_ctx.__aenter__.return_value = response
157    provider_mock.mass.http_session.request = MagicMock(return_value=request_ctx)
158
159    await api_client.delete("test/endpoint/123")
160
161    # Verify DELETE was called
162    provider_mock.mass.http_session.request.assert_called_once()
163    call_args = provider_mock.mass.http_session.request.call_args
164    assert call_args[0][0] == "DELETE"
165
166
167async def test_delete_with_headers(api_client: TidalAPIClient, provider_mock: Mock) -> None:
168    """Test DELETE request with custom headers."""
169    response = AsyncMock(spec=ClientResponse)
170    response.status = 204
171
172    request_ctx = AsyncMock()
173    request_ctx.__aenter__.return_value = response
174    provider_mock.mass.http_session.request = MagicMock(return_value=request_ctx)
175
176    await api_client.delete("test/endpoint/123", headers={"If-Match": "etag123"})
177
178    # Verify headers were passed
179    call_args = provider_mock.mass.http_session.request.call_args
180    assert "If-Match" in call_args[1]["headers"]
181    assert call_args[1]["headers"]["If-Match"] == "etag123"
182
183
184async def test_put_success(api_client: TidalAPIClient, provider_mock: Mock) -> None:
185    """Test successful PUT request."""
186    response = AsyncMock(spec=ClientResponse)
187    response.status = 200
188    response.json.return_value = {"updated": True}
189
190    request_ctx = AsyncMock()
191    request_ctx.__aenter__.return_value = response
192    provider_mock.mass.http_session.request = MagicMock(return_value=request_ctx)
193
194    result = await api_client.put("test/endpoint", data={"key": "value"})
195    assert result == {"updated": True}
196
197    # Verify PUT was called
198    call_args = provider_mock.mass.http_session.request.call_args
199    assert call_args[0][0] == "PUT"
200
201
202async def test_put_with_form_data(api_client: TidalAPIClient, provider_mock: Mock) -> None:
203    """Test PUT request with form data."""
204    response = AsyncMock(spec=ClientResponse)
205    response.status = 200
206    response.json.return_value = {"success": True}
207
208    request_ctx = AsyncMock()
209    request_ctx.__aenter__.return_value = response
210    provider_mock.mass.http_session.request = MagicMock(return_value=request_ctx)
211
212    result = await api_client.put("test/endpoint", data={"key": "value"}, as_form=True)
213    assert result == {"success": True}
214
215    # Verify form data was used
216    call_args = provider_mock.mass.http_session.request.call_args
217    assert "data" in call_args[1]
218