from unittest.mock import MagicMock, patch import requests from lyra_worker.browser import ArtistHit from lyra_worker.similarity._lastfm import LastfmSource from lyra_worker.similarity.base import SimilarArtist def _resp(payload, status=200): r = MagicMock() r.status_code = status r.json.return_value = payload return r class _FakeBrowser: """Records search_artist calls; resolves via a name -> [ArtistHit] map.""" def __init__(self, hits=None): self._hits = dict(hits or {}) self.calls = [] def search_artist(self, name): self.calls.append(name) return list(self._hits.get(name, [])) def test_similar_artists_parses_and_uses_supplied_mbid(): payload = {"similarartists": {"artist": [ {"name": "Cand One", "mbid": "c1", "match": "0.9"}, ]}} browser = _FakeBrowser() with patch("lyra_worker.similarity._lastfm.requests.get", return_value=_resp(payload)): out = LastfmSource(api_key="k", browser=browser).similar_artists("seed") assert out == [SimilarArtist("c1", "Cand One", 0.9)] assert browser.calls == [] # mbid supplied, no resolution needed def test_similar_artists_resolves_missing_mbid_and_caches(): payload = {"similarartists": {"artist": [ {"name": "Cand Two", "mbid": "", "match": "0.5"}, {"name": "Cand Two", "mbid": "", "match": "0.4"}, ]}} browser = _FakeBrowser(hits={"Cand Two": [ArtistHit(mbid="c2", name="Cand Two")]}) with patch("lyra_worker.similarity._lastfm.requests.get", return_value=_resp(payload)): out = LastfmSource(api_key="k", browser=browser).similar_artists("seed") assert out == [ SimilarArtist("c2", "Cand Two", 0.5), SimilarArtist("c2", "Cand Two", 0.4), ] assert browser.calls == ["Cand Two"] # cached after first lookup def test_similar_artists_skips_unresolvable_row(): payload = {"similarartists": {"artist": [ {"name": "Unknown Artist", "mbid": "", "match": "0.5"}, {"name": "Cand One", "mbid": "c1", "match": "0.9"}, ]}} browser = _FakeBrowser(hits={}) # no hits for "Unknown Artist" with patch("lyra_worker.similarity._lastfm.requests.get", return_value=_resp(payload)): out = LastfmSource(api_key="k", browser=browser).similar_artists("seed") assert out == [SimilarArtist("c1", "Cand One", 0.9)] def test_similar_artists_skips_self(): payload = {"similarartists": {"artist": [ {"name": "Seed", "mbid": "seed", "match": "1.0"}, {"name": "Cand One", "mbid": "c1", "match": "0.9"}, ]}} browser = _FakeBrowser() with patch("lyra_worker.similarity._lastfm.requests.get", return_value=_resp(payload)): out = LastfmSource(api_key="k", browser=browser).similar_artists("seed") assert out == [SimilarArtist("c1", "Cand One", 0.9)] def test_similar_artists_coerces_single_object(): payload = {"similarartists": {"artist": {"name": "Cand One", "mbid": "c1", "match": "0.9"}}} browser = _FakeBrowser() with patch("lyra_worker.similarity._lastfm.requests.get", return_value=_resp(payload)): out = LastfmSource(api_key="k", browser=browser).similar_artists("seed") assert out == [SimilarArtist("c1", "Cand One", 0.9)] def test_similar_artists_error_envelope_returns_empty_list(): payload = {"error": 6, "message": "no artist"} browser = _FakeBrowser() with patch("lyra_worker.similarity._lastfm.requests.get", return_value=_resp(payload)): out = LastfmSource(api_key="k", browser=browser).similar_artists("seed") assert out == [] def test_health_true_on_normal_response(): payload = {"similarartists": {"artist": [{"name": "Radiohead", "mbid": "x", "match": "1.0"}]}} with patch("lyra_worker.similarity._lastfm.requests.get", return_value=_resp(payload)): assert LastfmSource(api_key="k", browser=_FakeBrowser()).health() is True def test_health_false_on_error_envelope(): payload = {"error": 10, "message": "invalid api key"} with patch("lyra_worker.similarity._lastfm.requests.get", return_value=_resp(payload)): assert LastfmSource(api_key="k", browser=_FakeBrowser()).health() is False def test_health_false_on_request_exception(): with patch("lyra_worker.similarity._lastfm.requests.get", side_effect=requests.ConnectionError("down")): assert LastfmSource(api_key="k", browser=_FakeBrowser()).health() is False