170 lines
5.6 KiB
Python
170 lines
5.6 KiB
Python
import asyncio
|
|
import time
|
|
from pathlib import Path
|
|
from unittest.mock import AsyncMock, MagicMock, call
|
|
|
|
import pytest
|
|
|
|
from repositories.audiodb_models import AudioDBAlbumImages, AudioDBArtistImages
|
|
from repositories.coverart_album import AlbumCoverFetcher
|
|
from repositories.coverart_artist import ArtistImageFetcher
|
|
|
|
AUDIODB_CDN_URL = "https://r2.theaudiodb.com/test.jpg"
|
|
AUDIODB_ARTIST_CDN_URL = "https://r2.theaudiodb.com/artist.jpg"
|
|
CAA_URL_PREFIX = "https://coverartarchive.org"
|
|
|
|
|
|
def _response(
|
|
status_code: int = 200,
|
|
content_type: str = "image/jpeg",
|
|
content: bytes = b"img",
|
|
) -> MagicMock:
|
|
response = MagicMock()
|
|
response.status_code = status_code
|
|
response.headers = {"content-type": content_type}
|
|
response.content = content
|
|
return response
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_album_byte_path_cache_hit_downloads_and_caches():
|
|
"""8.5.a — Full chain: AudioDB cache hit → CDN download → disk write.
|
|
CoverArtArchive is NOT called (short-circuited)."""
|
|
audiodb_response = _response(content=b"fake-jpeg-bytes")
|
|
http_get = AsyncMock(return_value=audiodb_response)
|
|
write_cache = AsyncMock()
|
|
audiodb_service = MagicMock()
|
|
audiodb_service.fetch_and_cache_album_images = AsyncMock(
|
|
return_value=AudioDBAlbumImages(
|
|
album_thumb_url=AUDIODB_CDN_URL,
|
|
is_negative=False,
|
|
cached_at=time.time(),
|
|
),
|
|
)
|
|
fetcher = AlbumCoverFetcher(
|
|
http_get_fn=http_get,
|
|
write_cache_fn=write_cache,
|
|
audiodb_service=audiodb_service,
|
|
)
|
|
|
|
result = await fetcher.fetch_release_group_cover(
|
|
"release-group-id", "500", Path("/tmp/album.bin"),
|
|
)
|
|
|
|
assert result == (b"fake-jpeg-bytes", "image/jpeg", "audiodb")
|
|
http_get.assert_awaited_once()
|
|
assert http_get.call_args[0][0] == AUDIODB_CDN_URL
|
|
await asyncio.sleep(0)
|
|
assert write_cache.await_count == 1
|
|
write_meta = write_cache.call_args[0][3]
|
|
assert write_meta == {"source": "audiodb"}
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_artist_byte_path_cache_hit_downloads_and_caches():
|
|
"""8.5.b — Full chain: AudioDB cache hit → CDN download → disk write.
|
|
Wikidata is NOT called (short-circuited)."""
|
|
http_get = AsyncMock(
|
|
return_value=_response(content=b"fake-artist-bytes"),
|
|
)
|
|
write_cache = AsyncMock()
|
|
audiodb_service = MagicMock()
|
|
audiodb_service.fetch_and_cache_artist_images = AsyncMock(
|
|
return_value=AudioDBArtistImages(
|
|
thumb_url=AUDIODB_ARTIST_CDN_URL,
|
|
is_negative=False,
|
|
cached_at=time.time(),
|
|
),
|
|
)
|
|
fetcher = ArtistImageFetcher(
|
|
http_get_fn=http_get,
|
|
write_cache_fn=write_cache,
|
|
cache=MagicMock(),
|
|
audiodb_service=audiodb_service,
|
|
)
|
|
|
|
result = await fetcher.fetch_artist_image(
|
|
"artist-id-00", 500, Path("/tmp/artist.bin"),
|
|
)
|
|
|
|
assert result == (b"fake-artist-bytes", "image/jpeg", "audiodb")
|
|
http_get.assert_awaited_once()
|
|
assert http_get.call_args[0][0] == AUDIODB_ARTIST_CDN_URL
|
|
await asyncio.sleep(0)
|
|
assert write_cache.await_count == 1
|
|
write_meta = write_cache.call_args[0][3]
|
|
assert write_meta == {"source": "audiodb"}
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_album_byte_path_cdn_404_falls_through_to_caa():
|
|
"""8.5.c — CDN returns 404 → AudioDB falls through → CoverArtArchive is called."""
|
|
cdn_404 = _response(status_code=404)
|
|
caa_ok = _response(content=b"caa-bytes", content_type="image/png")
|
|
|
|
async def _route_http_get(url, *args, **kwargs):
|
|
if "theaudiodb.com" in url:
|
|
return cdn_404
|
|
return caa_ok
|
|
|
|
http_get = AsyncMock(side_effect=_route_http_get)
|
|
write_cache = AsyncMock()
|
|
audiodb_service = MagicMock()
|
|
audiodb_service.fetch_and_cache_album_images = AsyncMock(
|
|
return_value=AudioDBAlbumImages(
|
|
album_thumb_url=AUDIODB_CDN_URL,
|
|
is_negative=False,
|
|
cached_at=time.time(),
|
|
),
|
|
)
|
|
fetcher = AlbumCoverFetcher(
|
|
http_get_fn=http_get,
|
|
write_cache_fn=write_cache,
|
|
audiodb_service=audiodb_service,
|
|
)
|
|
|
|
result = await fetcher.fetch_release_group_cover(
|
|
"release-group-id", "500", Path("/tmp/album.bin"),
|
|
)
|
|
|
|
assert result == (b"caa-bytes", "image/png", "cover-art-archive")
|
|
urls_called = [c[0][0] for c in http_get.call_args_list]
|
|
assert AUDIODB_CDN_URL in urls_called
|
|
assert any(CAA_URL_PREFIX in u for u in urls_called)
|
|
await asyncio.sleep(0)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_album_byte_path_cdn_invalid_content_type_falls_through():
|
|
"""8.5.d — CDN returns text/html → AudioDB falls through → CoverArtArchive called."""
|
|
cdn_html = _response(status_code=200, content_type="text/html")
|
|
caa_ok = _response(content=b"caa-bytes", content_type="image/jpeg")
|
|
|
|
async def _route_http_get(url, *args, **kwargs):
|
|
if "theaudiodb.com" in url:
|
|
return cdn_html
|
|
return caa_ok
|
|
|
|
http_get = AsyncMock(side_effect=_route_http_get)
|
|
write_cache = AsyncMock()
|
|
audiodb_service = MagicMock()
|
|
audiodb_service.fetch_and_cache_album_images = AsyncMock(
|
|
return_value=AudioDBAlbumImages(
|
|
album_thumb_url=AUDIODB_CDN_URL,
|
|
is_negative=False,
|
|
cached_at=time.time(),
|
|
),
|
|
)
|
|
fetcher = AlbumCoverFetcher(
|
|
http_get_fn=http_get,
|
|
write_cache_fn=write_cache,
|
|
audiodb_service=audiodb_service,
|
|
)
|
|
|
|
result = await fetcher.fetch_release_group_cover(
|
|
"release-group-id", "500", Path("/tmp/album.bin"),
|
|
)
|
|
|
|
assert result == (b"caa-bytes", "image/jpeg", "cover-art-archive")
|
|
await asyncio.sleep(0)
|