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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user