Files
wehub-resource-sync 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
chore: import upstream snapshot with attribution
2026-07-13 13:08:55 +08:00

719 lines
24 KiB
Python

"""
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 <span>query</span> 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 <span>matching</span> 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": "<span class='match'>keyword</span> in text"
},
}
preview = engine._convert_document_to_preview(doc_data)
assert "<span" not in preview["snippet"]
assert "</span>" 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 <span>match</span> here",
"Second <span>match</span> there",
"Third <span>match</span> 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