"""Unit tests for YouTubeMusicSource + registry (no network, injected libs).""" from pathlib import Path from typing import Any import pytest from app.core.config import Settings from app.domain.sources import KIND_FETCH from app.infrastructure.sources.registry import build_source_registry from app.infrastructure.sources.youtube import YouTubeMusicSource pytestmark = pytest.mark.asyncio def _song_row(**overrides: Any) -> dict[str, Any]: row: dict[str, Any] = { "videoId": "abc123", "title": "Bohemian Rhapsody", "artists": [{"name": "Queen", "id": "a1"}], "album": {"name": "A Night at the Opera", "id": "al1"}, "duration_seconds": 354, "thumbnails": [ {"url": "http://img/small.jpg", "width": 60, "height": 60}, {"url": "http://img/large.jpg", "width": 240, "height": 240}, ], } row.update(overrides) return row def _settings(**overrides: object) -> Settings: return Settings(**overrides) # type: ignore[arg-type] async def test_search_maps_ytmusic_rows() -> None: source = YouTubeMusicSource(search_fn=lambda q, limit: [_song_row()]) [result] = await source.search("queen", limit=10) assert result.source == "youtube" assert result.source_id == "abc123" assert result.title == "Bohemian Rhapsody" assert result.artist == "Queen" assert result.album == "A Night at the Opera" assert result.duration_seconds == 354 assert result.thumbnail_url == "http://img/large.jpg" # last (largest) async def test_search_joins_multiple_artists_and_tolerates_missing_fields() -> None: row = _song_row( artists=[{"name": "Queen"}, {"name": "David Bowie"}], album=None, thumbnails=[], duration_seconds=None, ) source = YouTubeMusicSource(search_fn=lambda q, limit: [row]) [result] = await source.search("under pressure", limit=10) assert result.artist == "Queen, David Bowie" assert result.album is None assert result.thumbnail_url is None assert result.duration_seconds is None async def test_search_drops_rows_without_video_id() -> None: rows = [_song_row(), _song_row(videoId=None), _song_row(videoId="xyz")] source = YouTubeMusicSource(search_fn=lambda q, limit: rows) results = await source.search("q", limit=10) assert [r.source_id for r in results] == ["abc123", "xyz"] async def test_search_empty_query_short_circuits() -> None: called = False def _search(q: str, limit: int) -> list[dict[str, Any]]: nonlocal called called = True return [] source = YouTubeMusicSource(search_fn=_search) assert await source.search(" ", limit=10) == [] assert called is False async def test_search_degrades_to_empty_on_error() -> None: def _boom(q: str, limit: int) -> list[dict[str, Any]]: raise RuntimeError("service down") source = YouTubeMusicSource(search_fn=_boom) assert await source.search("q", limit=10) == [] async def test_fetch_maps_download_result(tmp_path: Path) -> None: audio = tmp_path / "abc123.m4a" audio.write_bytes(b"opus-bytes" * 10) def _download(video_id: str, tmp_dir: Path, hook: Any, cookies: Path | None) -> dict[str, Any]: return { "filepath": audio, "file_format": "m4a", "bitrate": 160, "title": "Bohemian Rhapsody", } source = YouTubeMusicSource(download_fn=_download) result = await source.fetch("abc123") assert result.source_id == "abc123" assert result.path == audio assert result.file_format == "m4a" assert result.file_size == len(b"opus-bytes" * 10) assert result.bitrate == 160 assert result.suggested_title == "Bohemian Rhapsody" async def test_info_and_availability_with_injected_fn() -> None: source = YouTubeMusicSource(search_fn=lambda q, limit: []) info = source.info() assert info.name == "youtube" assert info.kind == KIND_FETCH assert info.available is True # injected fn → treated as available async def test_registry_registers_youtube_when_enabled() -> None: registry = build_source_registry(_settings(youtube_enabled=True)) names = {info.name for info in registry.infos()} assert "youtube" in names # youtube is searchable + fetchable, not indexable assert registry.searchable("youtube").name == "youtube" assert registry.fetchable("youtube").name == "youtube" async def test_registry_omits_youtube_when_disabled() -> None: registry = build_source_registry(_settings(youtube_enabled=False)) names = {info.name for info in registry.infos()} assert "youtube" not in names