soulsync/tests/test_album_completeness_job.py
Broque Thomas c940363ec2 Fix CI test failures from incomplete dummy config and encoding
8 test files had _DummyConfigManager missing get_active_media_server(),
causing failures when pytest ran them before the test file that had it.
Whichever file set sys.modules first won, and the incomplete dummy broke
later tests. Also fix script.js read_text() missing encoding='utf-8'
which failed on non-UTF-8 default locales.
2026-04-19 13:04:55 -07:00

308 lines
9.9 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 "plex"
settings_mod.config_manager = _DummyConfigManager()
config_pkg.settings = settings_mod
sys.modules["config"] = config_pkg
sys.modules["config.settings"] = settings_mod
from core.repair_jobs.album_completeness import AlbumCompletenessJob
import core.repair_jobs.album_completeness as album_completeness_module
class _FakeCursor:
def __init__(self, owned_track_numbers):
self._owned_track_numbers = owned_track_numbers
self._last_query = ""
def execute(self, query, params=None):
self._last_query = query
return self
def fetchall(self):
if "SELECT track_number" in self._last_query:
return [(track_number,) for track_number in self._owned_track_numbers]
return []
class _FakeConnection:
def __init__(self, owned_track_numbers):
self._cursor = _FakeCursor(owned_track_numbers)
def cursor(self):
return self._cursor
def close(self):
return None
class _FakeDB:
def __init__(self, owned_track_numbers):
self._owned_track_numbers = owned_track_numbers
def _get_connection(self):
return _FakeConnection(self._owned_track_numbers)
class _FakeSpotifyClient:
def __init__(self, track_count=5):
self.track_count = track_count
self.calls = []
def is_spotify_authenticated(self):
return True
def get_album_tracks(self, album_id):
self.calls.append(album_id)
return {
"items": [
{"id": f"sp-{i}", "name": f"Spotify Track {i}", "track_number": i, "disc_number": 1, "artists": []}
for i in range(1, self.track_count + 1)
]
}
class _FakeDeezerClient:
def __init__(self, track_count=2):
self.track_count = track_count
self.calls = []
def is_authenticated(self):
return True
def get_album_tracks(self, album_id):
self.calls.append(album_id)
return {
"items": [
{"id": f"dz-{i}", "name": f"Deezer Track {i}", "track_number": i, "disc_number": 1, "artists": []}
for i in range(1, self.track_count + 1)
]
}
class _FakeITunesClient:
def __init__(self):
self.calls = []
def is_authenticated(self):
return True
def get_album_tracks(self, album_id):
self.calls.append(album_id)
return {"items": []}
class _FakeDiscogsClient:
def __init__(self, track_count=3):
self.track_count = track_count
self.calls = []
def get_album_tracks(self, album_id):
self.calls.append(album_id)
return {
"items": [
{"id": f"dg-{i}", "name": f"Discogs Track {i}", "track_number": i, "disc_number": 1, "artists": []}
for i in range(1, self.track_count + 1)
]
}
class _FakeHydrabaseClient:
def __init__(self, track_count=4):
self.track_count = track_count
self.calls = []
def is_connected(self):
return True
def get_album_tracks_dict(self, album_id):
self.calls.append(album_id)
return {
"items": [
{"id": f"hy-{i}", "name": f"Hydrabase Track {i}", "track_number": i, "disc_number": 1, "artists": []}
for i in range(1, self.track_count + 1)
]
}
def test_album_completeness_uses_primary_provider_first(monkeypatch):
job = AlbumCompletenessJob()
context = types.SimpleNamespace(
db=_FakeDB(owned_track_numbers={1}),
spotify_client=_FakeSpotifyClient(track_count=5),
is_spotify_rate_limited=lambda: False,
)
deezer_client = _FakeDeezerClient(track_count=2)
itunes_client = _FakeITunesClient()
calls = []
album_ids = {
"spotify": "spotify-album",
"itunes": "itunes-album",
"deezer": "deezer-album",
"discogs": "",
"hydrabase": "",
}
monkeypatch.setattr(album_completeness_module, "get_primary_source", lambda: "deezer")
monkeypatch.setattr(
album_completeness_module,
"get_album_tracks_for_source",
lambda source, album_id: (
calls.append((source, album_id)) or
(
deezer_client.get_album_tracks(album_id) if source == "deezer" else
itunes_client.get_album_tracks(album_id) if source == "itunes" else
context.spotify_client.get_album_tracks(album_id) if source == "spotify" else
{"items": []}
)
)
)
expected_total = job._get_expected_total(context, "deezer", album_ids)
missing_tracks = job._find_missing_tracks(context, "deezer", 42, album_ids)
assert expected_total == 2
assert calls == [("deezer", "deezer-album"), ("deezer", "deezer-album")]
assert deezer_client.calls == ["deezer-album", "deezer-album"]
assert context.spotify_client.calls == []
assert itunes_client.calls == []
assert [track["track_number"] for track in missing_tracks] == [2]
assert missing_tracks[0]["source"] == "deezer"
assert missing_tracks[0]["source_track_id"] == "dz-2"
assert missing_tracks[0]["spotify_track_id"] == "dz-2"
def test_album_completeness_supports_discogs_primary(monkeypatch):
job = AlbumCompletenessJob()
context = types.SimpleNamespace(
db=_FakeDB(owned_track_numbers={1}),
spotify_client=_FakeSpotifyClient(track_count=5),
is_spotify_rate_limited=lambda: False,
)
discogs_client = _FakeDiscogsClient(track_count=3)
itunes_client = _FakeITunesClient()
deezer_client = _FakeDeezerClient(track_count=2)
calls = []
album_ids = {
"spotify": "spotify-album",
"itunes": "itunes-album",
"deezer": "deezer-album",
"discogs": "discogs-release",
"hydrabase": "",
}
monkeypatch.setattr(album_completeness_module, "get_primary_source", lambda: "discogs")
monkeypatch.setattr(
album_completeness_module,
"get_album_tracks_for_source",
lambda source, album_id: (
calls.append((source, album_id)) or
(
discogs_client.get_album_tracks(album_id) if source == "discogs" else
itunes_client.get_album_tracks(album_id) if source == "itunes" else
deezer_client.get_album_tracks(album_id) if source == "deezer" else
context.spotify_client.get_album_tracks(album_id) if source == "spotify" else
{"items": []}
)
)
)
expected_total = job._get_expected_total(context, "discogs", album_ids)
missing_tracks = job._find_missing_tracks(context, "discogs", 42, album_ids)
assert expected_total == 3
assert calls == [("discogs", "discogs-release"), ("discogs", "discogs-release")]
assert discogs_client.calls == ["discogs-release", "discogs-release"]
assert context.spotify_client.calls == []
assert itunes_client.calls == []
assert deezer_client.calls == []
assert [track["track_number"] for track in missing_tracks] == [2, 3]
assert missing_tracks[0]["source"] == "discogs"
assert missing_tracks[0]["source_track_id"] == "dg-2"
def test_album_completeness_supports_hydrabase_primary(monkeypatch):
job = AlbumCompletenessJob()
context = types.SimpleNamespace(
db=_FakeDB(owned_track_numbers={1}),
spotify_client=_FakeSpotifyClient(track_count=5),
is_spotify_rate_limited=lambda: False,
)
hydrabase_client = _FakeHydrabaseClient(track_count=4)
itunes_client = _FakeITunesClient()
deezer_client = _FakeDeezerClient(track_count=2)
calls = []
album_ids = {
"spotify": "spotify-album",
"itunes": "itunes-album",
"deezer": "deezer-album",
"discogs": "",
"hydrabase": "soul-album",
}
monkeypatch.setattr(album_completeness_module, "get_primary_source", lambda: "hydrabase")
monkeypatch.setattr(
album_completeness_module,
"get_album_tracks_for_source",
lambda source, album_id: (
calls.append((source, album_id)) or
(
hydrabase_client.get_album_tracks_dict(album_id) if source == "hydrabase" else
itunes_client.get_album_tracks(album_id) if source == "itunes" else
deezer_client.get_album_tracks(album_id) if source == "deezer" else
context.spotify_client.get_album_tracks(album_id) if source == "spotify" else
{"items": []}
)
)
)
expected_total = job._get_expected_total(context, "hydrabase", album_ids)
missing_tracks = job._find_missing_tracks(context, "hydrabase", 42, album_ids)
assert expected_total == 4
assert calls == [("hydrabase", "soul-album"), ("hydrabase", "soul-album")]
assert hydrabase_client.calls == ["soul-album", "soul-album"]
assert context.spotify_client.calls == []
assert itunes_client.calls == []
assert deezer_client.calls == []
assert [track["track_number"] for track in missing_tracks] == [2, 3, 4]
assert missing_tracks[0]["source"] == "hydrabase"
assert missing_tracks[0]["source_track_id"] == "hy-2"