Spaces:
Sleeping
Sleeping
| """ | |
| 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) | |