This repository has been archived on 2026-05-31. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
GallerySubscriber/backend/tests/tasks/test_validate_library.py
T
bvandeusen 6d875662fa feat(maintenance): add library-validation sweep task and API
Walks the downloads tree and reports files that fail magic-byte
validation — the "how many pre-existing files in my library are
silently truncated?" question, answered. Skips the _quarantine
subtree (already known-bad) and caps suspect_paths at 500 with a
truncated flag so the persisted report can't grow unbounded.

On-demand only: a full filesystem walk on a NAS-mounted library is
expensive and the user should choose when to pay for it. Pre-existing
files are reported, not quarantined — they may be the only copy and
the user decides what to do.

Adds GET /api/settings/library-validation (most recent report) and
POST /api/settings/library-validation/run (queue a fresh sweep).
Cached under cache.library_validation_report.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-25 22:56:01 -04:00

82 lines
2.5 KiB
Python

"""Tests for the library-validation sweep.
Covers: scanning a fake library, ignoring the _quarantine subtree,
counting suspect files by format, and capping suspect_paths.
"""
from pathlib import Path
import pytest
from app.services.file_validator import (
GIF_HEAD_89A,
JPEG_HEAD,
JPEG_TAIL,
PNG_HEAD,
PNG_TAIL,
)
from app.tasks.maintenance import scan_library_for_corruption
def _good_jpeg(p: Path) -> None:
p.parent.mkdir(parents=True, exist_ok=True)
p.write_bytes(JPEG_HEAD + b"\x00" * 32 + JPEG_TAIL)
def _bad_jpeg(p: Path) -> None:
p.parent.mkdir(parents=True, exist_ok=True)
p.write_bytes(JPEG_HEAD + b"\x00" * 32) # no EOI
def _bad_png(p: Path) -> None:
p.parent.mkdir(parents=True, exist_ok=True)
p.write_bytes(PNG_HEAD + b"\x00" * 32) # no IEND
def test_scan_finds_truncated_files(tmp_path):
_good_jpeg(tmp_path / "Artist" / "patreon" / "01.jpg")
_bad_jpeg(tmp_path / "Artist" / "patreon" / "02.jpg")
_bad_png(tmp_path / "Artist" / "patreon" / "03.png")
# JSON sidecars must not be scanned
(tmp_path / "Artist" / "patreon" / "01.json").write_bytes(b"{}")
report = scan_library_for_corruption(tmp_path)
assert report["scanned"] == 3
assert report["suspect_count"] == 2
assert report["by_format"] == {"jpeg": 1, "png": 1}
paths = {entry["path"] for entry in report["suspect_paths"]}
assert any("02.jpg" in p for p in paths)
assert any("03.png" in p for p in paths)
assert not report["truncated"]
def test_scan_skips_quarantine_subtree(tmp_path):
"""Files already moved to _quarantine are known-bad; don't re-scan them."""
_bad_jpeg(tmp_path / "_quarantine" / "Artist" / "patreon" / "01.jpg")
_good_jpeg(tmp_path / "Artist" / "patreon" / "02.jpg")
report = scan_library_for_corruption(tmp_path)
assert report["scanned"] == 1
assert report["suspect_count"] == 0
def test_scan_caps_suspect_paths(tmp_path):
"""suspect_count is the true total; suspect_paths is capped + truncated flag set."""
for i in range(7):
_bad_jpeg(tmp_path / "Artist" / "patreon" / f"{i:02d}.jpg")
report = scan_library_for_corruption(tmp_path, max_suspect_paths=3)
assert report["scanned"] == 7
assert report["suspect_count"] == 7
assert len(report["suspect_paths"]) == 3
assert report["truncated"] is True
def test_scan_missing_root_returns_empty_report(tmp_path):
report = scan_library_for_corruption(tmp_path / "does_not_exist")
assert report["scanned"] == 0
assert report["suspect_count"] == 0
assert report["suspect_paths"] == []