""" Comprehensive tests for the Paperless-ngx search engine. Tests initialization, API requests, search functionality, and document conversion. Note: These tests mock HTTP requests to avoid requiring an actual Paperless instance. """ import pytest from unittest.mock import Mock, patch @pytest.fixture def mock_requests_get(): """Mock requests.get to avoid actual HTTP calls.""" with patch( "local_deep_research.web_search_engines.engines.search_engine_paperless.requests.get" ) as mock_get: yield mock_get class TestPaperlessSearchEngineInit: """Tests for Paperless search engine initialization.""" def test_init_with_api_url_and_token(self, mock_requests_get): """Test initialization with API URL and token.""" from local_deep_research.web_search_engines.engines.search_engine_paperless import ( PaperlessSearchEngine, ) engine = PaperlessSearchEngine( api_url="http://localhost:8000", api_key="test_token" ) assert engine.api_url == "http://localhost:8000" assert engine.api_token == "test_token" assert engine.headers["Authorization"] == "Token test_token" def test_init_with_api_token_compatibility(self, mock_requests_get): """Test initialization with api_token parameter (backwards compatibility).""" from local_deep_research.web_search_engines.engines.search_engine_paperless import ( PaperlessSearchEngine, ) engine = PaperlessSearchEngine( api_url="http://localhost:8000", api_token="legacy_token" ) assert engine.api_token == "legacy_token" def test_init_strips_trailing_slash(self, mock_requests_get): """Test that trailing slash is stripped from API URL.""" from local_deep_research.web_search_engines.engines.search_engine_paperless import ( PaperlessSearchEngine, ) engine = PaperlessSearchEngine(api_url="http://localhost:8000/") assert engine.api_url == "http://localhost:8000" def test_init_default_values(self, mock_requests_get): """Test initialization with default values.""" from local_deep_research.web_search_engines.engines.search_engine_paperless import ( PaperlessSearchEngine, ) engine = PaperlessSearchEngine() assert engine.max_results == 10 assert engine.timeout == 30 assert engine.verify_ssl is True assert engine.include_content is True def test_init_with_custom_max_results(self, mock_requests_get): """Test initialization with custom max results.""" from local_deep_research.web_search_engines.engines.search_engine_paperless import ( PaperlessSearchEngine, ) engine = PaperlessSearchEngine(max_results=25) assert engine.max_results == 25 def test_init_with_custom_timeout(self, mock_requests_get): """Test initialization with custom timeout.""" from local_deep_research.web_search_engines.engines.search_engine_paperless import ( PaperlessSearchEngine, ) engine = PaperlessSearchEngine(timeout=60) assert engine.timeout == 60 def test_init_with_ssl_disabled(self, mock_requests_get): """Test initialization with SSL verification disabled.""" from local_deep_research.web_search_engines.engines.search_engine_paperless import ( PaperlessSearchEngine, ) engine = PaperlessSearchEngine(verify_ssl=False) assert engine.verify_ssl is False def test_init_with_content_disabled(self, mock_requests_get): """Test initialization with content inclusion disabled.""" from local_deep_research.web_search_engines.engines.search_engine_paperless import ( PaperlessSearchEngine, ) engine = PaperlessSearchEngine(include_content=False) assert engine.include_content is False def test_init_from_settings_snapshot(self, mock_requests_get): """Test initialization from settings snapshot.""" from local_deep_research.web_search_engines.engines.search_engine_paperless import ( PaperlessSearchEngine, ) settings = { "search.engine.web.paperless.default_params.api_url": "http://paperless.local:9000", "search.engine.web.paperless.api_key": "snapshot_token", } engine = PaperlessSearchEngine(settings_snapshot=settings) assert engine.api_url == "http://paperless.local:9000" assert engine.api_token == "snapshot_token" def test_init_with_no_token_empty_headers(self, mock_requests_get): """Test that headers are empty when no token provided.""" from local_deep_research.web_search_engines.engines.search_engine_paperless import ( PaperlessSearchEngine, ) engine = PaperlessSearchEngine(api_url="http://localhost:8000") assert "Authorization" not in engine.headers class TestPaperlessAPIRequest: """Tests for Paperless API request handling.""" @pytest.fixture def engine(self, mock_requests_get): """Create a Paperless engine.""" from local_deep_research.web_search_engines.engines.search_engine_paperless import ( PaperlessSearchEngine, ) return PaperlessSearchEngine( api_url="http://localhost:8000", api_key="test_token" ) def test_make_request_success(self, engine, mock_requests_get): """Test successful API request.""" mock_response = Mock() mock_response.json.return_value = {"results": [], "count": 0} mock_response.raise_for_status = Mock() mock_requests_get.return_value = mock_response result = engine._make_request("/api/documents/", {"query": "test"}) assert result == {"results": [], "count": 0} mock_requests_get.assert_called_once() def test_make_request_includes_auth_header(self, engine, mock_requests_get): """Test that request includes authorization header.""" mock_response = Mock() mock_response.json.return_value = {} mock_response.raise_for_status = Mock() mock_requests_get.return_value = mock_response engine._make_request("/api/documents/") call_kwargs = mock_requests_get.call_args[1] assert call_kwargs["headers"]["Authorization"] == "Token test_token" def test_make_request_handles_exception(self, engine, mock_requests_get): """Test that request exceptions are handled gracefully.""" import requests mock_requests_get.side_effect = requests.exceptions.RequestException( "Connection error" ) result = engine._make_request("/api/documents/") assert result == {} def test_make_request_uses_correct_timeout(self, engine, mock_requests_get): """Test that request uses configured timeout.""" mock_response = Mock() mock_response.json.return_value = {} mock_response.raise_for_status = Mock() mock_requests_get.return_value = mock_response engine._make_request("/api/documents/") call_kwargs = mock_requests_get.call_args[1] assert call_kwargs["timeout"] == 30 class TestPaperlessSearchExecution: """Tests for Paperless search execution.""" @pytest.fixture def engine(self, mock_requests_get): """Create a Paperless engine.""" from local_deep_research.web_search_engines.engines.search_engine_paperless import ( PaperlessSearchEngine, ) return PaperlessSearchEngine( api_url="http://localhost:8000", api_key="test_token", max_results=10, ) def test_get_previews_success(self, engine, mock_requests_get): """Test successful preview retrieval.""" mock_response = Mock() mock_response.json.return_value = { "results": [ { "id": 1, "title": "Test Document", "content": "This is test content about the query.", "__search_hit__": { "score": 0.95, "rank": 1, "highlights": "Test query content", }, } ], "count": 1, } mock_response.raise_for_status = Mock() mock_requests_get.return_value = mock_response previews = engine._get_previews("test query") assert len(previews) == 1 assert previews[0]["title"] == "Test Document" assert "query" in previews[0]["snippet"] def test_get_previews_empty_results(self, engine, mock_requests_get): """Test preview retrieval with no results.""" mock_response = Mock() mock_response.json.return_value = {"results": [], "count": 0} mock_response.raise_for_status = Mock() mock_requests_get.return_value = mock_response previews = engine._get_previews("nonexistent query") assert previews == [] def test_get_previews_handles_exception(self, engine, mock_requests_get): """Test that preview exceptions are handled.""" import requests mock_requests_get.side_effect = requests.exceptions.RequestException( "Error" ) previews = engine._get_previews("test") assert previews == [] def test_multi_pass_search_deduplicates(self, engine, mock_requests_get): """Test that multi-pass search deduplicates results.""" # Return same document in multiple passes mock_response = Mock() mock_response.json.return_value = { "results": [ {"id": 1, "title": "Doc 1", "__search_hit__": {"score": 0.9}}, {"id": 2, "title": "Doc 2", "__search_hit__": {"score": 0.8}}, ], "count": 2, } mock_response.raise_for_status = Mock() mock_requests_get.return_value = mock_response results = engine._multi_pass_search("test") # Should deduplicate by doc_id assert len(results) == 2 class TestPaperlessDocumentConversion: """Tests for document conversion to LDR format.""" @pytest.fixture def engine(self, mock_requests_get): """Create a Paperless engine.""" from local_deep_research.web_search_engines.engines.search_engine_paperless import ( PaperlessSearchEngine, ) return PaperlessSearchEngine(api_url="http://localhost:8000") def test_convert_document_with_highlights(self, engine, mock_requests_get): """Test document conversion with search highlights.""" doc_data = { "id": 123, "title": "Invoice 2024", "content": "Full content here", "correspondent_name": "Acme Corp", "created": "2024-01-15", "__search_hit__": { "score": 0.95, "rank": 1, "highlights": "Found matching text here", }, } preview = engine._convert_document_to_preview(doc_data, "matching") assert "Invoice 2024" in preview["title"] assert preview["url"] == "http://localhost:8000/documents/123/details" assert "matching" in preview["snippet"] assert preview["author"] == "Acme Corp" assert preview["source"] == "Paperless" def test_convert_document_cleans_html_tags(self, engine, mock_requests_get): """Test that HTML tags are cleaned from highlights.""" doc_data = { "id": 1, "title": "Test", "__search_hit__": { "highlights": "keyword in text" }, } preview = engine._convert_document_to_preview(doc_data) assert "" not in preview["snippet"] # Highlights are converted to markdown bold assert "**keyword**" in preview["snippet"] def test_convert_document_without_highlights( self, engine, mock_requests_get ): """Test document conversion when no highlights available.""" doc_data = { "id": 1, "title": "Test Document", "content": "This is the document content with the query term inside.", } preview = engine._convert_document_to_preview(doc_data, "query") # Should use content as fallback assert "content" in preview["snippet"] def test_convert_document_builds_enhanced_title( self, engine, mock_requests_get ): """Test that enhanced title includes metadata.""" doc_data = { "id": 1, "title": "Report Q4", "correspondent_name": "Finance Dept", "document_type_name": "Financial Report", "created": "2024-03-15", "__search_hit__": {"highlights": "text"}, } preview = engine._convert_document_to_preview(doc_data) # Enhanced title should include correspondent, title, type, year assert "Finance Dept" in preview["title"] assert "Report Q4" in preview["title"] assert "Financial Report" in preview["title"] assert "2024" in preview["title"] def test_convert_document_includes_metadata( self, engine, mock_requests_get ): """Test that metadata is included in preview.""" doc_data = { "id": 42, "title": "Test", "correspondent_name": "Test Sender", "document_type_name": "Invoice", "created": "2024-01-01", "modified": "2024-01-02", "archive_serial_number": "ASN-001", "__search_hit__": {"score": 0.85, "rank": 3, "highlights": "text"}, } preview = engine._convert_document_to_preview(doc_data) assert preview["metadata"]["doc_id"] == "42" assert preview["metadata"]["correspondent"] == "Test Sender" assert preview["metadata"]["document_type"] == "Invoice" assert preview["metadata"]["search_score"] == 0.85 assert preview["metadata"]["search_rank"] == 3 def test_convert_document_multiple_highlights( self, engine, mock_requests_get ): """Test document conversion with multiple highlights returns multiple previews.""" doc_data = { "id": 1, "title": "Multi-match Document", "__search_hit__": { "highlights": [ "First match here", "Second match there", "Third match elsewhere", ] }, } previews = engine._convert_document_to_preview(doc_data) # Should return list of previews for multiple highlights assert isinstance(previews, list) assert len(previews) == 3 assert "(excerpt 1)" in previews[0]["title"] assert "(excerpt 2)" in previews[1]["title"] class TestPaperlessFullContent: """Tests for Paperless full content retrieval.""" @pytest.fixture def engine(self, mock_requests_get): """Create a Paperless engine.""" from local_deep_research.web_search_engines.engines.search_engine_paperless import ( PaperlessSearchEngine, ) return PaperlessSearchEngine(api_url="http://localhost:8000") def test_get_full_content_from_raw_data(self, engine, mock_requests_get): """Test full content extraction from raw data.""" items = [ { "title": "Test Doc", "snippet": "Short snippet", "metadata": {"doc_id": "1"}, "_raw_data": {"content": "This is the full document content."}, } ] results = engine._get_full_content(items) assert ( results[0]["full_content"] == "This is the full document content." ) assert "_raw_data" not in results[0] def test_get_full_content_fetches_if_missing( self, engine, mock_requests_get ): """Test that content is fetched if not in raw data.""" mock_response = Mock() mock_response.json.return_value = {"content": "Fetched content"} mock_response.raise_for_status = Mock() mock_requests_get.return_value = mock_response items = [ { "title": "Test", "snippet": "Snippet", "metadata": {"doc_id": "123"}, "_raw_data": {}, # No content in raw data } ] results = engine._get_full_content(items) assert results[0]["full_content"] == "Fetched content" def test_get_full_content_disabled(self, mock_requests_get): """Test that content is not fetched when disabled.""" from local_deep_research.web_search_engines.engines.search_engine_paperless import ( PaperlessSearchEngine, ) engine = PaperlessSearchEngine( api_url="http://localhost:8000", include_content=False ) items = [{"title": "Test", "snippet": "Snippet"}] results = engine._get_full_content(items) # Should return items unchanged assert results == items def test_get_full_content_handles_exception( self, engine, mock_requests_get ): """Test that exceptions during full content retrieval are handled.""" items = [ { "title": "Test", "snippet": "Snippet fallback", "metadata": {"doc_id": "1"}, # Missing _raw_data will cause issues } ] results = engine._get_full_content(items) # Should fallback to snippet assert results[0]["full_content"] == "Snippet fallback" class TestPaperlessQueryExpansion: """Tests for LLM-based query expansion.""" @pytest.fixture def engine(self, mock_requests_get): """Create a Paperless engine without LLM.""" from local_deep_research.web_search_engines.engines.search_engine_paperless import ( PaperlessSearchEngine, ) return PaperlessSearchEngine(api_url="http://localhost:8000") def test_expand_query_without_llm(self, engine, mock_requests_get): """Test that query is returned unchanged without LLM.""" result = engine._expand_query_with_llm("original query") assert result == "original query" def test_expand_query_with_llm(self, mock_requests_get): """Test query expansion with LLM.""" from local_deep_research.web_search_engines.engines.search_engine_paperless import ( PaperlessSearchEngine, ) mock_llm = Mock() mock_llm.invoke.return_value = Mock( content='invoice OR "billing statement" OR receipt' ) engine = PaperlessSearchEngine( api_url="http://localhost:8000", llm=mock_llm ) result = engine._expand_query_with_llm("find my invoices") assert "invoice" in result.lower() or "billing" in result.lower() def test_expand_query_handles_llm_exception(self, mock_requests_get): """Test that LLM exceptions fall back to original query.""" from local_deep_research.web_search_engines.engines.search_engine_paperless import ( PaperlessSearchEngine, ) mock_llm = Mock() mock_llm.invoke.side_effect = Exception("LLM error") engine = PaperlessSearchEngine( api_url="http://localhost:8000", llm=mock_llm ) result = engine._expand_query_with_llm("test query") assert result == "test query" class TestPaperlessRun: """Tests for Paperless run method.""" @pytest.fixture def engine(self, mock_requests_get): """Create a Paperless engine.""" from local_deep_research.web_search_engines.engines.search_engine_paperless import ( PaperlessSearchEngine, ) return PaperlessSearchEngine(api_url="http://localhost:8000") def test_run_success(self, engine, mock_requests_get): """Test successful search run.""" mock_response = Mock() mock_response.json.return_value = { "results": [ { "id": 1, "title": "Test Doc", "content": "Full content", "__search_hit__": {"score": 0.9, "highlights": "text"}, } ], "count": 1, } mock_response.raise_for_status = Mock() mock_requests_get.return_value = mock_response results = engine.run("test query") assert len(results) == 1 assert "full_content" in results[0] def test_run_empty_results(self, engine, mock_requests_get): """Test run with no results.""" mock_response = Mock() mock_response.json.return_value = {"results": [], "count": 0} mock_response.raise_for_status = Mock() mock_requests_get.return_value = mock_response results = engine.run("nonexistent") assert results == [] def test_run_handles_exception(self, engine, mock_requests_get): """Test that run handles exceptions.""" import requests mock_requests_get.side_effect = requests.exceptions.RequestException( "Error" ) results = engine.run("test") assert results == [] class TestPaperlessUtilities: """Tests for Paperless utility methods.""" @pytest.fixture def engine(self, mock_requests_get): """Create a Paperless engine.""" from local_deep_research.web_search_engines.engines.search_engine_paperless import ( PaperlessSearchEngine, ) return PaperlessSearchEngine( api_url="http://localhost:8000", api_key="token" ) def test_test_connection_success(self, engine, mock_requests_get): """Test connection test success.""" mock_response = Mock() mock_response.json.return_value = {"status": "ok"} mock_response.raise_for_status = Mock() mock_requests_get.return_value = mock_response result = engine.test_connection() assert result is True def test_test_connection_failure(self, engine, mock_requests_get): """Test connection test failure.""" import requests mock_requests_get.side_effect = requests.exceptions.RequestException( "Connection failed" ) result = engine.test_connection() assert result is False def test_get_document_count_success(self, engine, mock_requests_get): """Test document count retrieval.""" mock_response = Mock() mock_response.json.return_value = {"count": 150, "results": []} mock_response.raise_for_status = Mock() mock_requests_get.return_value = mock_response count = engine.get_document_count() assert count == 150 def test_get_document_count_error(self, engine, mock_requests_get): """Test document count on error.""" import requests mock_requests_get.side_effect = requests.exceptions.RequestException( "Error" ) count = engine.get_document_count() assert count == -1 class TestPaperlessAsync: """Tests for Paperless async methods.""" @pytest.fixture def engine(self, mock_requests_get): """Create a Paperless engine.""" from local_deep_research.web_search_engines.engines.search_engine_paperless import ( PaperlessSearchEngine, ) return PaperlessSearchEngine(api_url="http://localhost:8000") @pytest.mark.asyncio async def test_arun_falls_back_to_sync(self, engine, mock_requests_get): """Test that arun falls back to sync run.""" mock_response = Mock() mock_response.json.return_value = { "results": [ { "id": 1, "title": "Test", "__search_hit__": {"highlights": "text"}, } ], "count": 1, } mock_response.raise_for_status = Mock() mock_requests_get.return_value = mock_response results = await engine.arun("test") assert len(results) == 1