/
/
/
1"""Tests for the Cover Art Archive metadata provider."""
2
3from unittest.mock import AsyncMock, MagicMock
4
5import pytest
6from aiohttp import ClientError
7from music_assistant_models.errors import ResourceTemporarilyUnavailable
8
9from music_assistant.providers.coverartarchive import (
10 SUPPORTED_FEATURES,
11 CoverArtArchiveMetadataProvider,
12)
13from tests.common import use_real_create_task
14
15
16@pytest.fixture
17def provider() -> CoverArtArchiveMetadataProvider:
18 """Return a provider with mocked dependencies and a cold cache."""
19 mass = AsyncMock()
20 mass.http_session = MagicMock()
21 # force a cache miss so the wrapped fetch always runs
22 mass.cache.get_with_freshness = AsyncMock(return_value=(None, False, False))
23 use_real_create_task(mass)
24 manifest = MagicMock()
25 manifest.domain = "coverartarchive"
26 config = MagicMock()
27 config.get_value.return_value = "GLOBAL"
28 return CoverArtArchiveMetadataProvider(mass, manifest, config, SUPPORTED_FEATURES)
29
30
31def _response_cm(response: MagicMock) -> MagicMock:
32 """Build a fake async context manager mimicking aiohttp's session.head()."""
33 cm = MagicMock()
34 cm.__aenter__ = AsyncMock(return_value=response)
35 cm.__aexit__ = AsyncMock(return_value=False)
36 return cm
37
38
39async def test_get_cover_art_url_returns_url_on_success(
40 provider: CoverArtArchiveMetadataProvider,
41) -> None:
42 """A 200 response returns the resolved cover art URL."""
43 response = MagicMock()
44 response.status = 200
45 response.url = "https://coverartarchive.org/release-group/mbid/front-1200"
46 provider.mass.http_session.head = MagicMock( # type: ignore[method-assign]
47 return_value=_response_cm(response)
48 )
49
50 result = await provider._get_cover_art_url("mbid")
51 assert result == "https://coverartarchive.org/release-group/mbid/front-1200"
52
53
54async def test_get_cover_art_url_returns_none_when_missing(
55 provider: CoverArtArchiveMetadataProvider,
56) -> None:
57 """A 404 for every size means there is genuinely no cover art, so None is returned."""
58 response = MagicMock()
59 response.status = 404
60 provider.mass.http_session.head = MagicMock( # type: ignore[method-assign]
61 return_value=_response_cm(response)
62 )
63
64 assert await provider._get_cover_art_url("mbid") is None
65
66
67async def test_get_cover_art_url_propagates_transient_error(
68 provider: CoverArtArchiveMetadataProvider,
69) -> None:
70 """A 5xx failure surfaces as ResourceTemporarilyUnavailable, not cached as 'no cover art'."""
71 response = MagicMock()
72 response.status = 503
73 response.raise_for_status = MagicMock(side_effect=ClientError("service unavailable"))
74 provider.mass.http_session.head = MagicMock( # type: ignore[method-assign]
75 return_value=_response_cm(response)
76 )
77
78 with pytest.raises(ResourceTemporarilyUnavailable):
79 await provider._get_cover_art_url("mbid")
80