7a0da7932b
OSV-Scanner (Scheduled) / scan-scheduled (push) Failing after 0s
Create Release / test-gate (push) Has been cancelled
Create Release / release-gate (push) Has been cancelled
Create Release / ci-gate (push) Has been cancelled
Create Release / version-check (push) Has been cancelled
Create Release / e2e-test-gate (push) Has been cancelled
Create Release / responsive-test-gate (push) Has been cancelled
Create Release / compat-test-gate (push) Has been cancelled
Create Release / compose-integration-gate (push) Has been cancelled
Create Release / vulture-gate (push) Has been cancelled
Create Release / build (push) Has been cancelled
Create Release / provenance (push) Has been cancelled
Create Release / prerelease-docker (push) Has been cancelled
Create Release / publish-docker (push) Has been cancelled
Create Release / create-release (push) Has been cancelled
Create Release / cleanup-changelog (push) Has been cancelled
Create Release / trigger-pypi (push) Has been cancelled
Create Release / monitor-pypi (push) Has been cancelled
Create Release / Clean up orphan prerelease tags and signatures (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-form] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-metrics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-workflow] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-core] (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [history-news] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [library] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [link-analytics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-core] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-lifecycle] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [error-benchmark] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) (push) Has been cancelled
Docker Tests (Consolidated) / Accessibility Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Unit Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Example Tests (push) Has been cancelled
Docker Tests (Consolidated) / Production Image Smoke Test (push) Has been cancelled
Docker Tests (Consolidated) / Infrastructure Tests (push) Has been cancelled
OSSF Scorecard / OSSF Security Scorecard Analysis (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [mobile] (push) Has been cancelled
Backwards Compatibility / Verify Encryption Constants (push) Has been cancelled
Backwards Compatibility / PyPI Version Compatibility (push) Has been cancelled
Backwards Compatibility / Database Migration Tests (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
Docker Tests (Consolidated) / detect-changes (push) Has been cancelled
Docker Tests (Consolidated) / Build Test Image (push) Has been cancelled
Docker Tests (Consolidated) / All Pytest Tests + Coverage (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [accessibility] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [api-crud] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-login] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-register] (push) Has been cancelled
489 lines
17 KiB
Python
489 lines
17 KiB
Python
"""
|
|
Comprehensive tests for PDFExtractionService.
|
|
Tests PDF text extraction, metadata extraction, and batch processing.
|
|
"""
|
|
|
|
from unittest.mock import Mock, patch, MagicMock
|
|
|
|
|
|
class TestPDFExtractionServiceExtractTextAndMetadata:
|
|
"""Tests for extract_text_and_metadata method."""
|
|
|
|
def test_returns_dict_with_required_keys(self, valid_pdf_bytes):
|
|
"""Test that extraction returns dict with all required keys."""
|
|
from local_deep_research.web.services.pdf_extraction_service import (
|
|
PDFExtractionService,
|
|
)
|
|
|
|
result = PDFExtractionService.extract_text_and_metadata(
|
|
valid_pdf_bytes, "test.pdf"
|
|
)
|
|
|
|
assert "text" in result
|
|
assert "pages" in result
|
|
assert "size" in result
|
|
assert "filename" in result
|
|
assert "success" in result
|
|
assert "error" in result
|
|
|
|
def test_extracts_text_from_valid_pdf(self, valid_pdf_bytes):
|
|
"""Test text extraction from valid PDF."""
|
|
from local_deep_research.web.services.pdf_extraction_service import (
|
|
PDFExtractionService,
|
|
)
|
|
|
|
result = PDFExtractionService.extract_text_and_metadata(
|
|
valid_pdf_bytes, "test.pdf"
|
|
)
|
|
|
|
# The minimal PDF contains "Hello World"
|
|
assert "Hello World" in result["text"]
|
|
assert result["success"] is True
|
|
|
|
def test_returns_page_count(self, valid_pdf_bytes):
|
|
"""Test that page count is returned."""
|
|
from local_deep_research.web.services.pdf_extraction_service import (
|
|
PDFExtractionService,
|
|
)
|
|
|
|
result = PDFExtractionService.extract_text_and_metadata(
|
|
valid_pdf_bytes, "test.pdf"
|
|
)
|
|
|
|
assert result["pages"] == 1
|
|
|
|
def test_returns_file_size(self, valid_pdf_bytes):
|
|
"""Test that file size is returned."""
|
|
from local_deep_research.web.services.pdf_extraction_service import (
|
|
PDFExtractionService,
|
|
)
|
|
|
|
result = PDFExtractionService.extract_text_and_metadata(
|
|
valid_pdf_bytes, "test.pdf"
|
|
)
|
|
|
|
assert result["size"] == len(valid_pdf_bytes)
|
|
|
|
def test_returns_filename(self, valid_pdf_bytes):
|
|
"""Test that filename is returned."""
|
|
from local_deep_research.web.services.pdf_extraction_service import (
|
|
PDFExtractionService,
|
|
)
|
|
|
|
result = PDFExtractionService.extract_text_and_metadata(
|
|
valid_pdf_bytes, "my_document.pdf"
|
|
)
|
|
|
|
assert result["filename"] == "my_document.pdf"
|
|
|
|
def test_handles_invalid_pdf(self, invalid_pdf_bytes):
|
|
"""Test handling of invalid/corrupted PDF."""
|
|
from local_deep_research.web.services.pdf_extraction_service import (
|
|
PDFExtractionService,
|
|
)
|
|
|
|
result = PDFExtractionService.extract_text_and_metadata(
|
|
invalid_pdf_bytes, "corrupt.pdf"
|
|
)
|
|
|
|
assert result["success"] is False
|
|
assert result["error"] is not None
|
|
assert "Failed to extract" in result["error"]
|
|
|
|
def test_handles_empty_file(self, empty_file_bytes):
|
|
"""Test handling of empty file."""
|
|
from local_deep_research.web.services.pdf_extraction_service import (
|
|
PDFExtractionService,
|
|
)
|
|
|
|
result = PDFExtractionService.extract_text_and_metadata(
|
|
empty_file_bytes, "empty.pdf"
|
|
)
|
|
|
|
assert result["success"] is False
|
|
assert result["error"] is not None
|
|
|
|
def test_handles_pdf_with_no_text(self):
|
|
"""Test handling of PDF with no extractable text."""
|
|
from local_deep_research.web.services.pdf_extraction_service import (
|
|
PDFExtractionService,
|
|
)
|
|
|
|
# Mock pdfplumber to return empty text
|
|
mock_page = Mock()
|
|
mock_page.extract_text.return_value = ""
|
|
|
|
mock_pdf = MagicMock()
|
|
mock_pdf.__enter__.return_value = mock_pdf
|
|
mock_pdf.__exit__.return_value = None
|
|
mock_pdf.pages = [mock_page]
|
|
|
|
with patch(
|
|
"local_deep_research.web.services.pdf_extraction_service.pdfplumber"
|
|
) as mock_pdfplumber:
|
|
mock_pdfplumber.open.return_value = mock_pdf
|
|
|
|
result = PDFExtractionService.extract_text_and_metadata(
|
|
b"fake pdf content", "empty_text.pdf"
|
|
)
|
|
|
|
assert result["success"] is False
|
|
assert "No extractable text found" in result["error"]
|
|
|
|
def test_logs_warning_for_no_text(self):
|
|
"""Test that warning is logged when no text found."""
|
|
from local_deep_research.web.services.pdf_extraction_service import (
|
|
PDFExtractionService,
|
|
)
|
|
|
|
mock_page = Mock()
|
|
mock_page.extract_text.return_value = ""
|
|
|
|
mock_pdf = MagicMock()
|
|
mock_pdf.__enter__.return_value = mock_pdf
|
|
mock_pdf.__exit__.return_value = None
|
|
mock_pdf.pages = [mock_page]
|
|
|
|
with patch(
|
|
"local_deep_research.web.services.pdf_extraction_service.pdfplumber"
|
|
) as mock_pdfplumber:
|
|
mock_pdfplumber.open.return_value = mock_pdf
|
|
|
|
with patch(
|
|
"local_deep_research.web.services.pdf_extraction_service.logger"
|
|
) as mock_logger:
|
|
PDFExtractionService.extract_text_and_metadata(
|
|
b"fake pdf", "no_text.pdf"
|
|
)
|
|
|
|
mock_logger.warning.assert_called_once()
|
|
assert (
|
|
"No extractable text" in mock_logger.warning.call_args[0][0]
|
|
)
|
|
|
|
def test_logs_success_for_extraction(self, valid_pdf_bytes):
|
|
"""Test that success is logged on extraction."""
|
|
from local_deep_research.web.services.pdf_extraction_service import (
|
|
PDFExtractionService,
|
|
)
|
|
|
|
with patch(
|
|
"local_deep_research.web.services.pdf_extraction_service.logger"
|
|
) as mock_logger:
|
|
result = PDFExtractionService.extract_text_and_metadata(
|
|
valid_pdf_bytes, "test.pdf"
|
|
)
|
|
|
|
if result["success"]:
|
|
mock_logger.info.assert_called()
|
|
|
|
def test_logs_exception_on_error(self, invalid_pdf_bytes):
|
|
"""Test that exceptions are logged."""
|
|
from local_deep_research.web.services.pdf_extraction_service import (
|
|
PDFExtractionService,
|
|
)
|
|
|
|
with patch(
|
|
"local_deep_research.web.services.pdf_extraction_service.logger"
|
|
) as mock_logger:
|
|
PDFExtractionService.extract_text_and_metadata(
|
|
invalid_pdf_bytes, "corrupt.pdf"
|
|
)
|
|
|
|
mock_logger.exception.assert_called()
|
|
|
|
def test_returns_generic_error_message(self, invalid_pdf_bytes):
|
|
"""Test that generic error message is returned (no internal details)."""
|
|
from local_deep_research.web.services.pdf_extraction_service import (
|
|
PDFExtractionService,
|
|
)
|
|
|
|
result = PDFExtractionService.extract_text_and_metadata(
|
|
invalid_pdf_bytes, "corrupt.pdf"
|
|
)
|
|
|
|
# Should not expose internal exception details
|
|
assert "Failed to extract text from PDF" in result["error"]
|
|
|
|
|
|
class TestPDFExtractionServiceExtractBatch:
|
|
"""Tests for extract_batch method."""
|
|
|
|
def test_returns_dict_with_required_keys(self, valid_pdf_bytes):
|
|
"""Test that batch returns dict with all required keys."""
|
|
from local_deep_research.web.services.pdf_extraction_service import (
|
|
PDFExtractionService,
|
|
)
|
|
|
|
files_data = [{"content": valid_pdf_bytes, "filename": "test.pdf"}]
|
|
|
|
result = PDFExtractionService.extract_batch(files_data)
|
|
|
|
assert "results" in result
|
|
assert "total_files" in result
|
|
assert "successful" in result
|
|
assert "failed" in result
|
|
assert "errors" in result
|
|
|
|
def test_processes_multiple_files(self, valid_pdf_bytes):
|
|
"""Test processing multiple PDF files."""
|
|
from local_deep_research.web.services.pdf_extraction_service import (
|
|
PDFExtractionService,
|
|
)
|
|
|
|
files_data = [
|
|
{"content": valid_pdf_bytes, "filename": "test1.pdf"},
|
|
{"content": valid_pdf_bytes, "filename": "test2.pdf"},
|
|
{"content": valid_pdf_bytes, "filename": "test3.pdf"},
|
|
]
|
|
|
|
result = PDFExtractionService.extract_batch(files_data)
|
|
|
|
assert result["total_files"] == 3
|
|
assert len(result["results"]) == 3
|
|
|
|
def test_counts_successful_extractions(self, valid_pdf_bytes):
|
|
"""Test counting of successful extractions."""
|
|
from local_deep_research.web.services.pdf_extraction_service import (
|
|
PDFExtractionService,
|
|
)
|
|
|
|
files_data = [
|
|
{"content": valid_pdf_bytes, "filename": "test1.pdf"},
|
|
{"content": valid_pdf_bytes, "filename": "test2.pdf"},
|
|
]
|
|
|
|
result = PDFExtractionService.extract_batch(files_data)
|
|
|
|
assert result["successful"] == 2
|
|
assert result["failed"] == 0
|
|
|
|
def test_counts_failed_extractions(
|
|
self, valid_pdf_bytes, invalid_pdf_bytes
|
|
):
|
|
"""Test counting of failed extractions."""
|
|
from local_deep_research.web.services.pdf_extraction_service import (
|
|
PDFExtractionService,
|
|
)
|
|
|
|
files_data = [
|
|
{"content": valid_pdf_bytes, "filename": "valid.pdf"},
|
|
{"content": invalid_pdf_bytes, "filename": "invalid.pdf"},
|
|
]
|
|
|
|
result = PDFExtractionService.extract_batch(files_data)
|
|
|
|
assert result["successful"] == 1
|
|
assert result["failed"] == 1
|
|
|
|
def test_aggregates_errors(self, invalid_pdf_bytes):
|
|
"""Test error aggregation in batch mode."""
|
|
from local_deep_research.web.services.pdf_extraction_service import (
|
|
PDFExtractionService,
|
|
)
|
|
|
|
files_data = [
|
|
{"content": invalid_pdf_bytes, "filename": "bad1.pdf"},
|
|
{"content": invalid_pdf_bytes, "filename": "bad2.pdf"},
|
|
]
|
|
|
|
result = PDFExtractionService.extract_batch(files_data)
|
|
|
|
assert len(result["errors"]) == 2
|
|
assert any("bad1.pdf" in err for err in result["errors"])
|
|
assert any("bad2.pdf" in err for err in result["errors"])
|
|
|
|
def test_handles_empty_batch(self):
|
|
"""Test handling of empty file list."""
|
|
from local_deep_research.web.services.pdf_extraction_service import (
|
|
PDFExtractionService,
|
|
)
|
|
|
|
result = PDFExtractionService.extract_batch([])
|
|
|
|
assert result["total_files"] == 0
|
|
assert result["successful"] == 0
|
|
assert result["failed"] == 0
|
|
assert result["results"] == []
|
|
assert result["errors"] == []
|
|
|
|
def test_preserves_order_of_results(self, valid_pdf_bytes):
|
|
"""Test that results preserve the order of input files."""
|
|
from local_deep_research.web.services.pdf_extraction_service import (
|
|
PDFExtractionService,
|
|
)
|
|
|
|
files_data = [
|
|
{"content": valid_pdf_bytes, "filename": "first.pdf"},
|
|
{"content": valid_pdf_bytes, "filename": "second.pdf"},
|
|
{"content": valid_pdf_bytes, "filename": "third.pdf"},
|
|
]
|
|
|
|
result = PDFExtractionService.extract_batch(files_data)
|
|
|
|
assert result["results"][0]["filename"] == "first.pdf"
|
|
assert result["results"][1]["filename"] == "second.pdf"
|
|
assert result["results"][2]["filename"] == "third.pdf"
|
|
|
|
def test_each_result_has_extraction_data(self, valid_pdf_bytes):
|
|
"""Test that each result has complete extraction data."""
|
|
from local_deep_research.web.services.pdf_extraction_service import (
|
|
PDFExtractionService,
|
|
)
|
|
|
|
files_data = [{"content": valid_pdf_bytes, "filename": "test.pdf"}]
|
|
|
|
result = PDFExtractionService.extract_batch(files_data)
|
|
|
|
file_result = result["results"][0]
|
|
assert "text" in file_result
|
|
assert "pages" in file_result
|
|
assert "size" in file_result
|
|
assert "filename" in file_result
|
|
assert "success" in file_result
|
|
|
|
|
|
class TestPDFExtractionServiceSingleton:
|
|
"""Tests for get_pdf_extraction_service singleton."""
|
|
|
|
def test_get_service_returns_instance(self):
|
|
"""Test that get_pdf_extraction_service returns an instance."""
|
|
from local_deep_research.web.services.pdf_extraction_service import (
|
|
get_pdf_extraction_service,
|
|
PDFExtractionService,
|
|
)
|
|
|
|
service = get_pdf_extraction_service()
|
|
|
|
assert isinstance(service, PDFExtractionService)
|
|
|
|
def test_get_service_returns_same_instance(self):
|
|
"""Test that get_pdf_extraction_service returns same instance."""
|
|
from local_deep_research.web.services.pdf_extraction_service import (
|
|
get_pdf_extraction_service,
|
|
)
|
|
|
|
service1 = get_pdf_extraction_service()
|
|
service2 = get_pdf_extraction_service()
|
|
|
|
assert service1 is service2
|
|
|
|
def test_singleton_can_be_reset(self):
|
|
"""Test that singleton can be reset for testing."""
|
|
import local_deep_research.web.services.pdf_extraction_service as extraction_module
|
|
|
|
# Reset singleton
|
|
extraction_module._pdf_extraction_service = None
|
|
|
|
service1 = extraction_module.get_pdf_extraction_service()
|
|
|
|
# Reset again
|
|
extraction_module._pdf_extraction_service = None
|
|
|
|
service2 = extraction_module.get_pdf_extraction_service()
|
|
|
|
# Different instances after reset
|
|
assert service1 is not service2
|
|
|
|
|
|
class TestPDFExtractionServiceMultiPage:
|
|
"""Tests for multi-page PDF extraction."""
|
|
|
|
def test_extracts_all_pages(self):
|
|
"""Test that text is extracted from all pages."""
|
|
from local_deep_research.web.services.pdf_extraction_service import (
|
|
PDFExtractionService,
|
|
)
|
|
|
|
# Mock multi-page PDF
|
|
mock_page1 = Mock()
|
|
mock_page1.extract_text.return_value = "Page 1 content"
|
|
|
|
mock_page2 = Mock()
|
|
mock_page2.extract_text.return_value = "Page 2 content"
|
|
|
|
mock_page3 = Mock()
|
|
mock_page3.extract_text.return_value = "Page 3 content"
|
|
|
|
mock_pdf = MagicMock()
|
|
mock_pdf.__enter__.return_value = mock_pdf
|
|
mock_pdf.__exit__.return_value = None
|
|
mock_pdf.pages = [mock_page1, mock_page2, mock_page3]
|
|
|
|
with patch(
|
|
"local_deep_research.web.services.pdf_extraction_service.pdfplumber"
|
|
) as mock_pdfplumber:
|
|
mock_pdfplumber.open.return_value = mock_pdf
|
|
|
|
result = PDFExtractionService.extract_text_and_metadata(
|
|
b"fake pdf", "multipage.pdf"
|
|
)
|
|
|
|
assert result["pages"] == 3
|
|
assert "Page 1 content" in result["text"]
|
|
assert "Page 2 content" in result["text"]
|
|
assert "Page 3 content" in result["text"]
|
|
|
|
def test_handles_page_with_no_text(self):
|
|
"""Test handling pages that return no text."""
|
|
from local_deep_research.web.services.pdf_extraction_service import (
|
|
PDFExtractionService,
|
|
)
|
|
|
|
mock_page1 = Mock()
|
|
mock_page1.extract_text.return_value = "Page 1 content"
|
|
|
|
mock_page2 = Mock()
|
|
mock_page2.extract_text.return_value = None # Image-only page
|
|
|
|
mock_page3 = Mock()
|
|
mock_page3.extract_text.return_value = "Page 3 content"
|
|
|
|
mock_pdf = MagicMock()
|
|
mock_pdf.__enter__.return_value = mock_pdf
|
|
mock_pdf.__exit__.return_value = None
|
|
mock_pdf.pages = [mock_page1, mock_page2, mock_page3]
|
|
|
|
with patch(
|
|
"local_deep_research.web.services.pdf_extraction_service.pdfplumber"
|
|
) as mock_pdfplumber:
|
|
mock_pdfplumber.open.return_value = mock_pdf
|
|
|
|
result = PDFExtractionService.extract_text_and_metadata(
|
|
b"fake pdf", "mixed.pdf"
|
|
)
|
|
|
|
assert result["pages"] == 3
|
|
assert "Page 1 content" in result["text"]
|
|
assert "Page 3 content" in result["text"]
|
|
assert result["success"] is True
|
|
|
|
def test_joins_pages_with_newline(self):
|
|
"""Test that pages are joined with newlines."""
|
|
from local_deep_research.web.services.pdf_extraction_service import (
|
|
PDFExtractionService,
|
|
)
|
|
|
|
mock_page1 = Mock()
|
|
mock_page1.extract_text.return_value = "First"
|
|
|
|
mock_page2 = Mock()
|
|
mock_page2.extract_text.return_value = "Second"
|
|
|
|
mock_pdf = MagicMock()
|
|
mock_pdf.__enter__.return_value = mock_pdf
|
|
mock_pdf.__exit__.return_value = None
|
|
mock_pdf.pages = [mock_page1, mock_page2]
|
|
|
|
with patch(
|
|
"local_deep_research.web.services.pdf_extraction_service.pdfplumber"
|
|
) as mock_pdfplumber:
|
|
mock_pdfplumber.open.return_value = mock_pdf
|
|
|
|
result = PDFExtractionService.extract_text_and_metadata(
|
|
b"fake pdf", "test.pdf"
|
|
)
|
|
|
|
# Text should be joined with newline
|
|
assert result["text"] == "First\nSecond"
|