Files
musicseerr/backend/tests/services/test_audiodb_byte_caching_integration.py
T
2026-04-03 15:53:00 +01:00

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)