soulsync/tests/test_metadata_service_musicmap.py
Broque Thomas 6314827e91 Fix test-order-dependent failures in new metadata_service tests
The _DummyConfigManager stubs in test_metadata_service_musicmap.py and
test_metadata_service_artist_image.py were missing get_active_media_server(),
which the existing test_metadata_service_discography.py dummy provides.

Both files install their dummy via sys.modules["config.settings"] with an
"if not in sys.modules" guard, so whichever test file loads first wins.
When the new files load alphabetically before discography, the limited
dummy persists and later tests hit AttributeError on get_active_media_server.

Adds the same get_active_media_server method to both dummies so all three
test files are equivalent and test ordering no longer affects outcomes.
2026-04-21 11:39:56 -07:00

235 lines
8.4 KiB
Python

import sys
import types
if "spotipy" not in sys.modules:
spotipy = types.ModuleType("spotipy")
class _DummySpotify:
def __init__(self, *args, **kwargs):
pass
oauth2 = types.ModuleType("spotipy.oauth2")
class _DummyOAuth:
def __init__(self, *args, **kwargs):
pass
spotipy.Spotify = _DummySpotify
oauth2.SpotifyOAuth = _DummyOAuth
oauth2.SpotifyClientCredentials = _DummyOAuth
spotipy.oauth2 = oauth2
sys.modules["spotipy"] = spotipy
sys.modules["spotipy.oauth2"] = oauth2
if "config.settings" not in sys.modules:
config_pkg = types.ModuleType("config")
settings_mod = types.ModuleType("config.settings")
class _DummyConfigManager:
def get(self, key, default=None):
return default
def get_active_media_server(self):
return "primary"
settings_mod.config_manager = _DummyConfigManager()
config_pkg.settings = settings_mod
sys.modules["config"] = config_pkg
sys.modules["config.settings"] = settings_mod
import types as pytypes
from core import metadata_service
class _FakeMusicMapResponse:
def __init__(self, text):
self.text = text
def raise_for_status(self):
return None
class _FakeSourceClient:
def __init__(self, results_by_query):
self.results_by_query = results_by_query
self.search_calls = []
def search_artists(self, query, **kwargs):
self.search_calls.append((query, dict(kwargs)))
return list(self.results_by_query.get(query, []))
def test_iter_musicmap_similar_artist_events_uses_source_priority(monkeypatch):
html = """
<html>
<body>
<div id="gnodMap">
<a href="/artist/seed">Artist One</a>
<a href="/artist/similar">Similar Artist</a>
</div>
</body>
</html>
"""
deezer = _FakeSourceClient({
"Artist One": [pytypes.SimpleNamespace(id="dz-seed", name="Artist One")],
"Similar Artist": [],
})
itunes = _FakeSourceClient({
"Artist One": [pytypes.SimpleNamespace(id="it-seed", name="Artist One")],
"Similar Artist": [
pytypes.SimpleNamespace(
id="it-match",
name="iTunes Canonical",
image_url="https://itunes.example/it-match.jpg",
genres=["indie", "alt"],
popularity=77,
)
],
})
spotify = _FakeSourceClient({})
monkeypatch.setattr(metadata_service.requests, "get", lambda *args, **kwargs: _FakeMusicMapResponse(html))
monkeypatch.setattr(metadata_service, "get_primary_source", lambda: "deezer")
monkeypatch.setattr(metadata_service, "get_source_priority", lambda primary: [primary, "itunes", "spotify"])
monkeypatch.setattr(
metadata_service,
"get_client_for_source",
lambda source: {"deezer": deezer, "itunes": itunes, "spotify": spotify}.get(source),
)
events = list(metadata_service.iter_musicmap_similar_artist_events("Artist One", limit=5))
assert events[0]["type"] == "start"
assert events[0]["source_priority"] == ["deezer", "itunes", "spotify"]
assert events[1]["type"] == "artist"
assert events[1]["artist"]["id"] == "it-match"
assert events[1]["artist"]["name"] == "iTunes Canonical"
assert events[1]["artist"]["image_url"] == "https://itunes.example/it-match.jpg"
assert events[1]["artist"]["genres"] == ["indie", "alt"]
assert events[1]["artist"]["popularity"] == 77
assert events[1]["artist"]["source"] == "itunes"
assert events[-1]["type"] == "complete"
assert events[-1]["complete"] is True
assert events[-1]["total"] == 1
assert events[-1]["total_found"] == 1
assert [call[0] for call in deezer.search_calls] == ["Artist One", "Similar Artist"]
assert [call[0] for call in itunes.search_calls] == ["Artist One", "Similar Artist"]
assert spotify.search_calls == [("Artist One", {"limit": 1, "allow_fallback": False})]
def test_iter_musicmap_similar_artist_events_enriches_itunes_images(monkeypatch):
html = """
<html>
<body>
<div id="gnodMap">
<a href="/artist/similar">Similar Artist</a>
</div>
</body>
</html>
"""
class _ItunesClient(_FakeSourceClient):
def __init__(self):
super().__init__({
"Artist One": [pytypes.SimpleNamespace(id="it-seed", name="Artist One")],
"Similar Artist": [pytypes.SimpleNamespace(id="it-match", name="iTunes Canonical")],
})
self.get_artist_calls = []
def get_artist(self, artist_id):
self.get_artist_calls.append(artist_id)
return {
"id": artist_id,
"name": "iTunes Canonical",
"images": [{"url": "https://itunes.example/full-art.jpg"}],
"genres": ["indie"],
"popularity": 0,
}
itunes = _ItunesClient()
monkeypatch.setattr(metadata_service.requests, "get", lambda *args, **kwargs: _FakeMusicMapResponse(html))
monkeypatch.setattr(metadata_service, "get_primary_source", lambda: "itunes")
monkeypatch.setattr(metadata_service, "get_source_priority", lambda primary: [primary])
monkeypatch.setattr(metadata_service, "get_client_for_source", lambda source: itunes if source == "itunes" else None)
events = list(metadata_service.iter_musicmap_similar_artist_events("Artist One", limit=5))
artist_events = [event for event in events if event.get("type") == "artist"]
assert len(artist_events) == 1
assert artist_events[0]["artist"]["image_url"] == "https://itunes.example/full-art.jpg"
assert itunes.get_artist_calls == ["it-match"]
def test_iter_musicmap_similar_artist_events_falls_back_to_itunes_album_art(monkeypatch):
html = """
<html>
<body>
<div id="gnodMap">
<a href="/artist/similar">Similar Artist</a>
</div>
</body>
</html>
"""
class _ItunesClient(_FakeSourceClient):
def __init__(self):
super().__init__({
"Artist One": [pytypes.SimpleNamespace(id="it-seed", name="Artist One")],
"Similar Artist": [pytypes.SimpleNamespace(id="it-match", name="iTunes Canonical")],
})
self.get_artist_calls = []
self.album_art_calls = []
def get_artist(self, artist_id):
self.get_artist_calls.append(artist_id)
return {
"id": artist_id,
"name": "iTunes Canonical",
"images": [],
"genres": ["indie"],
"popularity": 0,
}
def _get_artist_image_from_albums(self, artist_id):
self.album_art_calls.append(artist_id)
return "https://itunes.example/album-art.jpg"
itunes = _ItunesClient()
monkeypatch.setattr(metadata_service.requests, "get", lambda *args, **kwargs: _FakeMusicMapResponse(html))
monkeypatch.setattr(metadata_service, "get_primary_source", lambda: "itunes")
monkeypatch.setattr(metadata_service, "get_source_priority", lambda primary: [primary])
monkeypatch.setattr(metadata_service, "get_client_for_source", lambda source: itunes if source == "itunes" else None)
events = list(metadata_service.iter_musicmap_similar_artist_events("Artist One", limit=5))
artist_events = [event for event in events if event.get("type") == "artist"]
assert len(artist_events) == 1
assert artist_events[0]["artist"]["image_url"] == "https://itunes.example/album-art.jpg"
assert itunes.get_artist_calls == ["it-match"]
assert itunes.album_art_calls == ["it-match"]
def test_get_musicmap_similar_artists_returns_not_found_when_musicmap_missing(monkeypatch):
html = """
<html>
<body>
<div class="no-map">Nothing here</div>
</body>
</html>
"""
monkeypatch.setattr(metadata_service.requests, "get", lambda *args, **kwargs: _FakeMusicMapResponse(html))
monkeypatch.setattr(metadata_service, "get_primary_source", lambda: "deezer")
monkeypatch.setattr(metadata_service, "get_source_priority", lambda primary: [primary, "itunes"])
monkeypatch.setattr(metadata_service, "get_client_for_source", lambda source: object())
result = metadata_service.get_musicmap_similar_artists("Artist One", limit=5)
assert result["success"] is False
assert result["status_code"] == 404
assert "Could not find artist map" in result["error"]
assert result["similar_artists"] == []