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

830 lines
28 KiB
Python

"""
Coverage tests for LibraryService — targeting the 29 missing statements.
Focuses narrowly on uncovered branches:
- _get_safe_absolute_path: empty/sentinel paths, None abs_path return
- get_document_by_id: downloaded_at branches (processed_at with/without isoformat,
uploaded_at fallback, None), research_created_at (str vs datetime vs None),
original_url from resource fallback, no-resource domain fallback
- get_documents: downloaded_at with processed_at, uploaded_at fallback,
_sort_date with uploaded_at, collection_id provided (skips get_default_library_id)
- open_file_location: library_root is None, ValueError from validate_safe_path
"""
from contextlib import contextmanager
from unittest.mock import Mock, MagicMock, patch
MODULE = "local_deep_research.research_library.services.library_service"
# ============== Helper ==============
def _make_service():
from local_deep_research.research_library.services.library_service import (
LibraryService,
)
with patch.object(LibraryService, "__init__", lambda self, username: None):
service = LibraryService.__new__(LibraryService)
service.username = "test_user"
return service
def _mock_session_cm(mocker, mock_session):
"""Patch get_user_db_session as a proper context manager."""
@contextmanager
def _cm(username, password=None):
yield mock_session
mocker.patch(
f"{MODULE}.get_user_db_session",
side_effect=_cm,
)
def _setup_get_document_by_id(
mocker,
mock_session,
doc_overrides=None,
research_overrides=None,
resource=None,
):
"""
Configure mock_session for get_document_by_id.
Returns (service, mock_doc, mock_resource, mock_research).
"""
service = _make_service()
mock_doc = Mock()
mock_doc.id = "doc-123"
mock_doc.resource_id = "res-1"
mock_doc.research_id = "research-1"
mock_doc.title = "Test Paper"
mock_doc.original_url = "https://arxiv.org/abs/2301.00001"
mock_doc.file_path = "pdfs/test.pdf"
mock_doc.filename = "test.pdf"
mock_doc.file_size = 1024
mock_doc.file_type = "pdf"
mock_doc.mime_type = "application/pdf"
mock_doc.text_content = None
mock_doc.status = "completed"
mock_doc.processed_at = None
mock_doc.favorite = False
mock_doc.tags = []
mock_doc.storage_mode = "filesystem"
if doc_overrides:
for k, v in doc_overrides.items():
setattr(mock_doc, k, v)
if resource is None:
mock_resource = Mock()
mock_resource.title = "Resource Title"
mock_resource.url = "https://arxiv.org/abs/2301.00001"
else:
mock_resource = resource
mock_research = Mock()
mock_research.query = "quantum computing"
mock_research.created_at = "2024-01-01T00:00:00"
if research_overrides:
for k, v in research_overrides.items():
setattr(mock_research, k, v)
mock_main_query = MagicMock()
mock_main_query.outerjoin.return_value = mock_main_query
mock_main_query.filter.return_value = mock_main_query
mock_main_query.first.return_value = (
mock_doc,
mock_resource,
mock_research,
)
mock_dc = Mock()
mock_dc.indexed = False
mock_dc.chunk_count = 0
mock_coll = Mock()
mock_coll.id = "coll-1"
mock_coll.name = "Library"
mock_coll_query = MagicMock()
mock_coll_query.join.return_value = mock_coll_query
mock_coll_query.filter.return_value = mock_coll_query
mock_coll_query.all.return_value = [(mock_dc, mock_coll)]
call_count = {"n": 0}
def query_side_effect(*args):
call_count["n"] += 1
return mock_main_query if call_count["n"] == 1 else mock_coll_query
mock_session.query.side_effect = query_side_effect
_mock_session_cm(mocker, mock_session)
mocker.patch(
f"{MODULE}.get_absolute_path_from_settings",
return_value=None,
)
return service, mock_doc, mock_resource, mock_research
# ============== _get_safe_absolute_path ==============
class TestGetSafeAbsolutePath:
"""Tests for the _get_safe_absolute_path helper."""
def test_empty_file_path_returns_none(self):
"""Empty string returns None without calling get_absolute_path_from_settings."""
service = _make_service()
result = service._get_safe_absolute_path("")
assert result is None
def test_sentinel_path_returns_none(self):
"""A FILE_PATH_SENTINELS value returns None."""
from local_deep_research.constants import FILE_PATH_METADATA_ONLY
service = _make_service()
result = service._get_safe_absolute_path(FILE_PATH_METADATA_ONLY)
assert result is None
def test_valid_path_with_none_abs_path_returns_none(self, mocker):
"""When get_absolute_path_from_settings returns None, result is None."""
service = _make_service()
mocker.patch(
f"{MODULE}.get_absolute_path_from_settings",
return_value=None,
)
result = service._get_safe_absolute_path("some/relative/path.pdf")
assert result is None
def test_valid_path_with_abs_path_returns_string(self, mocker):
"""When get_absolute_path_from_settings returns a path, result is str."""
service = _make_service()
from pathlib import Path
fake_path = Path("/library/some/relative/path.pdf")
mocker.patch(
f"{MODULE}.get_absolute_path_from_settings",
return_value=fake_path,
)
result = service._get_safe_absolute_path("some/relative/path.pdf")
assert result == str(fake_path)
assert isinstance(result, str)
# ============== get_document_by_id downloaded_at branches ==============
class TestGetDocumentByIdDownloadedAt:
"""Uncovered branches in the downloaded_at ternary expression."""
def test_processed_at_without_isoformat_uses_str(self, mocker):
"""processed_at exists but has no isoformat → str(processed_at) used."""
mock_session = MagicMock()
# Use a plain string as processed_at — strings have no .isoformat(), so
# the ternary falls to str(processed_at).
fake_processed_at = "2024-01-15"
service, mock_doc, _, _ = _setup_get_document_by_id(
mocker,
mock_session,
doc_overrides={"processed_at": fake_processed_at},
)
result = service.get_document_by_id("doc-123")
assert result is not None
# str("2024-01-15") == "2024-01-15"
assert result["downloaded_at"] == str(fake_processed_at)
def test_no_processed_at_with_uploaded_at_uses_isoformat(self, mocker):
"""processed_at is None, uploaded_at has isoformat → uploaded_at.isoformat() used."""
mock_session = MagicMock()
mock_uploaded_at = Mock()
mock_uploaded_at.isoformat.return_value = "2024-02-20T10:00:00"
service, mock_doc, _, _ = _setup_get_document_by_id(
mocker,
mock_session,
doc_overrides={
"processed_at": None,
"uploaded_at": mock_uploaded_at,
},
)
result = service.get_document_by_id("doc-123")
assert result is not None
assert result["downloaded_at"] == "2024-02-20T10:00:00"
def test_no_processed_at_no_uploaded_at_returns_none(self, mocker):
"""processed_at is None and no uploaded_at → downloaded_at is None."""
mock_session = MagicMock()
# Use a real Mock but delete uploaded_at attribute
class DocWithoutUploadedAt:
pass
service, mock_doc, _, _ = _setup_get_document_by_id(
mocker,
mock_session,
doc_overrides={"processed_at": None},
)
# Ensure hasattr(doc, 'uploaded_at') returns False
del mock_doc.uploaded_at
result = service.get_document_by_id("doc-123")
assert result is not None
assert result["downloaded_at"] is None
# ============== get_document_by_id research_created_at branches ==============
class TestGetDocumentByIdResearchCreatedAt:
"""Uncovered branches for research_created_at ternary."""
def test_research_created_at_as_datetime_uses_isoformat(self, mocker):
"""research.created_at is a datetime object → .isoformat() called."""
mock_session = MagicMock()
mock_created_at = Mock()
mock_created_at.isoformat.return_value = "2024-03-01T12:00:00"
# isinstance(research.created_at, str) == False → goes to elif branch
service, _, _, mock_research = _setup_get_document_by_id(
mocker,
mock_session,
research_overrides={"created_at": mock_created_at},
)
# Make isinstance check return False (created_at is not str)
mock_research.created_at = mock_created_at
result = service.get_document_by_id("doc-123")
assert result is not None
assert result["research_created_at"] == "2024-03-01T12:00:00"
def test_no_research_created_at_returns_none(self, mocker):
"""research is None → research_created_at is None."""
service = _make_service()
mock_session = MagicMock()
mock_doc = Mock()
mock_doc.id = "doc-123"
mock_doc.resource_id = None
mock_doc.research_id = None
mock_doc.title = "Upload"
mock_doc.original_url = None
mock_doc.file_path = None
mock_doc.filename = "upload.pdf"
mock_doc.file_size = 512
mock_doc.file_type = "pdf"
mock_doc.mime_type = "application/pdf"
mock_doc.text_content = None
mock_doc.status = "completed"
mock_doc.processed_at = None
mock_doc.favorite = False
mock_doc.tags = []
mock_doc.storage_mode = "filesystem"
mock_main_query = MagicMock()
mock_main_query.outerjoin.return_value = mock_main_query
mock_main_query.filter.return_value = mock_main_query
# research is None (user upload)
mock_main_query.first.return_value = (mock_doc, None, None)
mock_coll_query = MagicMock()
mock_coll_query.join.return_value = mock_coll_query
mock_coll_query.filter.return_value = mock_coll_query
mock_coll_query.all.return_value = []
call_count = {"n": 0}
def query_side_effect(*args):
call_count["n"] += 1
return mock_main_query if call_count["n"] == 1 else mock_coll_query
mock_session.query.side_effect = query_side_effect
_mock_session_cm(mocker, mock_session)
mocker.patch(
f"{MODULE}.get_absolute_path_from_settings", return_value=None
)
result = service.get_document_by_id("doc-123")
assert result is not None
assert result["research_created_at"] is None
assert result["research_title"] == "User Upload"
assert result["domain"] == "User Upload"
# ============== get_document_by_id original_url fallback ==============
class TestGetDocumentByIdOriginalUrlFallback:
"""original_url falls back to resource.url when doc.original_url is None."""
def test_doc_original_url_none_uses_resource_url(self, mocker):
"""doc.original_url=None → original_url comes from resource.url."""
mock_session = MagicMock()
service, mock_doc, mock_resource, _ = _setup_get_document_by_id(
mocker,
mock_session,
doc_overrides={"original_url": None},
)
mock_resource.url = "https://example.com/paper.pdf"
result = service.get_document_by_id("doc-123")
assert result is not None
assert result["original_url"] == "https://example.com/paper.pdf"
# ============== open_file_location uncovered branches ==============
class TestOpenFileLocationMissingBranches:
"""Branches in open_file_location not covered by existing tests."""
def test_library_root_none_returns_false(self, mocker):
"""get_absolute_path_from_settings returns None for library root → False."""
service = _make_service()
mock_session = MagicMock()
mock_doc = Mock()
mock_doc.original_url = "https://example.com/doc.pdf"
mock_tracker = Mock()
mock_tracker.file_path = "pdfs/doc.pdf"
mock_doc_query = MagicMock()
mock_doc_query.get.return_value = mock_doc
mock_tracker_query = MagicMock()
mock_tracker_query.filter_by.return_value.first.return_value = (
mock_tracker
)
def query_router(model):
name = getattr(model, "__name__", str(model))
if "DownloadTracker" in str(name) or "Tracker" in str(model):
return mock_tracker_query
return mock_doc_query
mock_session.query.side_effect = query_router
_mock_session_cm(mocker, mock_session)
# library_root is None when get_absolute_path_from_settings("") returns None
mocker.patch(
f"{MODULE}.get_absolute_path_from_settings",
return_value=None,
)
result = service.open_file_location("doc-123")
assert result is False
def test_path_validation_raises_value_error_returns_false(self, mocker):
"""PathValidator.validate_safe_path raises ValueError → returns False."""
service = _make_service()
mock_session = MagicMock()
mock_doc = Mock()
mock_doc.original_url = "https://example.com/doc.pdf"
mock_tracker = Mock()
mock_tracker.file_path = "../../etc/passwd" # traversal attempt
mock_doc_query = MagicMock()
mock_doc_query.get.return_value = mock_doc
mock_tracker_query = MagicMock()
mock_tracker_query.filter_by.return_value.first.return_value = (
mock_tracker
)
def query_router(model):
name = getattr(model, "__name__", str(model))
if "DownloadTracker" in str(name) or "Tracker" in str(model):
return mock_tracker_query
return mock_doc_query
mock_session.query.side_effect = query_router
_mock_session_cm(mocker, mock_session)
mocker.patch(
f"{MODULE}.get_absolute_path_from_settings",
return_value=MagicMock(), # non-None library root
)
mocker.patch(
f"{MODULE}.PathValidator.validate_safe_path",
side_effect=ValueError("Path traversal detected"),
)
result = service.open_file_location("doc-123")
assert result is False
# ============== get_documents with explicit collection_id ==============
class TestGetDocumentsWithCollectionId:
"""get_documents when collection_id is explicitly provided (skips get_default_library_id)."""
def test_explicit_collection_id_skips_default_lookup(self, mocker):
"""When collection_id is provided, get_default_library_id is NOT called."""
service = _make_service()
mock_session = MagicMock()
mock_query = MagicMock()
mock_query.join.return_value = mock_query
mock_query.outerjoin.return_value = mock_query
mock_query.filter.return_value = mock_query
mock_query.order_by.return_value = mock_query
mock_query.offset.return_value = mock_query
mock_query.limit.return_value = mock_query
mock_query.subquery.return_value = MagicMock()
mock_query.all.return_value = []
mock_session.query.return_value = mock_query
_mock_session_cm(mocker, mock_session)
mock_get_default = mocker.patch(
"local_deep_research.database.library_init.get_default_library_id",
return_value="default-id",
)
result = service.get_documents(collection_id="explicit-coll-id")
assert isinstance(result, list)
mock_get_default.assert_not_called()
# ============== get_documents downloaded_at / _sort_date with uploaded_at ==============
class TestGetDocumentsDateBranches:
"""Test downloaded_at and _sort_date branches in get_documents result processing."""
def _make_mock_doc_result(self, processed_at=None, uploaded_at=None):
"""Create a (doc, res_by_fk, res_by_doc, research, doc_collection) tuple."""
mock_doc = Mock()
mock_doc.id = "doc-1"
mock_doc.resource_id = "res-1"
mock_doc.research_id = "research-1"
mock_doc.title = "Test"
mock_doc.authors = "Author"
mock_doc.published_date = None
mock_doc.doi = None
mock_doc.arxiv_id = None
mock_doc.pmid = None
mock_doc.file_path = "pdfs/test.pdf"
mock_doc.filename = "test.pdf"
mock_doc.file_size = 1024
mock_doc.file_type = "pdf"
mock_doc.original_url = "https://arxiv.org/abs/1234.5678"
mock_doc.status = "completed"
mock_doc.processed_at = processed_at
mock_doc.favorite = False
mock_doc.tags = []
mock_doc.text_content = None
mock_doc.storage_mode = "filesystem"
if uploaded_at is not None:
mock_doc.uploaded_at = uploaded_at
mock_resource = Mock()
mock_resource.title = "Resource"
mock_research = Mock()
mock_research.title = "Research"
mock_research.query = "test query"
mock_research.mode = "deep"
mock_research.created_at = "2024-01-01"
mock_doc_collection = Mock()
mock_doc_collection.indexed = False
mock_doc_collection.chunk_count = 0
return (
mock_doc,
mock_resource,
None,
mock_research,
mock_doc_collection,
)
def test_downloaded_at_uses_processed_at_isoformat(self, mocker):
"""processed_at with isoformat → downloaded_at = processed_at.isoformat()."""
service = _make_service()
mock_session = MagicMock()
mock_processed_at = Mock()
mock_processed_at.isoformat.return_value = "2024-05-01T09:00:00"
result_row = self._make_mock_doc_result(processed_at=mock_processed_at)
mock_query = MagicMock()
mock_query.join.return_value = mock_query
mock_query.outerjoin.return_value = mock_query
mock_query.filter.return_value = mock_query
mock_query.order_by.return_value = mock_query
mock_query.offset.return_value = mock_query
mock_query.limit.return_value = mock_query
mock_query.subquery.return_value = MagicMock()
mock_query.all.return_value = [result_row]
mock_session.query.return_value = mock_query
_mock_session_cm(mocker, mock_session)
mocker.patch(
"local_deep_research.database.library_init.get_default_library_id",
return_value="default-id",
)
mocker.patch(
f"{MODULE}.get_absolute_path_from_settings",
return_value=None,
)
results = service.get_documents()
assert len(results) == 1
assert results[0]["downloaded_at"] == "2024-05-01T09:00:00"
def test_downloaded_at_falls_back_to_uploaded_at(self, mocker):
"""No processed_at, has uploaded_at → downloaded_at = uploaded_at.isoformat()."""
service = _make_service()
mock_session = MagicMock()
mock_uploaded_at = Mock()
mock_uploaded_at.isoformat.return_value = "2024-06-15T14:30:00"
result_row = self._make_mock_doc_result(
processed_at=None, uploaded_at=mock_uploaded_at
)
mock_query = MagicMock()
mock_query.join.return_value = mock_query
mock_query.outerjoin.return_value = mock_query
mock_query.filter.return_value = mock_query
mock_query.order_by.return_value = mock_query
mock_query.offset.return_value = mock_query
mock_query.limit.return_value = mock_query
mock_query.subquery.return_value = MagicMock()
mock_query.all.return_value = [result_row]
mock_session.query.return_value = mock_query
_mock_session_cm(mocker, mock_session)
mocker.patch(
"local_deep_research.database.library_init.get_default_library_id",
return_value="default-id",
)
mocker.patch(
f"{MODULE}.get_absolute_path_from_settings",
return_value=None,
)
results = service.get_documents()
assert len(results) == 1
assert results[0]["downloaded_at"] == "2024-06-15T14:30:00"
# ============== get_documents resource coalesce, dedup, blob, search ==============
class TestGetDocumentsResourceCoalesce:
"""Test resource coalescing (res_by_fk vs res_by_doc) and dedup in get_documents."""
def _setup_query(self, mocker, mock_session, result_rows):
"""Wire up the mock query chain and session for get_documents."""
mock_query = MagicMock()
mock_query.join.return_value = mock_query
mock_query.outerjoin.return_value = mock_query
mock_query.filter.return_value = mock_query
mock_query.order_by.return_value = mock_query
mock_query.offset.return_value = mock_query
mock_query.limit.return_value = mock_query
mock_query.distinct.return_value = mock_query
mock_query.subquery.return_value = MagicMock()
mock_query.all.return_value = result_rows
mock_session.query.return_value = mock_query
_mock_session_cm(mocker, mock_session)
mocker.patch(
"local_deep_research.database.library_init.get_default_library_id",
return_value="default-id",
)
mocker.patch(
f"{MODULE}.get_absolute_path_from_settings",
return_value=None,
)
def _make_doc(self, doc_id="doc-1", storage_mode="filesystem"):
mock_doc = Mock()
mock_doc.id = doc_id
mock_doc.resource_id = None
mock_doc.research_id = "research-1"
mock_doc.title = "Test"
mock_doc.authors = "Author"
mock_doc.published_date = None
mock_doc.doi = None
mock_doc.arxiv_id = None
mock_doc.pmid = None
mock_doc.file_path = "pdfs/test.pdf"
mock_doc.filename = "test.pdf"
mock_doc.file_size = 1024
mock_doc.file_type = "pdf"
mock_doc.original_url = "https://example.com/paper.pdf"
mock_doc.status = "completed"
mock_doc.processed_at = Mock()
mock_doc.processed_at.isoformat.return_value = "2024-01-01T00:00:00"
mock_doc.favorite = False
mock_doc.tags = []
mock_doc.text_content = None
mock_doc.storage_mode = storage_mode
return mock_doc
def _make_research(self):
mock_research = Mock()
mock_research.title = "Research"
mock_research.query = "test query"
mock_research.mode = "deep"
mock_research.created_at = "2024-01-01"
return mock_research
def _make_doc_collection(self):
mock_dc = Mock()
mock_dc.indexed = False
mock_dc.chunk_count = 0
return mock_dc
def test_resource_coalesce_falls_back_to_res_by_doc(self, mocker):
"""When res_by_fk is None, res_by_doc is used for document_title."""
service = _make_service()
mock_session = MagicMock()
mock_resource = Mock()
mock_resource.title = "From document_id FK"
doc = self._make_doc()
doc.title = None # Force fallback to resource.title
# (doc, res_by_fk=None, res_by_doc=resource, research, doc_collection)
row = (
doc,
None,
mock_resource,
self._make_research(),
self._make_doc_collection(),
)
self._setup_query(mocker, mock_session, [row])
results = service.get_documents()
assert len(results) == 1
assert results[0]["document_title"] == "From document_id FK"
def test_resource_coalesce_both_none(self, mocker):
"""When both res_by_fk and res_by_doc are None, falls back to filename."""
service = _make_service()
mock_session = MagicMock()
doc = self._make_doc()
doc.title = None # Force fallback to filename
# (doc, res_by_fk=None, res_by_doc=None, research, doc_collection)
row = (
doc,
None,
None,
self._make_research(),
self._make_doc_collection(),
)
self._setup_query(mocker, mock_session, [row])
results = service.get_documents()
assert len(results) == 1
assert results[0]["document_title"] == "test.pdf"
def test_dedup_skips_fan_out_duplicate_rows(self, mocker):
"""When ResourceByDoc fan-out produces duplicate doc rows, dedup keeps first."""
service = _make_service()
mock_session = MagicMock()
doc = self._make_doc(doc_id="doc-dup")
res_a = Mock()
res_a.title = "Resource A"
res_b = Mock()
res_b.title = "Resource B"
research = self._make_research()
dc = self._make_doc_collection()
# Two rows for same doc — fan-out from ResourceByDoc
row1 = (doc, None, res_a, research, dc)
row2 = (doc, None, res_b, research, dc)
self._setup_query(mocker, mock_session, [row1, row2])
results = service.get_documents()
# Only one result despite two rows — dedup works
assert len(results) == 1
assert results[0]["id"] == "doc-dup"
def test_batch_blob_check_for_database_storage(self, mocker):
"""Documents with storage_mode=database use batch blob check for has_pdf."""
service = _make_service()
mock_session = MagicMock()
doc = self._make_doc(doc_id="doc-blob", storage_mode="database")
doc.file_path = None # No filesystem path
doc.filename = "paper.pdf"
row = (
doc,
None,
None,
self._make_research(),
self._make_doc_collection(),
)
mock_query = MagicMock()
mock_query.join.return_value = mock_query
mock_query.outerjoin.return_value = mock_query
mock_query.filter.return_value = mock_query
mock_query.order_by.return_value = mock_query
mock_query.offset.return_value = mock_query
mock_query.limit.return_value = mock_query
mock_query.subquery.return_value = MagicMock()
# First .all() → main query results, second .all() → blob query results
mock_query.all.side_effect = [
[row],
[("doc-blob",)], # blob exists for this doc
]
mock_session.query.return_value = mock_query
_mock_session_cm(mocker, mock_session)
mocker.patch(
"local_deep_research.database.library_init.get_default_library_id",
return_value="default-id",
)
mocker.patch(
f"{MODULE}.get_absolute_path_from_settings",
return_value=None,
)
results = service.get_documents()
assert len(results) == 1
assert results[0]["has_pdf"] is True
def test_search_query_exercises_distinct_path(self, mocker):
"""When search_query is provided, the subquery uses outerjoin + distinct."""
service = _make_service()
mock_session = MagicMock()
doc = self._make_doc()
row = (
doc,
None,
None,
self._make_research(),
self._make_doc_collection(),
)
mock_query = MagicMock()
mock_query.join.return_value = mock_query
mock_query.outerjoin.return_value = mock_query
mock_query.filter.return_value = mock_query
mock_query.order_by.return_value = mock_query
mock_query.offset.return_value = mock_query
mock_query.limit.return_value = mock_query
mock_query.distinct.return_value = mock_query
mock_query.subquery.return_value = MagicMock()
mock_query.all.return_value = [row]
mock_session.query.return_value = mock_query
_mock_session_cm(mocker, mock_session)
mocker.patch(
"local_deep_research.database.library_init.get_default_library_id",
return_value="default-id",
)
mocker.patch(
f"{MODULE}.get_absolute_path_from_settings",
return_value=None,
)
results = service.get_documents(search_query="test")
assert len(results) == 1
# Verify distinct was called (search_query path)
mock_query.distinct.assert_called()