152 lines
5.1 KiB
Python
152 lines
5.1 KiB
Python
import runpy
|
||
from pathlib import Path
|
||
|
||
import pytest
|
||
|
||
from serato_doctor.web import (
|
||
STATIC_FILES,
|
||
_file_token,
|
||
_verified_audio,
|
||
analyze_paths,
|
||
duplicate_repair,
|
||
duplicate_repair_batch,
|
||
)
|
||
|
||
|
||
def test_web_analysis_uses_production_health_pipeline(tmp_path):
|
||
generator = runpy.run_path(
|
||
str(Path(__file__).parents[1] / "samples/small-library/generate.py")
|
||
)
|
||
sample = generator["build_sample"](tmp_path / "sample")
|
||
|
||
result = analyze_paths(
|
||
sample / "Serato" / "_Serato_", sample / "Music"
|
||
)
|
||
|
||
assert result["score"] == 77.8
|
||
assert result["disk_tracks"] == 10
|
||
assert result["missing_references"] == 2
|
||
assert result["static_crates"] == 5
|
||
assert result["smart_crates"] == 2
|
||
assert result["database_entries"] == 10
|
||
assert result["database_library_matches"] == 10
|
||
assert result["database_missing_paths"] == 0
|
||
assert result["database_missing_unique_filenames"] == 0
|
||
assert result["tracks_missing_from_database"] == 0
|
||
assert result["details"]["old_crate_references"]["total"] == 2
|
||
assert result["details"]["old_crate_references"]["items"][0]["crate"]
|
||
assert result["details"]["suggested_matches"]["total"] == 0
|
||
assert result["details"]["unused_tracks"]["total"] == 1
|
||
assert result["orphan_candidates"] == 0
|
||
assert result["details"]["orphan_candidates"]["assessed"] is True
|
||
|
||
|
||
def test_web_analysis_rejects_missing_folders(tmp_path):
|
||
with pytest.raises(ValueError, match="Serato folder does not exist"):
|
||
analyze_paths(tmp_path / "missing", tmp_path)
|
||
|
||
|
||
def test_web_static_assets_are_declared_and_packaged():
|
||
asset_root = Path(__file__).parents[1] / "serato_doctor" / "webui"
|
||
|
||
assert set(STATIC_FILES) == {
|
||
"/",
|
||
"/app.css",
|
||
"/recovery.css",
|
||
"/layout-fixes.css",
|
||
"/app.js",
|
||
}
|
||
assert all((asset_root / filename).is_file() for filename, _ in STATIC_FILES.values())
|
||
html = (asset_root / "index.html").read_text(encoding="utf-8")
|
||
assert "Missing tracks in Serato" in html
|
||
assert "Old crate references" in html
|
||
assert "Possible orphan files" in html
|
||
assert "Choose a diagnostic" in html
|
||
assert "Backup recovery" in html
|
||
assert "Here’s exactly what will happen" in html
|
||
assert 'id="batch-preview-modal"' in html
|
||
assert 'data-detail="database_missing_tracks"' in html
|
||
assert 'data-detail="old_crate_references"' in html
|
||
assert html.count('class="info-button"') >= 10
|
||
|
||
|
||
def test_duplicate_repair_preview_does_not_change_files(tmp_path):
|
||
serato = tmp_path / "_Serato_"
|
||
serato.mkdir()
|
||
music = tmp_path / "Music"
|
||
first = music / "A" / "Track.mp3"
|
||
second = music / "B" / "Track.mp3"
|
||
first.parent.mkdir(parents=True)
|
||
second.parent.mkdir(parents=True)
|
||
first.write_bytes(b"first")
|
||
second.write_bytes(b"second")
|
||
|
||
result = duplicate_repair(
|
||
serato, music, first, (first, second), backup_limit=10
|
||
)
|
||
|
||
assert result["applied"] is False
|
||
assert result["database_v2_modified"] is False
|
||
assert second.read_bytes() == b"second"
|
||
assert not second.is_symlink()
|
||
|
||
|
||
def test_audio_preview_tokens_only_open_signed_audio(tmp_path):
|
||
track = tmp_path / "Track.mp3"
|
||
track.write_bytes(b"audio")
|
||
|
||
token = _file_token(track)
|
||
|
||
assert _verified_audio(token) == track
|
||
with pytest.raises(ValueError, match="Invalid or expired"):
|
||
_verified_audio(token + "changed")
|
||
|
||
|
||
def test_duplicate_details_include_preview_and_finder_controls(tmp_path):
|
||
serato = tmp_path / "_Serato_"
|
||
serato.mkdir()
|
||
music = tmp_path / "Music"
|
||
first = music / "A" / "Track.mp3"
|
||
second = music / "B" / "Track.mp3"
|
||
first.parent.mkdir(parents=True)
|
||
second.parent.mkdir(parents=True)
|
||
first.write_bytes(b"first")
|
||
second.write_bytes(b"second")
|
||
|
||
result = analyze_paths(serato, music)
|
||
group = result["details"]["duplicate_filenames"]["items"][0]
|
||
|
||
assert len(group["file_previews"]) == 2
|
||
assert group["file_previews"][0]["audio_url"].startswith("/api/audio?")
|
||
assert group["file_previews"][0]["reveal_token"]
|
||
|
||
|
||
def test_batch_preview_combines_approved_groups_without_changes(tmp_path):
|
||
serato = tmp_path / "_Serato_"
|
||
serato.mkdir()
|
||
music = tmp_path / "Music"
|
||
choices = []
|
||
for filename in ("First.mp3", "Second.mp3"):
|
||
keeper = music / "A" / filename
|
||
duplicate = music / "B" / filename
|
||
keeper.parent.mkdir(parents=True, exist_ok=True)
|
||
duplicate.parent.mkdir(parents=True, exist_ok=True)
|
||
keeper.write_bytes(b"keeper")
|
||
duplicate.write_bytes(b"duplicate")
|
||
choices.append(
|
||
{"keeper": str(keeper), "group_files": [str(keeper), str(duplicate)]}
|
||
)
|
||
|
||
result = duplicate_repair_batch(
|
||
serato, music, choices, backup_limit=10
|
||
)
|
||
|
||
assert result["applied"] is False
|
||
assert result["choice_count"] == 2
|
||
assert len(result["replaced"]) == 2
|
||
assert len(result["decisions"]) == 2
|
||
assert result["decisions"][0]["keeper"].endswith("First.mp3")
|
||
assert result["decisions"][0]["hash_status"] == "different"
|
||
assert len(result["decisions"][0]["replaced"]) == 1
|
||
assert all(not Path(choice["group_files"][1]).is_symlink() for choice in choices)
|