"""Comprehensive unit tests for app/tasks/embed_metadata_into_pdf.py module.""" from unittest.mock import MagicMock, mock_open, patch import pytest from app.tasks.embed_metadata_into_pdf import embed_metadata_into_pdf, persist_metadata @pytest.mark.unit class TestPersistMetadata: """Tests for persist_metadata function.""" @patch("builtins.open", new_callable=mock_open) def test_saves_metadata_to_json_file(self, mock_file): """Test metadata is saved to JSON file with correct path.""" metadata = { "filename": "test_document.pdf", "document_type": "Invoice", "tags": ["test", "invoice"], } result = persist_metadata(metadata, "/workdir/processed/MyFile.pdf") assert result == "/workdir/processed/MyFile.json" mock_file.assert_called_once_with("/workdir/processed/MyFile.json", "w", encoding="utf-8") @patch("builtins.open", new_callable=mock_open) @patch("app.tasks.embed_metadata_into_pdf.json.dump") def test_augments_metadata_with_file_paths(self, mock_json_dump, mock_file): """Test metadata is augmented with file path references.""" metadata = {"filename": "test.pdf"} persist_metadata( metadata, "/workdir/processed/test.pdf", original_file_path="/workdir/original/file.pdf", processed_file_path="/workdir/processed/test.pdf", ) # Verify json.dump was called with augmented metadata call_args = mock_json_dump.call_args augmented_metadata = call_args[0][0] assert augmented_metadata["original_file_path"] == "/workdir/original/file.pdf" assert augmented_metadata["processed_file_path"] == "/workdir/processed/test.pdf" assert augmented_metadata["filename"] == "test.pdf" @patch("builtins.open", new_callable=mock_open) @patch("app.tasks.embed_metadata_into_pdf.json.dump") def test_handles_metadata_without_optional_paths(self, mock_json_dump, mock_file): """Test metadata persistence works without optional file paths.""" metadata = {"filename": "test.pdf"} persist_metadata(metadata, "/workdir/processed/test.pdf") call_args = mock_json_dump.call_args augmented_metadata = call_args[0][0] assert "original_file_path" not in augmented_metadata assert "processed_file_path" not in augmented_metadata @pytest.mark.unit class TestEmbedMetadataIntoPdf: """Tests for embed_metadata_into_pdf Celery task.""" @patch("app.tasks.embed_metadata_into_pdf.finalize_document_storage") @patch("app.tasks.embed_metadata_into_pdf.persist_metadata") @patch("app.tasks.embed_metadata_into_pdf.shutil.move") @patch("app.tasks.embed_metadata_into_pdf.os.makedirs") @patch("app.tasks.embed_metadata_into_pdf.get_unique_filepath_with_counter") @patch("app.tasks.embed_metadata_into_pdf.sanitize_filename") @patch("app.tasks.embed_metadata_into_pdf.SessionLocal") @patch("app.tasks.embed_metadata_into_pdf.log_task_progress") @patch("builtins.open", new_callable=mock_open, read_data=b"%PDF-1.4 content") @patch("app.tasks.embed_metadata_into_pdf.pypdf.PdfReader") @patch("app.tasks.embed_metadata_into_pdf.pypdf.PdfWriter") def test_successful_metadata_embedding( self, mock_pdf_writer_class, mock_pdf_reader_class, mock_file, mock_log_progress, mock_session_local, mock_sanitize, mock_unique_path, mock_makedirs, mock_move, mock_persist, mock_finalize, ): """Test successful embedding of metadata into PDF.""" # Mock PDF reader/writer mock_reader = MagicMock() mock_reader.pages = [MagicMock(), MagicMock()] mock_pdf_reader_class.return_value = mock_reader mock_writer = MagicMock() mock_pdf_writer_class.return_value = mock_writer # Mock database session mock_db = MagicMock() mock_session_local.return_value.__enter__.return_value = mock_db mock_file_record = MagicMock() mock_file_record.id = 123 mock_file_record.original_file_path = "/workdir/original/file.pdf" mock_db.query.return_value.filter_by.return_value.first.return_value = mock_file_record # Mock other dependencies mock_sanitize.return_value = "2024-01-15_Invoice" mock_unique_path.return_value = "/workdir/processed/2024-01-15_Invoice.pdf" mock_persist.return_value = "/workdir/processed/2024-01-15_Invoice.json" # Mock tempfile with patch("app.tasks.embed_metadata_into_pdf.tempfile.NamedTemporaryFile") as mock_tempfile: mock_temp = MagicMock() mock_temp.name = "/tmp/processed_123.pdf" mock_tempfile.return_value = mock_temp with patch("app.tasks.embed_metadata_into_pdf.os.path.exists", return_value=True): with patch("app.tasks.embed_metadata_into_pdf.shutil.copy"): with patch("app.tasks.embed_metadata_into_pdf.os.remove"): with patch("app.tasks.embed_metadata_into_pdf.settings") as mock_settings: mock_settings.workdir = "/workdir" embed_metadata_into_pdf.request.id = "test-task-id" metadata = { "filename": "2024-01-15_Invoice.pdf", "absender": "Amazon", "document_type": "Invoice", "tags": ["invoice", "amazon"], } result = embed_metadata_into_pdf.__wrapped__( "/workdir/tmp/test.pdf", "Sample text", metadata, file_id=123 ) # Verify PDF metadata was set mock_writer.add_metadata.assert_called_once() metadata_call = mock_writer.add_metadata.call_args[0][0] assert metadata_call["/Title"] == "2024-01-15_Invoice.pdf" assert metadata_call["/Author"] == "Amazon" assert metadata_call["/Subject"] == "Invoice" assert "invoice" in metadata_call["/Keywords"] assert "amazon" in metadata_call["/Keywords"] # Verify file was moved mock_move.assert_called_once() # Verify finalize task was queued mock_finalize.delay.assert_called_once() # Verify result assert result["file"] == "/workdir/processed/2024-01-15_Invoice.pdf" assert result["metadata_file"] == "/workdir/processed/2024-01-15_Invoice.json" assert result["status"] == "Metadata embedded" @patch("app.tasks.embed_metadata_into_pdf.log_task_progress") def test_handles_missing_file(self, mock_log_progress): """Test handling of missing file.""" with patch("app.tasks.embed_metadata_into_pdf.os.path.exists", return_value=False): with patch("app.tasks.embed_metadata_into_pdf.settings") as mock_settings: mock_settings.workdir = "/workdir" embed_metadata_into_pdf.request.id = "test-task-id" result = embed_metadata_into_pdf.__wrapped__( "/nonexistent/file.pdf", "text", {"filename": "test.pdf"}, file_id=123 ) assert result == {"error": "File not found"} # Verify failure was logged failure_calls = [call for call in mock_log_progress.call_args_list if "failure" in str(call)] assert len(failure_calls) > 0 @patch("app.tasks.embed_metadata_into_pdf.finalize_document_storage") @patch("app.tasks.embed_metadata_into_pdf.persist_metadata") @patch("app.tasks.embed_metadata_into_pdf.shutil.move") @patch("app.tasks.embed_metadata_into_pdf.os.makedirs") @patch("app.tasks.embed_metadata_into_pdf.get_unique_filepath_with_counter") @patch("app.tasks.embed_metadata_into_pdf.sanitize_filename") @patch("app.tasks.embed_metadata_into_pdf.SessionLocal") @patch("app.tasks.embed_metadata_into_pdf.log_task_progress") def test_retrieves_file_id_from_database( self, mock_log_progress, mock_session_local, mock_sanitize, mock_unique_path, mock_makedirs, mock_move, mock_persist, mock_finalize, ): """Test file_id retrieval from database when not provided.""" # Mock database session mock_db = MagicMock() mock_session_local.return_value.__enter__.return_value = mock_db mock_file_record = MagicMock() mock_file_record.id = 456 mock_db.query.return_value.filter_by.return_value.first.return_value = mock_file_record mock_sanitize.return_value = "test" mock_unique_path.return_value = "/workdir/processed/test.pdf" mock_persist.return_value = "/workdir/processed/test.json" with patch("app.tasks.embed_metadata_into_pdf.os.path.exists", return_value=True): with patch("app.tasks.embed_metadata_into_pdf.shutil.copy"): with patch("app.tasks.embed_metadata_into_pdf.os.remove"): with patch("app.tasks.embed_metadata_into_pdf.settings") as mock_settings: with patch("builtins.open", new_callable=mock_open, read_data=b"%PDF-1.4"): with patch("app.tasks.embed_metadata_into_pdf.pypdf.PdfReader"): with patch("app.tasks.embed_metadata_into_pdf.pypdf.PdfWriter"): with patch("app.tasks.embed_metadata_into_pdf.tempfile.NamedTemporaryFile"): mock_settings.workdir = "/workdir" embed_metadata_into_pdf.request.id = "test-task-id" result = embed_metadata_into_pdf.__wrapped__( "/workdir/tmp/test.pdf", "text", {"filename": "test.pdf"} ) # Verify database was queried mock_db.query.assert_called() @patch("app.tasks.embed_metadata_into_pdf.log_task_progress") @patch("app.tasks.embed_metadata_into_pdf.SessionLocal") @patch("builtins.open", new_callable=mock_open, read_data=b"%PDF-1.4") @patch("app.tasks.embed_metadata_into_pdf.pypdf.PdfReader") def test_handles_pdf_processing_exception(self, mock_pdf_reader, mock_file, mock_session_local, mock_log_progress): """Test handling of PDF processing exceptions.""" mock_pdf_reader.side_effect = Exception("Invalid PDF structure") mock_db = MagicMock() mock_session_local.return_value.__enter__.return_value = mock_db mock_db.query.return_value.filter_by.return_value.first.return_value = None with patch("app.tasks.embed_metadata_into_pdf.os.path.exists", return_value=True): with patch("app.tasks.embed_metadata_into_pdf.shutil.copy"): with patch("app.tasks.embed_metadata_into_pdf.os.remove"): with patch("app.tasks.embed_metadata_into_pdf.tempfile.NamedTemporaryFile") as mock_tempfile: mock_temp = MagicMock() mock_temp.name = "/tmp/temp.pdf" mock_tempfile.return_value = mock_temp with patch("app.tasks.embed_metadata_into_pdf.settings") as mock_settings: mock_settings.workdir = "/workdir" embed_metadata_into_pdf.request.id = "test-task-id" result = embed_metadata_into_pdf.__wrapped__( "/workdir/tmp/test.pdf", "text", {"filename": "test.pdf"}, file_id=789 ) assert "error" in result # Verify failure was logged failure_calls = [ call for call in mock_log_progress.call_args_list if "failure" in str(call) ] assert len(failure_calls) > 0 @patch("app.tasks.embed_metadata_into_pdf.finalize_document_storage") @patch("app.tasks.embed_metadata_into_pdf.persist_metadata") @patch("app.tasks.embed_metadata_into_pdf.shutil.move") @patch("app.tasks.embed_metadata_into_pdf.os.makedirs") @patch("app.tasks.embed_metadata_into_pdf.get_unique_filepath_with_counter") @patch("app.tasks.embed_metadata_into_pdf.sanitize_filename") @patch("app.tasks.embed_metadata_into_pdf.SessionLocal") @patch("app.tasks.embed_metadata_into_pdf.log_task_progress") @patch("builtins.open", new_callable=mock_open, read_data=b"%PDF-1.4") @patch("app.tasks.embed_metadata_into_pdf.pypdf.PdfReader") @patch("app.tasks.embed_metadata_into_pdf.pypdf.PdfWriter") def test_sanitizes_malicious_filename( self, mock_pdf_writer_class, mock_pdf_reader_class, mock_file, mock_log_progress, mock_session_local, mock_sanitize, mock_unique_path, mock_makedirs, mock_move, mock_persist, mock_finalize, ): """Test filename sanitization to prevent path traversal.""" # Mock PDF reader/writer mock_reader = MagicMock() mock_reader.pages = [MagicMock()] mock_pdf_reader_class.return_value = mock_reader mock_pdf_writer_class.return_value = MagicMock() # Mock database mock_db = MagicMock() mock_session_local.return_value.__enter__.return_value = mock_db mock_db.query.return_value.filter_by.return_value.first.return_value = None # Sanitize should remove dangerous characters mock_sanitize.return_value = "safe_filename" mock_unique_path.return_value = "/workdir/processed/safe_filename.pdf" mock_persist.return_value = "/workdir/processed/safe_filename.json" with patch("app.tasks.embed_metadata_into_pdf.tempfile.NamedTemporaryFile") as mock_tempfile: mock_temp = MagicMock() mock_temp.name = "/tmp/processed.pdf" mock_tempfile.return_value = mock_temp with patch("app.tasks.embed_metadata_into_pdf.os.path.exists", return_value=True): with patch("app.tasks.embed_metadata_into_pdf.shutil.copy"): with patch("app.tasks.embed_metadata_into_pdf.os.remove"): with patch("app.tasks.embed_metadata_into_pdf.settings") as mock_settings: mock_settings.workdir = "/workdir" embed_metadata_into_pdf.request.id = "test-task-id" # Try to embed metadata with malicious filename metadata = {"filename": "../../../etc/passwd"} result = embed_metadata_into_pdf.__wrapped__( "/workdir/tmp/test.pdf", "text", metadata, file_id=111 ) # Verify sanitize_filename was called mock_sanitize.assert_called_once() @patch("app.tasks.embed_metadata_into_pdf.finalize_document_storage") @patch("app.tasks.embed_metadata_into_pdf.persist_metadata") @patch("app.tasks.embed_metadata_into_pdf.shutil.move") @patch("app.tasks.embed_metadata_into_pdf.os.makedirs") @patch("app.tasks.embed_metadata_into_pdf.get_unique_filepath_with_counter") @patch("app.tasks.embed_metadata_into_pdf.sanitize_filename") @patch("app.tasks.embed_metadata_into_pdf.SessionLocal") @patch("app.tasks.embed_metadata_into_pdf.log_task_progress") @patch("builtins.open", new_callable=mock_open, read_data=b"%PDF-1.4") @patch("app.tasks.embed_metadata_into_pdf.pypdf.PdfReader") @patch("app.tasks.embed_metadata_into_pdf.pypdf.PdfWriter") def test_handles_missing_metadata_fields( self, mock_pdf_writer_class, mock_pdf_reader_class, mock_file, mock_log_progress, mock_session_local, mock_sanitize, mock_unique_path, mock_makedirs, mock_move, mock_persist, mock_finalize, ): """Test handling of metadata with missing fields.""" mock_reader = MagicMock() mock_reader.pages = [MagicMock()] mock_pdf_reader_class.return_value = mock_reader mock_writer = MagicMock() mock_pdf_writer_class.return_value = mock_writer mock_db = MagicMock() mock_session_local.return_value.__enter__.return_value = mock_db mock_db.query.return_value.filter_by.return_value.first.return_value = None mock_sanitize.return_value = "test" mock_unique_path.return_value = "/workdir/processed/test.pdf" mock_persist.return_value = "/workdir/processed/test.json" with patch("app.tasks.embed_metadata_into_pdf.tempfile.NamedTemporaryFile") as mock_tempfile: mock_temp = MagicMock() mock_temp.name = "/tmp/temp.pdf" mock_tempfile.return_value = mock_temp with patch("app.tasks.embed_metadata_into_pdf.os.path.exists", return_value=True): with patch("app.tasks.embed_metadata_into_pdf.shutil.copy"): with patch("app.tasks.embed_metadata_into_pdf.os.remove"): with patch("app.tasks.embed_metadata_into_pdf.settings") as mock_settings: mock_settings.workdir = "/workdir" embed_metadata_into_pdf.request.id = "test-task-id" # Metadata with missing fields metadata = {} result = embed_metadata_into_pdf.__wrapped__( "/workdir/tmp/test.pdf", "text", metadata, file_id=222 ) # Verify PDF metadata was set with defaults mock_writer.add_metadata.assert_called_once() metadata_call = mock_writer.add_metadata.call_args[0][0] assert metadata_call["/Title"] == "Unknown Document" assert metadata_call["/Author"] == "Unknown" assert metadata_call["/Subject"] == "Unknown" assert metadata_call["/Keywords"] == "" @patch("app.tasks.embed_metadata_into_pdf.finalize_document_storage") @patch("app.tasks.embed_metadata_into_pdf.persist_metadata") @patch("app.tasks.embed_metadata_into_pdf.shutil.move") @patch("app.tasks.embed_metadata_into_pdf.os.makedirs") @patch("app.tasks.embed_metadata_into_pdf.get_unique_filepath_with_counter") @patch("app.tasks.embed_metadata_into_pdf.sanitize_filename") @patch("app.tasks.embed_metadata_into_pdf.SessionLocal") @patch("app.tasks.embed_metadata_into_pdf.log_task_progress") @patch("builtins.open", new_callable=mock_open, read_data=b"%PDF-1.4") @patch("app.tasks.embed_metadata_into_pdf.pypdf.PdfReader") @patch("app.tasks.embed_metadata_into_pdf.pypdf.PdfWriter") def test_deletes_original_file_from_tmp( self, mock_pdf_writer_class, mock_pdf_reader_class, mock_file, mock_log_progress, mock_session_local, mock_sanitize, mock_unique_path, mock_makedirs, mock_move, mock_persist, mock_finalize, ): """Test that original file in tmp directory is deleted after processing.""" mock_reader = MagicMock() mock_reader.pages = [MagicMock()] mock_pdf_reader_class.return_value = mock_reader mock_pdf_writer_class.return_value = MagicMock() mock_db = MagicMock() mock_session_local.return_value.__enter__.return_value = mock_db mock_db.query.return_value.filter_by.return_value.first.return_value = None mock_sanitize.return_value = "test" mock_unique_path.return_value = "/workdir/processed/test.pdf" mock_persist.return_value = "/workdir/processed/test.json" with patch("app.tasks.embed_metadata_into_pdf.tempfile.NamedTemporaryFile") as mock_tempfile: mock_temp = MagicMock() mock_temp.name = "/tmp/temp.pdf" mock_tempfile.return_value = mock_temp with patch("app.tasks.embed_metadata_into_pdf.os.path.exists", return_value=True): with patch("app.tasks.embed_metadata_into_pdf.shutil.copy"): with patch("app.tasks.embed_metadata_into_pdf.Path") as mock_path_class: # Mock Path for deletion logic mock_original_path = MagicMock() mock_resolved_path = MagicMock() mock_resolved_path.exists.return_value = True mock_resolved_path.is_relative_to.return_value = True mock_original_path.resolve.return_value = mock_resolved_path mock_workdir_path = MagicMock() mock_path_class.side_effect = [mock_workdir_path, mock_original_path] with patch("app.tasks.embed_metadata_into_pdf.os.remove"): with patch("app.tasks.embed_metadata_into_pdf.settings") as mock_settings: mock_settings.workdir = "/workdir" embed_metadata_into_pdf.request.id = "test-task-id" result = embed_metadata_into_pdf.__wrapped__( "/workdir/tmp/test.pdf", "text", {"filename": "test.pdf"}, file_id=333 ) # Verify unlink (delete) was called on the resolved path mock_resolved_path.unlink.assert_called_once()