/
/
/
1"""Tests for the LRCLIB metadata provider."""
2
3from unittest.mock import AsyncMock, MagicMock
4
5import pytest
6from aiohttp import ClientError
7from music_assistant_models.errors import MusicAssistantError
8
9from music_assistant.helpers.throttle_retry import ThrottlerManager
10from music_assistant.providers.lrclib import SUPPORTED_FEATURES, LrclibProvider
11from tests.common import use_real_create_task
12
13
14@pytest.fixture
15def provider() -> LrclibProvider:
16 """Return an LrclibProvider with mocked dependencies and a cold cache."""
17 mass = AsyncMock()
18 mass.http_session = MagicMock()
19 # force a cache miss so the wrapped fetch always runs
20 mass.cache.get_with_freshness = AsyncMock(return_value=(None, False, False))
21 use_real_create_task(mass)
22 manifest = MagicMock()
23 manifest.domain = "lrclib"
24 config = MagicMock()
25 config.get_value.return_value = "GLOBAL"
26 prov = LrclibProvider(mass, manifest, config, SUPPORTED_FEATURES)
27 prov.api_url = "https://lrclib.net/api"
28 # retry_attempts=1 keeps the transient-error test fast (no backoff sleeps)
29 prov.throttler = ThrottlerManager(rate_limit=1, period=1, retry_attempts=1)
30 return prov
31
32
33def _response_cm(*, response: MagicMock | None = None, exc: Exception | None = None) -> MagicMock:
34 """Build a fake async context manager mimicking aiohttp's session.get()."""
35 cm = MagicMock()
36 cm.__aenter__ = AsyncMock(return_value=response, side_effect=exc)
37 cm.__aexit__ = AsyncMock(return_value=False)
38 return cm
39
40
41async def test_get_data_returns_none_when_not_found(provider: LrclibProvider) -> None:
42 """A 404 means there are genuinely no lyrics, so None is returned (and cached)."""
43 response = MagicMock()
44 response.status = 404
45 response.raise_for_status = MagicMock()
46 provider.mass.http_session.get = MagicMock( # type: ignore[method-assign]
47 return_value=_response_cm(response=response)
48 )
49
50 assert await provider._get_data(track_name="Song", artist_name="Artist") is None
51
52
53async def test_get_data_propagates_transient_error(provider: LrclibProvider) -> None:
54 """A network failure surfaces as a MusicAssistantError instead of being cached as 'no lyrics'."""
55 provider.mass.http_session.get = MagicMock( # type: ignore[method-assign]
56 return_value=_response_cm(exc=ClientError("network down"))
57 )
58
59 with pytest.raises(MusicAssistantError):
60 await provider._get_data(track_name="Song", artist_name="Artist")
61