"""Tests for the R2 context-menu backend: per-song "Refresh metadata" (explicit re-match reset + kick) and "Get info" (file location + pack contents). The refresh flow reuses the P8 fake-transport pattern — nothing here opens a socket.""" import importlib import enrichment import sys import pytest from fastapi.testclient import TestClient @pytest.fixture() def server(tmp_path, monkeypatch, isolate_logging): monkeypatch.setenv("CONFIG_DIR", str(tmp_path / "config")) dlc = tmp_path / "dlc" dlc.mkdir() monkeypatch.setenv("DLC_DIR", str(dlc)) monkeypatch.setenv("FEEDBACK_SKIP_STARTUP_TASKS", "1") sys.modules.pop("server", None) srv = importlib.import_module("server") try: yield srv finally: conn = getattr(getattr(srv, "meta_db", None), "conn", None) if conn is not None: getattr(__import__("sys").modules.get("server"), "_join_background_db_threads", lambda: None)() conn.close() sys.modules.pop("server", None) @pytest.fixture() def client(server): return TestClient(server.app) def _put(server, fn, title="Thunderstruck", artist="AC/DC", album="", year="1990", duration=292): server.meta_db.put(fn, 0, 0, { "title": title, "artist": artist, "album": album, "year": year, "duration": duration, "arrangements": [{"name": "Lead", "index": 0}], }) def make_sloppak(server, name, extra_yaml="", title="Thunderstruck", artist="AC/DC"): d = server.DLC_DIR / name d.mkdir(parents=True) (d / "manifest.yaml").write_text( f"title: {title}\nartist: {artist}\nduration: 292\n" "arrangements:\n - name: Lead\n id: lead\n" "stems:\n - id: full\n file: stems/full.ogg\n" + extra_yaml, encoding="utf-8") _put(server, name, title=title, artist=artist) return d # ── Refresh metadata ────────────────────────────────────────────────────────── def test_refresh_resets_even_a_manual_pin_and_rematches(server, client, monkeypatch): _put(server, "a.sloppak") server.meta_db.set_enrichment_manual( "a.sloppak", {"recording_id": "old-pin", "title": "Thunderstruck", "artist": "AC/DC"}, source="search") # The reset itself is synchronous; the kicked pass runs on a daemon # thread, so assert the reset here and drive the re-match inline below. r = client.post("/api/enrichment/refresh/a.sloppak") assert r.status_code == 200 for _ in range(200): if not client.get("/api/enrichment/status").json()["running"]: break import time as _t _t.sleep(0.02) row = server.meta_db.get_enrichment("a.sloppak") assert row["match_state"] == "unscanned" # the pin was discarded assert row["mb_recording_id"] is None assert row["attempts"] == 0 # …and the normal pass re-matches it (fake transport, network flag on). def fake(path, params): return {"recordings": [{ "id": "rec-new", "score": 100, "title": "Thunderstruck", "length": 292000, "artist-credit": [{"name": "AC/DC", "artist": { "id": "art-1", "name": "AC/DC", "sort-name": "AC/DC"}}], "releases": [{"id": "rel-1", "title": "The Razors Edge", "status": "Official", "date": "1990-09-24", "release-group": {"primary-type": "Album"}}], }]} monkeypatch.setattr(enrichment, "_mb_http_get", fake) monkeypatch.setattr(enrichment, "_enrich_network_enabled", lambda: True) enrichment._background_enrich() assert server.meta_db.get_enrichment("a.sloppak")["mb_recording_id"] == "rec-new" def test_refresh_unknown_song_404(server, client): assert client.post("/api/enrichment/refresh/ghost.sloppak").status_code == 404 # ── Get info ────────────────────────────────────────────────────────────────── def test_fileinfo_sloppak_contents(server, client): make_sloppak(server, "a.sloppak", extra_yaml="mbid: 12345678-abcd-4ef0-9876-0123456789ab\n" "genres: [rock, hard rock]\ntrack: 3\n") body = client.get("/api/chart/a.sloppak/fileinfo").json() assert body["format"] == "sloppak" assert body["filename"] == "a.sloppak" assert body["path"].endswith("a.sloppak") assert body["size"] > 0 m = body["manifest"] assert m["title"] == "Thunderstruck" assert m["arrangements"] == ["Lead"] assert m["stems"] == ["full"] assert m["has_cover"] is False assert m["identity"]["mbid"] == "12345678-abcd-4ef0-9876-0123456789ab" assert m["identity"]["genres"] == ["rock", "hard rock"] assert m["identity"]["track"] == 3 assert "isrc" not in m["identity"] # only keys actually present # No enrichment row yet → no match block (the panel shows "Not scanned"). assert "match" not in body def test_fileinfo_includes_match_verdict(server, client): make_sloppak(server, "a.sloppak") server.meta_db.set_enrichment_manual( "a.sloppak", {"recording_id": "rec-1", "title": "Thunderstruck", "artist": "AC/DC", "album": "The Razors Edge"}, source="search") body = client.get("/api/chart/a.sloppak/fileinfo").json() assert body["match"]["match_state"] == "manual" assert body["match"]["canon_album"] == "The Razors Edge" def test_fileinfo_missing_and_traversal(server, client): assert client.get("/api/chart/ghost.sloppak/fileinfo").status_code == 404 assert client.get("/api/chart/..%2f..%2fetc%2fpasswd/fileinfo").status_code in (403, 404) def test_fileinfo_non_chart_file_is_404(server, client): """A stray non-song file the user keeps under DLC_DIR must not have its path/size/mtime exposed — the route is charts only, not a filesystem stat.""" (server.DLC_DIR / "private-notes.txt").write_text("secret", encoding="utf-8") assert client.get("/api/chart/private-notes.txt/fileinfo").status_code == 404