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