soulsync/tests/downloads/test_deezer_pinning.py
Broque Thomas c4c922c40f Surface engine-not-wired errors + exclude soulseek from monitor aggregation
Two findings from JohnBaumb on the engine refactor.

(1) Every download client returned None when self._engine was None,
just logging an error. The orchestrator's download_with_fallback
treated None as "source declined", so the user got no feedback —
download silently disappeared. Now each client raises a RuntimeError
on the engine-not-wired path. download_with_fallback already catches
plugin exceptions, logs a warning, and tries the next source — so
the visible behavior is "real error in logs + fallback to next
source" instead of "silent drop". Six clients touched (deezer, hifi,
qobuz, soundcloud, tidal, youtube). Pinning tests updated to expect
raise.

(2) Monitor's engine.get_all_downloads() walked every plugin
including soulseek, but the same monitor loop already pulled slskd
transfers via the transfers/downloads endpoint a few lines earlier —
soulseek's records were being fetched twice per tick. Same issue in
web_server.py's get_cached_transfer_data path. Added an exclude
parameter to engine.get_all_downloads(); both call sites now pass
('soulseek',). New test pins the exclude semantic.

Also fixed a stray 8-space over-indent on the for-loop body in
get_cached_transfer_data (cosmetic, JohnBaumb flagged the same
pattern in monitor.py earlier).
2026-05-05 12:20:51 -07:00

154 lines
5.4 KiB
Python

"""Phase A pinning tests for DeezerDownloadClient — UPDATED for Phase C6.
Deezer has the same engine-driven dispatch as the other streaming
sources, with three Deezer-specific quirks preserved:
- track_id stays as STRING (Deezer GW API uses string IDs).
- Engine record's `username` slot is the legacy `'deezer_dl'`
via worker username_override.
- Worker thread is named `deezer-dl-<track_id>` for diagnostics.
"""
from __future__ import annotations
import asyncio
import threading
from pathlib import Path
from unittest.mock import patch
import pytest
from core.download_engine import DownloadEngine
from core.deezer_download_client import DeezerDownloadClient
def _run_async(coro):
loop = asyncio.new_event_loop()
try:
return loop.run_until_complete(coro)
finally:
loop.close()
@pytest.fixture
def deezer_client_with_engine():
client = DeezerDownloadClient.__new__(DeezerDownloadClient)
client.download_path = Path('./test_deezer_downloads')
client.shutdown_check = None
client._authenticated = True
client._engine = None
engine = DownloadEngine()
client.set_engine(engine)
return client, engine
def test_download_returns_none_when_not_authenticated(deezer_client_with_engine):
client, _ = deezer_client_with_engine
client._authenticated = False
result = _run_async(client.download('deezer_dl', '12345||x', 0))
assert result is None
def test_download_raises_when_engine_not_wired():
"""Defensive: client without engine reference must raise so the
orchestrator's download_with_fallback surfaces the error and
moves on to the next source. Returning None silently would drop
the download with no user feedback (per JohnBaumb)."""
import pytest
client = DeezerDownloadClient.__new__(DeezerDownloadClient)
client._engine = None
# Bypass auth gate so we exercise the engine check.
client._authenticated = True
with pytest.raises(RuntimeError, match="engine reference"):
_run_async(client.download('deezer', 'v||t', 0))
def test_download_track_id_stays_as_string(deezer_client_with_engine):
"""Pinning: Deezer GW API uses string IDs — engine record must
keep track_id as str."""
client, engine = deezer_client_with_engine
started = threading.Event()
release = threading.Event()
def slow_impl(*args, **kwargs):
started.set()
release.wait(timeout=1.0)
return '/tmp/done.flac'
with patch.object(client, '_download_sync', side_effect=slow_impl):
download_id = _run_async(client.download('deezer_dl', '999||X', 0))
started.wait(timeout=1.0)
record = engine.get_record('deezer', download_id)
assert record['track_id'] == '999'
assert isinstance(record['track_id'], str)
release.set()
def test_download_username_slot_is_legacy_deezer_dl(deezer_client_with_engine):
"""Pinning: frontend status indicators key off `'deezer_dl'`,
not the canonical `'deezer'`."""
client, engine = deezer_client_with_engine
started = threading.Event()
release = threading.Event()
def slow_impl(*args, **kwargs):
started.set()
release.wait(timeout=1.0)
return '/tmp/done.flac'
with patch.object(client, '_download_sync', side_effect=slow_impl):
download_id = _run_async(client.download('deezer_dl', '999||x', 0))
started.wait(timeout=1.0)
assert engine.get_record('deezer', download_id)['username'] == 'deezer_dl'
release.set()
def test_download_handles_missing_display_name_with_fallback(deezer_client_with_engine):
"""Pinning: filename without `||` synthesizes display name `Track <id>`."""
client, engine = deezer_client_with_engine
started = threading.Event()
release = threading.Event()
def slow_impl(*args, **kwargs):
started.set()
release.wait(timeout=1.0)
return '/tmp/x.flac'
with patch.object(client, '_download_sync', side_effect=slow_impl):
download_id = _run_async(client.download('deezer_dl', '12345', 0))
started.wait(timeout=1.0)
assert engine.get_record('deezer', download_id)['display_name'] == 'Track 12345'
release.set()
def test_download_engine_record_carries_error_slot(deezer_client_with_engine):
"""Pinning: Deezer-specific `error` slot for ARL re-auth failure
messages must be present on init."""
client, engine = deezer_client_with_engine
started = threading.Event()
release = threading.Event()
def slow_impl(*args, **kwargs):
started.set()
release.wait(timeout=1.0)
return '/tmp/x.flac'
with patch.object(client, '_download_sync', side_effect=slow_impl):
download_id = _run_async(client.download('deezer_dl', '999||X', 1024))
started.wait(timeout=1.0)
record = engine.get_record('deezer', download_id)
assert 'error' in record
assert record['error'] is None
assert record['size'] == 1024
release.set()
def test_get_all_downloads_reads_engine_records(deezer_client_with_engine):
client, engine = deezer_client_with_engine
engine.add_record('deezer', 'dl-1', {
'id': 'dl-1', 'filename': '111||A', 'username': 'deezer_dl',
'state': 'InProgress, Downloading', 'progress': 50.0,
})
result = _run_async(client.get_all_downloads())
assert len(result) == 1
assert result[0].id == 'dl-1'
assert result[0].username == 'deezer_dl'