"""Tests für den MusicBrainz-Barcode-Lookup.""" from __future__ import annotations from unittest.mock import MagicMock, patch import pytest from musiksammlung.musicbrainz import _parse_release, lookup_by_barcode # --------------------------------------------------------------------------- # Hilfsfunktionen # --------------------------------------------------------------------------- def _mock_response(data: dict) -> MagicMock: """Erstellt ein Mock-httpx.Response-Objekt.""" r = MagicMock() r.json.return_value = data r.raise_for_status.return_value = None return r _BARCODE_RESPONSE = { "releases": [{"id": "abc-123"}], } _RELEASE_RESPONSE = { "title": "Abbey Road", "date": "1969-09-26", "artist-credit": [{"artist": {"name": "The Beatles"}}], "media": [ { "position": 1, "tracks": [ { "position": 1, "title": "Come Together", "artist-credit": [{"artist": {"name": "The Beatles"}}], }, { "position": 2, "title": "Something", "artist-credit": [{"artist": {"name": "The Beatles"}}], }, ], } ], } # --------------------------------------------------------------------------- # _parse_release # --------------------------------------------------------------------------- class TestParseRelease: def test_basic_fields(self) -> None: album = _parse_release(_RELEASE_RESPONSE) assert album.artist == "The Beatles" assert album.album == "Abbey Road" assert album.year == 1969 def test_tracks(self) -> None: album = _parse_release(_RELEASE_RESPONSE) assert len(album.discs) == 1 disc = album.discs[0] assert disc.disc_number == 1 assert len(disc.tracks) == 2 assert disc.tracks[0].title == "Come Together" assert disc.tracks[1].title == "Something" def test_track_artist_same_as_album_artist_is_none(self) -> None: album = _parse_release(_RELEASE_RESPONSE) # Track-Künstler = Album-Künstler → track.artist muss None sein assert album.discs[0].tracks[0].artist is None def test_track_artist_different_is_set(self) -> None: data = { "title": "Compilation", "date": "2000", "artist-credit": [{"artist": {"name": "Various Artists"}}], "media": [ { "position": 1, "tracks": [ { "position": 1, "title": "Song A", "artist-credit": [{"artist": {"name": "Artist A"}}], } ], } ], } album = _parse_release(data) assert album.discs[0].tracks[0].artist == "Artist A" def test_year_from_full_date(self) -> None: data = {**_RELEASE_RESPONSE, "date": "1969-09-26"} album = _parse_release(data) assert album.year == 1969 def test_year_from_year_only(self) -> None: data = {**_RELEASE_RESPONSE, "date": "1969"} album = _parse_release(data) assert album.year == 1969 def test_year_none_when_missing(self) -> None: data = {**_RELEASE_RESPONSE, "date": ""} album = _parse_release(data) assert album.year is None def test_year_none_when_invalid(self) -> None: data = {**_RELEASE_RESPONSE, "date": "unbekannt"} album = _parse_release(data) assert album.year is None def test_multi_disc(self) -> None: data = { "title": "The Wall", "date": "1979", "artist-credit": [{"artist": {"name": "Pink Floyd"}}], "media": [ { "position": 1, "tracks": [{"position": 1, "title": "In the Flesh?", "artist-credit": []}], }, { "position": 2, "tracks": [{"position": 1, "title": "Hey You", "artist-credit": []}], }, ], } album = _parse_release(data) assert len(album.discs) == 2 assert album.discs[0].disc_number == 1 assert album.discs[1].disc_number == 2 def test_raises_when_no_media(self) -> None: data = {**_RELEASE_RESPONSE, "media": []} with pytest.raises(ValueError, match="keine Medien"): _parse_release(data) def test_no_artist_credit_gives_empty_string(self) -> None: data = {**_RELEASE_RESPONSE, "artist-credit": []} album = _parse_release(data) assert album.artist == "" # --------------------------------------------------------------------------- # lookup_by_barcode # --------------------------------------------------------------------------- class TestLookupByBarcode: def test_successful_lookup(self) -> None: responses = [_mock_response(_BARCODE_RESPONSE), _mock_response(_RELEASE_RESPONSE)] with ( patch("musiksammlung.musicbrainz.httpx.get", side_effect=responses), patch("musiksammlung.musicbrainz.time.sleep"), ): album, mbid = lookup_by_barcode("0602557360561") assert album.artist == "The Beatles" assert album.album == "Abbey Road" assert album.year == 1969 assert mbid == "abc-123" def test_raises_when_no_releases(self) -> None: empty = _mock_response({"releases": []}) with ( patch("musiksammlung.musicbrainz.httpx.get", return_value=empty), patch("musiksammlung.musicbrainz.time.sleep"), pytest.raises(ValueError, match="Kein MusicBrainz-Eintrag"), ): lookup_by_barcode("0000000000000") # raises before returning tuple def test_uses_first_release(self) -> None: barcode_data = {"releases": [{"id": "first-id"}, {"id": "second-id"}]} responses = [_mock_response(barcode_data), _mock_response(_RELEASE_RESPONSE)] with ( patch("musiksammlung.musicbrainz.httpx.get", side_effect=responses) as mock_get, patch("musiksammlung.musicbrainz.time.sleep"), ): _album, mbid = lookup_by_barcode("1234567890123") # Zweiter Request muss die MBID des ersten Treffers verwenden second_call_url = mock_get.call_args_list[1][0][0] assert "first-id" in second_call_url assert mbid == "first-id" def test_rate_limit_sleep_is_called(self) -> None: responses = [_mock_response(_BARCODE_RESPONSE), _mock_response(_RELEASE_RESPONSE)] with ( patch("musiksammlung.musicbrainz.httpx.get", side_effect=responses), patch("musiksammlung.musicbrainz.time.sleep") as mock_sleep, ): _album, _mbid = lookup_by_barcode("0602557360561") mock_sleep.assert_called_once() assert mock_sleep.call_args[0][0] >= 1.0 def test_http_error_propagates(self) -> None: import httpx with ( patch("musiksammlung.musicbrainz.httpx.get", side_effect=httpx.HTTPError("timeout")), pytest.raises(httpx.HTTPError), ): lookup_by_barcode("0000000000000") # raises before returning tuple