mirror of https://github.com/Nezreka/SoulSync.git
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
181 lines
6.5 KiB
181 lines
6.5 KiB
"""AcoustID error-vs-no-match reporting.
|
|
|
|
Regression for the masking bug: an invalid API key (and other lookup errors)
|
|
used to collapse into the same `None` as a genuine no-match, so the UI showed a
|
|
benign "Skipped" and the "Test API key" button reported a dead key as valid.
|
|
These tests pin the distinction end to end:
|
|
- lookup_with_status separates ok / no_match / error / no_backend / unavailable
|
|
- fingerprint_and_lookup (legacy) stays dict-or-None
|
|
- verify_audio_file -> ERROR for a real error, SKIP for a genuine no-match
|
|
- test_api_key reports an invalid key (API error code 4) as invalid
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import sys
|
|
import types
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
import pytest
|
|
|
|
import core.acoustid_client as acc
|
|
from core.acoustid_client import AcoustIDClient
|
|
from core.acoustid_verification import AcoustIDVerification, VerificationResult
|
|
|
|
|
|
# ── lookup_with_status: structured status distinction ──────────────────────
|
|
|
|
def _client_with_fake_acoustid(monkeypatch, *, match=None, raises=None):
|
|
"""An AcoustIDClient wired to a fake `acoustid` module so we can drive
|
|
match() without network or chromaprint."""
|
|
fake = types.ModuleType("acoustid")
|
|
|
|
class WebServiceError(Exception):
|
|
pass
|
|
|
|
class NoBackendError(Exception):
|
|
pass
|
|
|
|
class FingerprintGenerationError(Exception):
|
|
pass
|
|
|
|
fake.WebServiceError = WebServiceError
|
|
fake.NoBackendError = NoBackendError
|
|
fake.FingerprintGenerationError = FingerprintGenerationError
|
|
|
|
def _match(api_key, audio_file, parse=True):
|
|
if raises is not None:
|
|
raise raises
|
|
return match or []
|
|
|
|
fake.match = _match
|
|
monkeypatch.setitem(sys.modules, "acoustid", fake)
|
|
monkeypatch.setattr(acc, "ACOUSTID_AVAILABLE", True)
|
|
|
|
c = AcoustIDClient()
|
|
c._api_key = "testkey123" # bypass config
|
|
return c, fake
|
|
|
|
|
|
def test_lookup_status_ok(tmp_path, monkeypatch):
|
|
f = tmp_path / "a.bin"; f.write_bytes(b"not audio") # mutagen -> None, channel check skipped
|
|
c, _ = _client_with_fake_acoustid(monkeypatch, match=[(0.95, "mbid-1", "Title", "Artist")])
|
|
res = c.lookup_with_status(str(f))
|
|
assert res["status"] == "ok"
|
|
assert res["recordings"] and res["recordings"][0]["mbid"] == "mbid-1"
|
|
|
|
|
|
def test_lookup_status_no_match(tmp_path, monkeypatch):
|
|
f = tmp_path / "a.bin"; f.write_bytes(b"not audio")
|
|
c, _ = _client_with_fake_acoustid(monkeypatch, match=[])
|
|
res = c.lookup_with_status(str(f))
|
|
assert res["status"] == "no_match"
|
|
assert res["recordings"] == []
|
|
|
|
|
|
def test_lookup_status_error_on_webservice(tmp_path, monkeypatch):
|
|
f = tmp_path / "a.bin"; f.write_bytes(b"not audio")
|
|
c, fake = _client_with_fake_acoustid(monkeypatch)
|
|
# invalid key surfaces (old pyacoustid) as the bare "status: error"
|
|
monkeypatch.setattr(c, "_api_key", "testkey123")
|
|
|
|
def _raise(*a, **k):
|
|
raise fake.WebServiceError("status: error")
|
|
fake.match = _raise
|
|
|
|
res = c.lookup_with_status(str(f))
|
|
assert res["status"] == "error"
|
|
assert res["invalid_key"] is True
|
|
|
|
|
|
def test_lookup_status_no_backend(tmp_path, monkeypatch):
|
|
f = tmp_path / "a.bin"; f.write_bytes(b"not audio")
|
|
c, fake = _client_with_fake_acoustid(monkeypatch)
|
|
|
|
def _raise(*a, **k):
|
|
raise fake.NoBackendError()
|
|
fake.match = _raise
|
|
|
|
assert c.lookup_with_status(str(f))["status"] == "no_backend"
|
|
|
|
|
|
def test_lookup_status_unavailable_without_key(tmp_path, monkeypatch):
|
|
f = tmp_path / "a.bin"; f.write_bytes(b"x")
|
|
monkeypatch.setattr(acc, "ACOUSTID_AVAILABLE", True)
|
|
c = AcoustIDClient()
|
|
c._api_key = "" # no key
|
|
assert c.lookup_with_status(str(f))["status"] == "unavailable"
|
|
|
|
|
|
# ── fingerprint_and_lookup keeps its dict-or-None contract ─────────────────
|
|
|
|
def test_legacy_wrapper_returns_dict_on_match(tmp_path, monkeypatch):
|
|
f = tmp_path / "a.bin"; f.write_bytes(b"not audio")
|
|
c, _ = _client_with_fake_acoustid(monkeypatch, match=[(0.9, "mbid-1", "T", "A")])
|
|
out = c.fingerprint_and_lookup(str(f))
|
|
assert out is not None and out["recordings"][0]["mbid"] == "mbid-1"
|
|
|
|
|
|
def test_legacy_wrapper_returns_none_on_error(tmp_path, monkeypatch):
|
|
f = tmp_path / "a.bin"; f.write_bytes(b"not audio")
|
|
c, fake = _client_with_fake_acoustid(monkeypatch)
|
|
|
|
def _raise(*a, **k):
|
|
raise fake.WebServiceError("status: error")
|
|
fake.match = _raise
|
|
assert c.fingerprint_and_lookup(str(f)) is None
|
|
|
|
|
|
# ── verify_audio_file: ERROR vs SKIP ───────────────────────────────────────
|
|
|
|
def _verifier_with_lookup(result):
|
|
v = AcoustIDVerification()
|
|
client = MagicMock()
|
|
client.is_available.return_value = (True, "ready")
|
|
client.lookup_with_status.return_value = result
|
|
v.acoustid_client = client
|
|
return v
|
|
|
|
|
|
def test_verify_reports_error_for_api_error():
|
|
v = _verifier_with_lookup({"status": "error", "recordings": [], "error": "AcoustID API error: invalid"})
|
|
result, msg = v.verify_audio_file("/x.flac", "Song", "Artist")
|
|
assert result == VerificationResult.ERROR
|
|
assert "error" in msg.lower() or "invalid" in msg.lower()
|
|
|
|
|
|
def test_verify_reports_skip_for_no_match():
|
|
v = _verifier_with_lookup({"status": "no_match", "recordings": [], "error": "Track not found in AcoustID database"})
|
|
result, msg = v.verify_audio_file("/x.flac", "Song", "Artist")
|
|
assert result == VerificationResult.SKIP
|
|
assert "no match" in msg.lower() or "not found" in msg.lower()
|
|
|
|
|
|
# ── test_api_key: invalid key reported as invalid ──────────────────────────
|
|
|
|
def _api_response(payload):
|
|
r = MagicMock()
|
|
r.json.return_value = payload
|
|
return r
|
|
|
|
|
|
def test_test_api_key_invalid_when_code_4(monkeypatch):
|
|
c = AcoustIDClient()
|
|
c._api_key = "badkey"
|
|
with patch("requests.get",
|
|
return_value=_api_response({"status": "error", "error": {"code": 4, "message": "invalid API key"}})):
|
|
ok, msg = c.test_api_key()
|
|
assert ok is False
|
|
assert "invalid" in msg.lower()
|
|
|
|
|
|
def test_test_api_key_valid_when_accepted(monkeypatch):
|
|
c = AcoustIDClient()
|
|
c._api_key = "goodkey"
|
|
# A non-key error (e.g. bad dummy fingerprint) means the key was accepted.
|
|
with patch("requests.get",
|
|
return_value=_api_response({"status": "error", "error": {"code": 3, "message": "invalid fingerprint"}})):
|
|
ok, msg = c.test_api_key()
|
|
assert ok is True
|
|
assert "valid" in msg.lower()
|