Files
FabledCurator/tests/test_safe_probe.py
T
bvandeusenandClaude Opus 5.5 4cdc851cb5
CI and images / lint (push) Successful in 2s
CI and images / extension-version (push) Successful in 2s
CI and images / frontend-build (push) Successful in 24s
CI and images / extension-test (push) Successful in 18s
CI and images / backend-lint-and-test (push) Successful in 47s
CI and images / integration (push) Successful in 2m44s
CI and images / sign-extension (push) Successful in 3s
CI and images / build-agent (push) Successful in 5s
CI and images / build-web (push) Successful in 2m1s
CI and images / smoke-web (push) Successful in 49s
CI and images / promote (push) Successful in 1s
CI and images / refresh-dev (push) Skipped
CI and images / unattended-failure (push) Skipped
fix: a game build is recognised before the archive integrity test, so re-extract no longer stalls on it
The archive probe read and checksummed every member before the importer
looked at what the archive held. For a multi-gigabyte game build that is
minutes of work on a file that is kept whole and never extracted, and it
held a re-extract chunk far past its time budget.

- The importer checks the archive's directory for a software build first,
  for the outer archive and for one nested inside an art pack, and only
  probes archives it may extract.
- The probe rejects an archive over the size cap from its directory totals,
  before the integrity test decompresses anything.
- Re-extract logs each archive as it starts and finishes, with its size and
  how long it took, so an archive that stalls a chunk is named in the log.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 07:53:05 -05:00

96 lines
3.4 KiB
Python

"""Layer-3 subprocess-isolated probe tests.
The bomb-guard cap is exercised against `_run_probe` directly (in-process,
where a monkeypatch on the module constant takes effect) — the real probe
runs in a subprocess that re-imports the module, so a parent-process
monkeypatch wouldn't reach the spawned interpreter.
"""
import zipfile
from pathlib import Path
from backend.app.utils import safe_probe
def _zip(path, entries):
with zipfile.ZipFile(path, "w") as zf:
for name, data in entries.items():
zf.writestr(name, data)
def test_probe_archive_valid_zip(tmp_path):
z = tmp_path / "ok.zip"
_zip(z, {"a.jpg": b"hello", "b.png": b"world"})
res = safe_probe.probe_archive(z)
assert res.ok is True
assert res.crashed is False
def test_probe_archive_corrupt_zip_clean_rejection(tmp_path):
z = tmp_path / "broken.zip"
z.write_bytes(b"PK\x03\x04 not really a zip past here")
res = safe_probe.probe_archive(z)
assert res.ok is False
# Corrupt-but-handled (zipfile raises BadZipFile in the child) — a
# clean rejection, not a hard crash.
assert res.crashed is False
assert res.reason
def test_inspect_archive_reports_size_and_clean_integrity(tmp_path):
z = tmp_path / "sized.zip"
_zip(z, {"a.txt": b"x" * 100, "b.txt": b"y" * 50})
total, bad = safe_probe._inspect_archive(z)
assert total == 150
assert bad is None
def test_inspect_archive_detects_misnamed_zip(tmp_path):
"""A zip with a mangled / extension-less name (Patreon URL-blob attachment)
is still bomb-guarded + integrity-tested via magic-byte detection."""
z = tmp_path / "01_https___www.patreon.com_media-u_v3_131083093"
_zip(z, {"a.txt": b"x" * 100})
total, bad = safe_probe._inspect_archive(z)
assert total == 100
assert bad is None
def test_run_probe_bomb_guard(tmp_path, monkeypatch):
"""In-process call to _run_probe so the monkeypatched cap takes effect.
The public probe_archive runs in a subprocess which re-imports the
module and wouldn't see the patched constant."""
monkeypatch.setattr(safe_probe, "MAX_ARCHIVE_UNCOMPRESSED_BYTES", 10)
z = tmp_path / "bomb.zip"
_zip(z, {"big.txt": b"x" * 5000}) # 5000 uncompressed > 10-byte cap
status, detail = safe_probe._run_probe(str(z))
assert status == "error"
assert "bomb-guard cap" in detail
def test_an_archive_over_the_cap_is_rejected_without_an_integrity_test(
tmp_path, monkeypatch,
):
"""The size total is read from the directory, so an oversize archive never
reaches the decompress-everything test."""
monkeypatch.setattr(safe_probe, "MAX_ARCHIVE_UNCOMPRESSED_BYTES", 10)
def _no_test(_self):
raise AssertionError("integrity test ran on an archive over the cap")
monkeypatch.setattr(zipfile.ZipFile, "testzip", _no_test)
z = tmp_path / "big.zip"
_zip(z, {"big.txt": b"x" * 5000})
status, detail = safe_probe._run_probe(str(z))
assert status == "error"
assert "bomb-guard cap" in detail
def test_probe_video_non_video_is_not_ok(tmp_path):
"""A text file is not a decodable video. Whether ffprobe is present
(returncode != 0) or absent (OSError → 'unavailable'), the result is
ok=False. We don't assert on crashed/reason so the test is robust to
ffprobe presence in CI."""
f = tmp_path / "nope.txt"
f.write_text("definitely not a video container")
res = safe_probe.probe_video(f)
assert res.ok is False