"""Tests for database/session_context.py.""" import pytest from unittest.mock import Mock, patch from flask import Flask @pytest.fixture def app(): """Create test Flask application.""" app = Flask(__name__) app.config["TESTING"] = True app.secret_key = "test-secret-key" return app class TestDatabaseSessionError: """Tests for DatabaseSessionError exception.""" def test_exception_can_be_raised(self): """Test that DatabaseSessionError can be raised.""" from local_deep_research.database.session_context import ( DatabaseSessionError, ) with pytest.raises(DatabaseSessionError): raise DatabaseSessionError("Test error") def test_exception_message(self): """Test that exception preserves message.""" from local_deep_research.database.session_context import ( DatabaseSessionError, ) try: raise DatabaseSessionError("Custom error message") except DatabaseSessionError as e: assert str(e) == "Custom error message" class TestGetUserDbSession: """Tests for get_user_db_session context manager.""" def test_raises_when_no_username_provided(self, app): """Test that error is raised when no username available.""" from local_deep_research.database.session_context import ( get_user_db_session, DatabaseSessionError, ) with app.test_request_context(): # No username in session with pytest.raises( DatabaseSessionError, match="No authenticated user" ): with get_user_db_session(): pass def test_uses_provided_username(self, app): """Test that explicitly provided username is used.""" from local_deep_research.database.session_context import ( get_user_db_session, ) with app.test_request_context(): with patch( "local_deep_research.database.session_context.db_manager" ) as mock_db: mock_db.has_encryption = False with patch( "local_deep_research.database.thread_local_session.get_metrics_session" ) as mock_get_session: mock_session = Mock() mock_get_session.return_value = mock_session with get_user_db_session( username="testuser", password="testpass" ) as session: assert session is mock_session def test_uses_flask_session_username_when_not_provided(self, app): """Test that Flask session username is used when not explicitly provided.""" from local_deep_research.database.session_context import ( get_user_db_session, UNENCRYPTED_DB_PLACEHOLDER, ) with app.test_request_context(): from flask import session as flask_session flask_session["username"] = "flask_user" with patch( "local_deep_research.database.session_context.db_manager" ) as mock_db: mock_db.has_encryption = False with patch( "local_deep_research.database.thread_local_session.get_metrics_session" ) as mock_get_session: mock_session = Mock() mock_get_session.return_value = mock_session with get_user_db_session() as _session: mock_get_session.assert_called_once_with( "flask_user", UNENCRYPTED_DB_PLACEHOLDER ) def test_raises_when_encrypted_db_requires_password(self, app): """Test error when encrypted DB accessed without password.""" from local_deep_research.database.session_context import ( get_user_db_session, DatabaseSessionError, ) with app.test_request_context(): from flask import session as flask_session flask_session["username"] = "testuser" with patch( "local_deep_research.database.session_context.db_manager" ) as mock_db: mock_db.has_encryption = True mock_db.connections = {} with patch( "local_deep_research.database.session_context.get_search_context" ) as mock_ctx: mock_ctx.return_value = None with patch( "local_deep_research.database.session_passwords.session_password_store" ) as mock_store: mock_store.get_session_password.return_value = None with pytest.raises( DatabaseSessionError, match="requires password" ): with get_user_db_session(): pass class TestWithUserDatabase: """Tests for with_user_database decorator.""" def test_decorator_injects_db_session(self, app): """Test that decorator injects db_session as first argument.""" from local_deep_research.database.session_context import ( with_user_database, ) @with_user_database def test_func(db_session, arg1, arg2): return (db_session, arg1, arg2) with app.test_request_context(): from flask import session as flask_session flask_session["username"] = "testuser" with patch( "local_deep_research.database.session_context.get_user_db_session" ) as mock_ctx: mock_session = Mock() mock_ctx.return_value.__enter__ = Mock( return_value=mock_session ) mock_ctx.return_value.__exit__ = Mock(return_value=False) result = test_func("value1", "value2") assert result == (mock_session, "value1", "value2") def test_decorator_passes_kwargs(self, app): """Test that decorator passes keyword arguments.""" from local_deep_research.database.session_context import ( with_user_database, ) @with_user_database def test_func(db_session, key1=None, key2=None): return {"session": db_session, "key1": key1, "key2": key2} with app.test_request_context(): from flask import session as flask_session flask_session["username"] = "testuser" with patch( "local_deep_research.database.session_context.get_user_db_session" ) as mock_ctx: mock_session = Mock() mock_ctx.return_value.__enter__ = Mock( return_value=mock_session ) mock_ctx.return_value.__exit__ = Mock(return_value=False) result = test_func(key1="a", key2="b") assert result["key1"] == "a" assert result["key2"] == "b" def test_decorator_extracts_special_kwargs(self, app): """Test that _username and _password are extracted from kwargs.""" from local_deep_research.database.session_context import ( with_user_database, ) @with_user_database def test_func(db_session): return db_session with app.app_context(): with patch( "local_deep_research.database.session_context.get_user_db_session" ) as mock_ctx: mock_session = Mock() mock_ctx.return_value.__enter__ = Mock( return_value=mock_session ) mock_ctx.return_value.__exit__ = Mock(return_value=False) test_func(_username="custom_user", _password="custom_pass") # Verify get_user_db_session was called with the custom credentials mock_ctx.assert_called_once_with("custom_user", "custom_pass") class TestDatabaseAccessMixin: """Tests for DatabaseAccessMixin class.""" def test_get_db_session_raises_deprecation_warning(self, app): """Test that get_db_session raises DeprecationWarning. The method was deprecated because it returned a closed session (context manager exits before returning). """ from local_deep_research.database.session_context import ( DatabaseAccessMixin, ) import pytest class TestService(DatabaseAccessMixin): pass service = TestService() with pytest.raises(DeprecationWarning) as exc_info: service.get_db_session() assert "deprecated" in str(exc_info.value).lower() assert "get_user_db_session" in str(exc_info.value) class TestUnencryptedDbPlaceholder: """Tests for UNENCRYPTED_DB_PLACEHOLDER constant.""" def test_placeholder_value(self): """Test the placeholder constant value.""" from local_deep_research.database.session_context import ( UNENCRYPTED_DB_PLACEHOLDER, ) assert UNENCRYPTED_DB_PLACEHOLDER == "unencrypted-mode" assert isinstance(UNENCRYPTED_DB_PLACEHOLDER, str) class TestSafeRollback: """Tests for the safe_rollback helper introduced for issue #3827.""" def test_calls_session_rollback(self): from local_deep_research.database.session_context import ( safe_rollback, ) session = Mock() safe_rollback(session, "ctx") session.rollback.assert_called_once() def test_swallows_rollback_exception(self): """Rollback errors must not propagate — call sites are exception handlers themselves and a raise here would mask the original error. """ from local_deep_research.database.session_context import ( safe_rollback, ) session = Mock() session.rollback.side_effect = RuntimeError("simulated rollback fail") # Must NOT raise. safe_rollback(session, "ctx") session.rollback.assert_called_once() def test_works_without_context_argument(self): from local_deep_research.database.session_context import ( safe_rollback, ) session = Mock() safe_rollback(session) session.rollback.assert_called_once()