""" Tests for database/library_init.py. Tests the library initialization module which handles: - Seeding source_types table with predefined types - Creating the default "Library" collection - Full library initialization orchestration """ from unittest.mock import MagicMock, patch import pytest from sqlalchemy.exc import IntegrityError from tests.test_utils import add_src_to_path add_src_to_path() from local_deep_research.database.library_init import ( # noqa: E402 seed_source_types, ensure_default_library_collection, ensure_research_history_collection, initialize_library_for_user, get_default_library_id, get_source_type_id, ) class TestSeedSourceTypes: """Tests for seed_source_types function.""" @patch("local_deep_research.database.library_init.get_user_db_session") @patch("local_deep_research.database.library_init.uuid.uuid4") def test_creates_all_predefined_types_when_none_exist( self, mock_uuid, mock_get_session ): """Should create all predefined source types.""" mock_session = MagicMock() mock_get_session.return_value.__enter__.return_value = mock_session # Simulate no existing types mock_session.query.return_value.filter_by.return_value.first.return_value = None mock_uuid.return_value = "test-uuid-123" seed_source_types("testuser", "testpass") # Should have queried for each type and added 6 new ones # (research_download, user_upload, manual_entry, research_report, # research_source, zotero) assert mock_session.add.call_count == 6 mock_session.commit.assert_called_once() @patch("local_deep_research.database.library_init.get_user_db_session") def test_skips_existing_types(self, mock_get_session): """Should not duplicate existing source types.""" mock_session = MagicMock() mock_get_session.return_value.__enter__.return_value = mock_session # Simulate all types already exist mock_existing = MagicMock() mock_session.query.return_value.filter_by.return_value.first.return_value = mock_existing seed_source_types("testuser", "testpass") # Should not add any new types mock_session.add.assert_not_called() mock_session.commit.assert_called_once() @patch("local_deep_research.database.library_init.get_user_db_session") def test_handles_integrity_error_gracefully(self, mock_get_session): """Should handle IntegrityError gracefully without raising.""" mock_session = MagicMock() mock_get_session.return_value.__enter__.return_value = mock_session mock_session.query.return_value.filter_by.return_value.first.return_value = None mock_session.commit.side_effect = IntegrityError( "statement", "params", "orig" ) # Should not raise seed_source_types("testuser", "testpass") @patch("local_deep_research.database.library_init.get_user_db_session") def test_raises_on_unexpected_error(self, mock_get_session): """Should re-raise unexpected exceptions.""" mock_session = MagicMock() mock_get_session.return_value.__enter__.return_value = mock_session mock_session.query.side_effect = RuntimeError( "Database connection failed" ) with pytest.raises(RuntimeError, match="Database connection failed"): seed_source_types("testuser", "testpass") @patch("local_deep_research.database.library_init.logger") @patch("local_deep_research.database.library_init.get_user_db_session") def test_logs_creation_messages(self, mock_get_session, mock_logger): """Should log when creating source types.""" mock_session = MagicMock() mock_get_session.return_value.__enter__.return_value = mock_session mock_session.query.return_value.filter_by.return_value.first.return_value = None seed_source_types("testuser", "testpass") # Should log info for each created type and final success assert mock_logger.info.call_count >= 3 @patch("local_deep_research.database.library_init.get_user_db_session") def test_creates_types_with_correct_attributes(self, mock_get_session): """Should create source types with correct name, display_name, description, icon.""" mock_session = MagicMock() mock_get_session.return_value.__enter__.return_value = mock_session mock_session.query.return_value.filter_by.return_value.first.return_value = None seed_source_types("testuser") # Check the types created added_types = [call.args[0] for call in mock_session.add.call_args_list] type_names = [t.name for t in added_types] assert "research_download" in type_names assert "user_upload" in type_names assert "manual_entry" in type_names @patch("local_deep_research.database.library_init.get_user_db_session") def test_password_is_optional(self, mock_get_session): """Should work when password is not provided.""" mock_session = MagicMock() mock_get_session.return_value.__enter__.return_value = mock_session mock_session.query.return_value.filter_by.return_value.first.return_value = MagicMock() # Should not raise when password is None seed_source_types("testuser") mock_get_session.assert_called_once_with("testuser", None) class TestEnsureDefaultLibraryCollection: """Tests for ensure_default_library_collection function.""" @patch("local_deep_research.database.library_init.get_user_db_session") @patch("local_deep_research.database.library_init.uuid.uuid4") def test_creates_collection_when_none_exists( self, mock_uuid, mock_get_session ): """Should create default Library collection when none exists.""" mock_session = MagicMock() mock_get_session.return_value.__enter__.return_value = mock_session mock_session.query.return_value.filter_by.return_value.first.return_value = None mock_uuid.return_value = "new-library-uuid" result = ensure_default_library_collection("testuser", "testpass") assert result == "new-library-uuid" mock_session.add.assert_called_once() mock_session.commit.assert_called_once() @patch("local_deep_research.database.library_init.get_user_db_session") def test_returns_existing_collection_id(self, mock_get_session): """Should return ID of existing default collection.""" mock_session = MagicMock() mock_get_session.return_value.__enter__.return_value = mock_session mock_existing = MagicMock() mock_existing.id = "existing-library-uuid" mock_session.query.return_value.filter_by.return_value.first.return_value = mock_existing result = ensure_default_library_collection("testuser", "testpass") assert result == "existing-library-uuid" mock_session.add.assert_not_called() @patch("local_deep_research.database.library_init.get_user_db_session") def test_creates_with_is_default_true(self, mock_get_session): """Should set is_default=True on new collection.""" mock_session = MagicMock() mock_get_session.return_value.__enter__.return_value = mock_session mock_session.query.return_value.filter_by.return_value.first.return_value = None ensure_default_library_collection("testuser") # Check that the collection was created with is_default=True added_collection = mock_session.add.call_args.args[0] assert added_collection.is_default is True @patch("local_deep_research.database.library_init.get_user_db_session") def test_creates_with_correct_name_and_type(self, mock_get_session): """Should create collection with name='Library' and type='default_library'.""" mock_session = MagicMock() mock_get_session.return_value.__enter__.return_value = mock_session mock_session.query.return_value.filter_by.return_value.first.return_value = None ensure_default_library_collection("testuser") added_collection = mock_session.add.call_args.args[0] assert added_collection.name == "Library" assert added_collection.collection_type == "default_library" @patch("local_deep_research.database.library_init.get_user_db_session") def test_raises_on_database_error(self, mock_get_session): """Should re-raise exceptions from database operations.""" mock_session = MagicMock() mock_get_session.return_value.__enter__.return_value = mock_session mock_session.query.side_effect = RuntimeError("Database error") with pytest.raises(RuntimeError, match="Database error"): ensure_default_library_collection("testuser") class TestEnsureResearchHistoryCollection: """Tests for ensure_research_history_collection function.""" @patch("local_deep_research.database.library_init.get_user_db_session") @patch("local_deep_research.database.library_init.uuid.uuid4") def test_creates_collection_when_none_exists( self, mock_uuid, mock_get_session ): """Should create Research History collection when none exists.""" mock_session = MagicMock() mock_get_session.return_value.__enter__.return_value = mock_session mock_session.query.return_value.filter_by.return_value.first.return_value = None mock_uuid.return_value = "new-history-uuid" result = ensure_research_history_collection("testuser", "testpass") assert result == "new-history-uuid" mock_session.add.assert_called_once() mock_session.commit.assert_called_once() @patch("local_deep_research.database.library_init.get_user_db_session") def test_returns_existing_collection_id(self, mock_get_session): """Should return ID of existing Research History collection.""" mock_session = MagicMock() mock_get_session.return_value.__enter__.return_value = mock_session mock_existing = MagicMock() mock_existing.id = "existing-history-uuid" mock_session.query.return_value.filter_by.return_value.first.return_value = mock_existing result = ensure_research_history_collection("testuser", "testpass") assert result == "existing-history-uuid" mock_session.add.assert_not_called() @patch("local_deep_research.database.library_init.get_user_db_session") def test_reraises_exception(self, mock_get_session): """Should re-raise exceptions from database operations.""" mock_session = MagicMock() mock_get_session.return_value.__enter__.return_value = mock_session mock_session.query.side_effect = RuntimeError("Database error") with pytest.raises(RuntimeError, match="Database error"): ensure_research_history_collection("testuser") class TestInitializeLibraryForUser: """Tests for initialize_library_for_user function.""" @patch( "local_deep_research.database.library_init.ensure_research_history_collection" ) @patch( "local_deep_research.database.library_init.ensure_default_library_collection" ) @patch("local_deep_research.database.library_init.seed_source_types") def test_returns_success_result(self, mock_seed, mock_ensure, mock_history): """Should return dict with success=True on success.""" mock_ensure.return_value = "library-uuid-123" mock_history.return_value = "history-uuid-456" result = initialize_library_for_user("testuser", "testpass") assert result["success"] is True assert result["source_types_seeded"] is True assert result["library_collection_id"] == "library-uuid-123" assert result["research_history_collection_id"] == "history-uuid-456" assert "error" not in result @patch( "local_deep_research.database.library_init.ensure_research_history_collection" ) @patch( "local_deep_research.database.library_init.ensure_default_library_collection" ) @patch("local_deep_research.database.library_init.seed_source_types") def test_returns_error_on_seed_failure( self, mock_seed, mock_ensure, mock_history ): """Should include error message when seed_source_types fails.""" mock_seed.side_effect = RuntimeError("Seeding failed") result = initialize_library_for_user("testuser") assert result["success"] is False assert result["source_types_seeded"] is False assert "error" in result assert "Seeding failed" in result["error"] @patch( "local_deep_research.database.library_init.ensure_research_history_collection" ) @patch( "local_deep_research.database.library_init.ensure_default_library_collection" ) @patch("local_deep_research.database.library_init.seed_source_types") def test_returns_error_on_ensure_failure( self, mock_seed, mock_ensure, mock_history ): """Should include error message when ensure_default_library_collection fails.""" mock_ensure.side_effect = RuntimeError("Collection creation failed") result = initialize_library_for_user("testuser") assert result["success"] is False assert result["source_types_seeded"] is True # Seeding succeeded assert result["library_collection_id"] is None assert "error" in result assert "Collection creation failed" in result["error"] @patch( "local_deep_research.database.library_init.ensure_research_history_collection" ) @patch( "local_deep_research.database.library_init.ensure_default_library_collection" ) @patch("local_deep_research.database.library_init.seed_source_types") def test_calls_seed_and_ensure_in_order( self, mock_seed, mock_ensure, mock_history ): """Should call both seed_source_types and ensure_default_library_collection.""" mock_ensure.return_value = "library-uuid" call_order = [] mock_seed.side_effect = lambda *args, **kwargs: call_order.append( "seed" ) mock_ensure.side_effect = lambda *args, **kwargs: ( call_order.append("ensure"), "library-uuid", )[1] initialize_library_for_user("testuser", "testpass") assert call_order == ["seed", "ensure"] mock_seed.assert_called_once_with("testuser", "testpass") mock_ensure.assert_called_once_with("testuser", "testpass") @patch( "local_deep_research.database.library_init.ensure_default_library_collection" ) @patch("local_deep_research.database.library_init.seed_source_types") def test_returns_all_expected_keys(self, mock_seed, mock_ensure): """Should return dict with all expected keys.""" mock_ensure.return_value = "lib-id" result = initialize_library_for_user("testuser") assert "source_types_seeded" in result assert "library_collection_id" in result assert "success" in result class TestGetDefaultLibraryId: """Tests for get_default_library_id function.""" @patch( "local_deep_research.database.library_init.ensure_default_library_collection" ) def test_returns_library_id(self, mock_ensure): """Should return the library collection ID.""" mock_ensure.return_value = "default-lib-uuid" result = get_default_library_id("testuser", "testpass") assert result == "default-lib-uuid" mock_ensure.assert_called_once_with("testuser", "testpass") @patch( "local_deep_research.database.library_init.ensure_default_library_collection" ) def test_creates_library_if_missing(self, mock_ensure): """Should create library if it doesn't exist (via ensure_default_library_collection).""" mock_ensure.return_value = "new-lib-uuid" result = get_default_library_id("testuser") assert result == "new-lib-uuid" class TestGetSourceTypeId: """Tests for get_source_type_id function.""" @patch("local_deep_research.database.library_init.get_user_db_session") def test_returns_id_for_valid_type(self, mock_get_session): """Should return ID for existing source type.""" mock_session = MagicMock() mock_get_session.return_value.__enter__.return_value = mock_session mock_source_type = MagicMock() mock_source_type.id = "research-download-uuid" mock_session.query.return_value.filter_by.return_value.first.return_value = mock_source_type result = get_source_type_id("testuser", "research_download", "testpass") assert result == "research-download-uuid" @patch("local_deep_research.database.library_init.get_user_db_session") def test_raises_value_error_for_unknown_type(self, mock_get_session): """Should raise ValueError for non-existent type.""" mock_session = MagicMock() mock_get_session.return_value.__enter__.return_value = mock_session mock_session.query.return_value.filter_by.return_value.first.return_value = None with pytest.raises(ValueError, match="Source type not found"): get_source_type_id("testuser", "nonexistent_type") @patch("local_deep_research.database.library_init.get_user_db_session") def test_raises_on_database_error(self, mock_get_session): """Should re-raise database errors after logging.""" mock_session = MagicMock() mock_get_session.return_value.__enter__.return_value = mock_session mock_session.query.side_effect = RuntimeError("Connection lost") with pytest.raises(RuntimeError, match="Connection lost"): get_source_type_id("testuser", "user_upload") @patch("local_deep_research.database.library_init.get_user_db_session") def test_password_is_optional(self, mock_get_session): """Should work when password is not provided.""" mock_session = MagicMock() mock_get_session.return_value.__enter__.return_value = mock_session mock_source_type = MagicMock() mock_source_type.id = "type-uuid" mock_session.query.return_value.filter_by.return_value.first.return_value = mock_source_type result = get_source_type_id("testuser", "manual_entry") assert result == "type-uuid" mock_get_session.assert_called_once_with("testuser", None)