Files
FabledCurator/tests/test_api_cleanup.py
T
bvandeusen def967a1a8 refactor(dry-S1): hoist app/client test fixtures into conftest
Removed the app/client fixtures duplicated across 36 test files (two
variants: separate app + client(app), and a self-contained client() that
called create_app inline) and the now-unused create_app imports. Both
fixtures now live once in conftest.py. test_suggestions_bulk keeps its
import (builds the app inline in two tests); test_health drops its local
client + unused pytest_asyncio.

Net -415 lines.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 11:33:05 -04:00

208 lines
6.6 KiB
Python

"""API tests for the /api/cleanup/* blueprint.
Per reference-async-coredml-test-assertions, post-DML state checks go
via column selects, not ORM entity access.
"""
import hashlib
from datetime import UTC, datetime
import pytest
from PIL import Image
from sqlalchemy import func, select
from backend.app.celery_app import celery
from backend.app.models import ImageRecord, LibraryAuditRun
pytestmark = pytest.mark.integration
@pytest.fixture(autouse=True)
def disable_celery_eager(monkeypatch):
monkeypatch.setattr(celery.conf, "task_always_eager", False)
def _sha256_min_dim_token(min_w: int, min_h: int) -> str:
canon = f"{min_w}x{min_h}"
return f"delete-min-dim-{hashlib.sha256(canon.encode()).hexdigest()[:8]}"
async def _seed_image(db, tmp_path, *, w, h, name):
path = tmp_path / name
Image.new("RGB", (w, h), (w % 256, h % 256, 0)).save(path)
sha = f"api-cleanup-{name}".ljust(64, "x")[:64]
rec = ImageRecord(
path=str(path), sha256=sha,
size_bytes=path.stat().st_size, mime="image/png",
width=w, height=h, origin="imported_filesystem",
integrity_status="ok",
)
db.add(rec)
await db.flush()
return rec
@pytest.mark.asyncio
async def test_min_dimension_preview_returns_count(client, db, tmp_path):
await _seed_image(db, tmp_path, w=50, h=50, name="small.png")
await _seed_image(db, tmp_path, w=500, h=500, name="big.png")
await db.commit()
resp = await client.post(
"/api/cleanup/min-dimension/preview",
json={"min_width": 200, "min_height": 200},
)
assert resp.status_code == 200
body = await resp.get_json()
assert body["count"] == 1
# Preview hands the canonical Tier-C confirm token back so the
# frontend doesn't have to recompute SHA-256 client-side
# (crypto.subtle is Secure-Context-gated; FC runs over plain HTTP).
assert body["confirm_token"] == _sha256_min_dim_token(200, 200)
@pytest.mark.asyncio
async def test_min_dimension_delete_with_token_removes_rows(client, db, tmp_path):
await _seed_image(db, tmp_path, w=50, h=50, name="s2.png")
await _seed_image(db, tmp_path, w=500, h=500, name="b2.png")
await db.commit()
token = _sha256_min_dim_token(200, 200)
resp = await client.post(
"/api/cleanup/min-dimension/delete",
json={"min_width": 200, "min_height": 200, "confirm": token},
)
assert resp.status_code == 200
body = await resp.get_json()
assert body["deleted"] == 1
remaining = await db.execute(select(func.count()).select_from(ImageRecord))
assert remaining.scalar_one() == 1
@pytest.mark.asyncio
async def test_min_dimension_delete_with_bad_token_returns_400(client, db, tmp_path):
await _seed_image(db, tmp_path, w=50, h=50, name="s3.png")
await db.commit()
resp = await client.post(
"/api/cleanup/min-dimension/delete",
json={"min_width": 200, "min_height": 200, "confirm": "nope"},
)
assert resp.status_code == 400
body = await resp.get_json()
assert body["error"] == "confirm_mismatch"
@pytest.mark.asyncio
async def test_audit_create_returns_id_and_running_status(
client, db, monkeypatch,
):
from backend.app.tasks import library_audit
monkeypatch.setattr(
library_audit.scan_library_for_rule, "delay", lambda audit_id: None,
)
resp = await client.post(
"/api/cleanup/audit",
json={"rule": "transparency", "params": {"threshold": 0.9}},
)
assert resp.status_code == 202
body = await resp.get_json()
assert body["status"] == "running"
assert isinstance(body["audit_id"], int)
@pytest.mark.asyncio
async def test_audit_create_returns_409_when_another_is_running(client, db):
audit = LibraryAuditRun(
rule="transparency", params={"threshold": 0.9},
status="running", matched_ids=[],
)
db.add(audit)
await db.commit()
resp = await client.post(
"/api/cleanup/audit",
json={"rule": "single_color", "params": {"threshold": 0.95, "tolerance": 30}},
)
assert resp.status_code == 409
@pytest.mark.asyncio
async def test_audit_get_by_id_returns_full_row(client, db):
audit = LibraryAuditRun(
rule="transparency", params={"threshold": 0.85},
status="ready", scanned_count=100, matched_count=3,
matched_ids=[1, 2, 3],
)
db.add(audit)
await db.commit()
resp = await client.get(f"/api/cleanup/audit/{audit.id}")
assert resp.status_code == 200
body = await resp.get_json()
assert body["rule"] == "transparency"
assert body["matched_count"] == 3
assert body["matched_ids"] == [1, 2, 3]
@pytest.mark.asyncio
async def test_audit_history_returns_recent_runs(client, db):
for _ in range(3):
db.add(LibraryAuditRun(
rule="transparency", params={"threshold": 0.9},
status="applied", matched_ids=[],
finished_at=datetime.now(UTC),
))
await db.commit()
resp = await client.get("/api/cleanup/audit?limit=5")
assert resp.status_code == 200
body = await resp.get_json()
assert len(body["runs"]) >= 3
@pytest.mark.asyncio
async def test_audit_apply_with_token_deletes(client, db, tmp_path):
rec = await _seed_image(db, tmp_path, w=100, h=100, name="apply.png")
audit = LibraryAuditRun(
rule="transparency", params={"threshold": 0.9},
status="ready", scanned_count=1, matched_count=1,
matched_ids=[rec.id],
)
db.add(audit)
await db.commit()
resp = await client.post(
f"/api/cleanup/audit/{audit.id}/apply",
json={"confirm": f"delete-audit-{audit.id}"},
)
assert resp.status_code == 200
body = await resp.get_json()
assert body["deleted"] == 1
@pytest.mark.asyncio
async def test_audit_apply_with_bad_token_returns_400(client, db):
audit = LibraryAuditRun(
rule="transparency", params={"threshold": 0.9},
status="ready", matched_ids=[],
)
db.add(audit)
await db.commit()
resp = await client.post(
f"/api/cleanup/audit/{audit.id}/apply",
json={"confirm": "wrong-token"},
)
assert resp.status_code == 400
body = await resp.get_json()
assert body["error"] == "confirm_mismatch"
@pytest.mark.asyncio
async def test_audit_cancel_flips_status(client, db):
audit = LibraryAuditRun(
rule="transparency", params={"threshold": 0.9},
status="running", matched_ids=[],
)
db.add(audit)
await db.commit()
resp = await client.post(f"/api/cleanup/audit/{audit.id}/cancel")
assert resp.status_code == 200
new_status = await db.execute(
select(LibraryAuditRun.status).where(LibraryAuditRun.id == audit.id)
)
assert new_status.scalar_one() == "cancelled"