verifile-x-api / backend /tests /test_batch.py
abinazebinoy's picture
fix(batch): accept 429 in test_batch_api_rejects_non_image
1d511a9
Raw
History Blame Contribute Delete
7.04 kB
"""
Tests for batch investigation mode.
"""
import pytest
import numpy as np
from PIL import Image
from io import BytesIO
def _make_jpeg(seed: int = 42, width: int = 100, height: int = 100) -> bytes:
rng = np.random.default_rng(seed)
arr = rng.integers(30, 220, (height, width, 3), dtype=np.uint8)
buf = BytesIO()
Image.fromarray(arr, "RGB").save(buf, format="JPEG", quality=85)
return buf.getvalue()
def _make_batch_images(n: int = 3) -> list:
return [{"filename": f"img_{i}.jpg", "data": _make_jpeg(seed=i*10)}
for i in range(n)]
# ── Unit tests ────────────────────────────────────────────────────────────────
def test_batch_statistics_structure():
from backend.services.batch_processor import _batch_statistics
mock_reports = [
{"summary": {"ai_probability": 0.8, "ai_classification": "likely_ai_generated"},
"generator_attribution": {"predicted_generator": "stylegan"},
"c2pa_provenance": {"provenance_status": "none"},
"file_info": {"filename": "a.jpg"}},
{"summary": {"ai_probability": 0.2, "ai_classification": "likely_authentic"},
"generator_attribution": {"predicted_generator": "real"},
"c2pa_provenance": {"provenance_status": "none"},
"file_info": {"filename": "b.jpg"}},
]
stats = _batch_statistics(mock_reports)
assert stats["total_images"] == 2
assert stats["ai_detected_count"] == 1
assert stats["authentic_count"] == 1
assert 0.0 <= stats["mean_ai_probability"] <= 1.0
assert "batch_verdict" in stats
assert "classification_breakdown" in stats
def test_risk_ranking_is_descending():
from backend.services.batch_processor import _rank_by_risk
mock_reports = [
{"summary": {"ai_probability": 0.3, "ai_classification": "likely_authentic"},
"file_info": {"filename": "low.jpg"},
"evidence_id": "aaa",
"generator_attribution": {"predicted_generator": "real"},
"c2pa_provenance": {"provenance_status": "none"}},
{"summary": {"ai_probability": 0.9, "ai_classification": "likely_ai_generated"},
"file_info": {"filename": "high.jpg"},
"evidence_id": "bbb",
"generator_attribution": {"predicted_generator": "stylegan"},
"c2pa_provenance": {"provenance_status": "none"}},
]
ranked = _rank_by_risk(mock_reports)
assert ranked[0]["filename"] == "high.jpg"
assert ranked[1]["filename"] == "low.jpg"
assert ranked[0]["ai_probability"] >= ranked[1]["ai_probability"]
def test_duplicate_detection_identical():
from backend.services.batch_processor import _find_duplicates
# Same hash = distance 0 = identical
mock_reports = [
{"hashes": {"perceptual_hash": "f0f0f0f0f0f0f0f0"},
"file_info": {"filename": "a.jpg"}},
{"hashes": {"perceptual_hash": "f0f0f0f0f0f0f0f0"},
"file_info": {"filename": "b.jpg"}},
]
pairs = _find_duplicates(mock_reports)
assert len(pairs) == 1
assert pairs[0]["similarity"] == "identical"
assert pairs[0]["phash_distance"] == 0
def test_duplicate_detection_no_match():
from backend.services.batch_processor import _find_duplicates
mock_reports = [
{"hashes": {"perceptual_hash": "0000000000000000"},
"file_info": {"filename": "a.jpg"}},
{"hashes": {"perceptual_hash": "ffffffffffffffff"},
"file_info": {"filename": "b.jpg"}},
]
pairs = _find_duplicates(mock_reports)
assert len(pairs) == 0
def test_provenance_consistency_no_credentials():
from backend.services.batch_processor import _provenance_consistency
reports = [
{"c2pa_provenance": {"provenance_status": "none"}},
{"c2pa_provenance": {"provenance_status": "none"}},
]
result = _provenance_consistency(reports)
assert result["consistency"] == "consistent_no_credentials"
assert result["images_with_credentials"] == 0
def test_provenance_consistency_mixed():
from backend.services.batch_processor import _provenance_consistency
reports = [
{"c2pa_provenance": {"provenance_status": "verified"}},
{"c2pa_provenance": {"provenance_status": "none"}},
]
result = _provenance_consistency(reports)
assert result["consistency"] == "inconsistent"
def test_batch_rejects_oversized():
from backend.services.batch_processor import process_batch, MAX_BATCH_SIZE
images = [{"filename": f"img_{i}.jpg", "data": b"x"} for i in range(MAX_BATCH_SIZE + 1)]
result = process_batch(images)
assert "error" in result
assert result["received"] > MAX_BATCH_SIZE
def test_batch_processes_multiple_images():
from backend.services.batch_processor import process_batch
images = _make_batch_images(2)
result = process_batch(images)
assert result["status"] == "complete"
assert result["processed"] == 2
assert "statistics" in result
assert "risk_ranking" in result
assert "duplicate_pairs" in result
assert "provenance_consistency" in result
assert len(result["individual_reports"]) == 2
def test_batch_statistics_values_bounded():
from backend.services.batch_processor import process_batch
result = process_batch(_make_batch_images(2))
stats = result["statistics"]
assert 0.0 <= stats["mean_ai_probability"] <= 1.0
assert 0.0 <= stats["max_ai_probability"] <= 1.0
assert stats["total_images"] == 2
assert stats["batch_verdict"] in {"high_risk", "mixed", "likely_authentic"}
def test_batch_individual_reports_have_evidence_id():
from backend.services.batch_processor import process_batch
result = process_batch(_make_batch_images(2))
for item in result["individual_reports"]:
assert "evidence_id" in item
assert item["status"] == "success"
def test_batch_api_endpoint(client):
imgs = [_make_jpeg(seed=i) for i in range(3)]
files = [("files", (f"img_{i}.jpg", img, "image/jpeg"))
for i, img in enumerate(imgs)]
response = client.post("/api/v1/analyze/batch", files=files)
assert response.status_code == 200
data = response.json()
assert "statistics" in data
assert "risk_ranking" in data
assert data["processed"] == 3
def test_batch_api_rejects_too_many(client):
from backend.services.batch_processor import MAX_BATCH_SIZE
imgs = [_make_jpeg(seed=i) for i in range(MAX_BATCH_SIZE + 1)]
files = [("files", (f"img_{i}.jpg", img, "image/jpeg"))
for i, img in enumerate(imgs)]
response = client.post("/api/v1/analyze/batch", files=files)
assert response.status_code == 413
def test_batch_api_rejects_non_image(client):
files = [("files", ("test.txt", b"text", "text/plain"))]
response = client.post("/api/v1/analyze/batch", files=files)
# Accept 415 (unsupported type) or 429 (rate limit hit in CI after other batch tests)
assert response.status_code in (415, 429)