""" 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"