feat: add bulk download, cloud OCR, and basic OCR quality filter

Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]
2026-03-01 13:38:42 +00:00
parent e8ed375bba
commit 705b970522
5 changed files with 595 additions and 0 deletions
+238
View File
@@ -2,6 +2,8 @@
Tests for bulk file operations (delete and reprocess).
"""
import io
import zipfile
from unittest.mock import MagicMock, patch
import pytest
@@ -293,3 +295,239 @@ class TestStatusFilter:
response = client.get("/files?status=failed")
assert response.status_code == 200
assert "failed.pdf" in response.text
@pytest.mark.integration
@pytest.mark.requires_db
class TestBulkDownload:
"""Tests for POST /api/files/bulk-download endpoint."""
def test_bulk_download_no_files_found(self, client: TestClient, db_session):
"""Test bulk download with non-existent IDs."""
response = client.post("/api/files/bulk-download", json=[99999, 99998])
assert response.status_code == 404
def test_bulk_download_files_not_on_disk(self, client: TestClient, db_session):
"""Test bulk download when files are not found on disk."""
file_record = FileRecord(
filehash="hash_dl1",
original_filename="nodisk.pdf",
local_filename="/nonexistent/nodisk.pdf",
file_size=1024,
mime_type="application/pdf",
)
db_session.add(file_record)
db_session.commit()
response = client.post("/api/files/bulk-download", json=[file_record.id])
assert response.status_code == 404
assert "None of the selected files" in response.json()["detail"]
def test_bulk_download_success(self, client: TestClient, db_session, tmp_path):
"""Test successful bulk download returns a ZIP archive."""
# Create a real file on disk
pdf_file = tmp_path / "sample.pdf"
pdf_file.write_bytes(b"PDF content")
file_record = FileRecord(
filehash="hash_dl2",
original_filename="sample.pdf",
local_filename=str(pdf_file),
processed_file_path=str(pdf_file),
file_size=len(b"PDF content"),
mime_type="application/pdf",
)
db_session.add(file_record)
db_session.commit()
response = client.post("/api/files/bulk-download", json=[file_record.id])
assert response.status_code == 200
assert response.headers["content-type"] == "application/zip"
assert "attachment" in response.headers["content-disposition"]
assert ".zip" in response.headers["content-disposition"]
def test_bulk_download_multiple_files(self, client: TestClient, db_session, tmp_path):
"""Test bulk download with multiple files produces a valid ZIP."""
ids = []
for i in range(3):
f = tmp_path / f"file{i}.pdf"
f.write_bytes(f"content {i}".encode())
rec = FileRecord(
filehash=f"hash_multi_{i}",
original_filename=f"file{i}.pdf",
local_filename=str(f),
processed_file_path=str(f),
file_size=len(f"content {i}".encode()),
mime_type="application/pdf",
)
db_session.add(rec)
db_session.commit()
ids.append(rec.id)
response = client.post("/api/files/bulk-download", json=ids)
assert response.status_code == 200
zip_data = io.BytesIO(response.content)
with zipfile.ZipFile(zip_data) as zf:
names = zf.namelist()
assert len(names) == 3
def test_bulk_download_duplicate_filenames(self, client: TestClient, db_session, tmp_path):
"""Test bulk download disambiguates duplicate filenames."""
ids = []
for i in range(2):
f = tmp_path / f"dup_{i}.pdf"
f.write_bytes(b"data")
rec = FileRecord(
filehash=f"hash_dup_{i}",
original_filename="dup.pdf", # same name
local_filename=str(f),
processed_file_path=str(f),
file_size=4,
mime_type="application/pdf",
)
db_session.add(rec)
db_session.commit()
ids.append(rec.id)
response = client.post("/api/files/bulk-download", json=ids)
assert response.status_code == 200
zip_data = io.BytesIO(response.content)
with zipfile.ZipFile(zip_data) as zf:
names = zf.namelist()
# Names must be unique
assert len(names) == len(set(names))
@pytest.mark.integration
@pytest.mark.requires_db
class TestBulkReprocessCloudOcr:
"""Tests for POST /api/files/bulk-reprocess-cloud-ocr endpoint."""
@patch("app.api.files.process_document")
def test_bulk_reprocess_cloud_ocr_success(self, mock_delay, client: TestClient, db_session, tmp_path):
"""Test bulk Cloud OCR reprocessing queues tasks."""
mock_task = MagicMock()
mock_task.id = "task-cloud-ocr-1"
mock_delay.delay.return_value = mock_task
pdf_file = tmp_path / "ocr_test.pdf"
pdf_file.write_bytes(b"PDF data")
file_record = FileRecord(
filehash="hash_ocr1",
original_filename="ocr_test.pdf",
local_filename=str(pdf_file),
file_size=8,
mime_type="application/pdf",
)
db_session.add(file_record)
db_session.commit()
response = client.post("/api/files/bulk-reprocess-cloud-ocr", json=[file_record.id])
assert response.status_code == 200
data = response.json()
assert data["status"] == "success"
assert len(data["processed_files"]) == 1
assert data["errors"] == []
# Ensure force_cloud_ocr=True was passed
call_kwargs = mock_delay.delay.call_args.kwargs
assert call_kwargs.get("force_cloud_ocr") is True
def test_bulk_reprocess_cloud_ocr_no_file_on_disk(self, client: TestClient, db_session):
"""Test Cloud OCR bulk reprocess skips files not on disk."""
file_record = FileRecord(
filehash="hash_ocr2",
original_filename="missing.pdf",
local_filename="/nonexistent/missing.pdf",
file_size=1024,
mime_type="application/pdf",
)
db_session.add(file_record)
db_session.commit()
response = client.post("/api/files/bulk-reprocess-cloud-ocr", json=[file_record.id])
assert response.status_code == 200
data = response.json()
assert data["status"] == "error"
assert len(data["errors"]) == 1
def test_bulk_reprocess_cloud_ocr_no_files_found(self, client: TestClient, db_session):
"""Test Cloud OCR bulk reprocess with non-existent IDs."""
response = client.post("/api/files/bulk-reprocess-cloud-ocr", json=[99999])
assert response.status_code == 404
@pytest.mark.integration
@pytest.mark.requires_db
class TestOcrQualityFilter:
"""Tests for the ocr_quality filter on the /files view."""
def test_ocr_quality_no_ocr_filter(self, client: TestClient, db_session):
"""Files without OCR text appear when filtering no_ocr."""
rec_no_ocr = FileRecord(
filehash="hash_noocr1",
original_filename="no_ocr.pdf",
local_filename="/tmp/no_ocr.pdf",
file_size=1024,
mime_type="application/pdf",
ocr_text=None,
)
rec_has_ocr = FileRecord(
filehash="hash_hasocr1",
original_filename="has_ocr.pdf",
local_filename="/tmp/has_ocr.pdf",
file_size=1024,
mime_type="application/pdf",
ocr_text="Some extracted text",
)
db_session.add_all([rec_no_ocr, rec_has_ocr])
db_session.commit()
response = client.get("/files?ocr_quality=no_ocr")
assert response.status_code == 200
assert "no_ocr.pdf" in response.text
assert "has_ocr.pdf" not in response.text
def test_ocr_quality_has_ocr_filter(self, client: TestClient, db_session):
"""Files with OCR text appear when filtering has_ocr."""
rec_no_ocr = FileRecord(
filehash="hash_noocr2",
original_filename="no_ocr2.pdf",
local_filename="/tmp/no_ocr2.pdf",
file_size=1024,
mime_type="application/pdf",
ocr_text=None,
)
rec_has_ocr = FileRecord(
filehash="hash_hasocr2",
original_filename="has_ocr2.pdf",
local_filename="/tmp/has_ocr2.pdf",
file_size=1024,
mime_type="application/pdf",
ocr_text="Meaningful extracted text",
)
db_session.add_all([rec_no_ocr, rec_has_ocr])
db_session.commit()
response = client.get("/files?ocr_quality=has_ocr")
assert response.status_code == 200
assert "has_ocr2.pdf" in response.text
assert "no_ocr2.pdf" not in response.text
def test_ocr_quality_no_filter(self, client: TestClient, db_session):
"""All files appear when no ocr_quality filter is applied."""
rec = FileRecord(
filehash="hash_all1",
original_filename="all_files.pdf",
local_filename="/tmp/all_files.pdf",
file_size=1024,
mime_type="application/pdf",
)
db_session.add(rec)
db_session.commit()
response = client.get("/files")
assert response.status_code == 200
assert "all_files.pdf" in response.text