""" Tests for SQLCipher missing scenario. Verifies that users get helpful error messages when SQLCipher is not installed and that the LDR_BOOTSTRAP_ALLOW_UNENCRYPTED workaround works. """ import os import pytest from unittest.mock import patch from tests.test_utils import add_src_to_path add_src_to_path() def _clear_allow_unencrypted_env(): """Clear both canonical and deprecated env vars.""" os.environ.pop("LDR_BOOTSTRAP_ALLOW_UNENCRYPTED", None) os.environ.pop("LDR_ALLOW_UNENCRYPTED", None) class TestSQLCipherMissing: """Test behavior when SQLCipher is not available.""" def test_error_message_mentions_sqlcipher(self): """Error message should mention SQLCipher so users know what's missing.""" from local_deep_research.database.encrypted_db import ( DatabaseManager, ) old_canonical = os.environ.pop("LDR_BOOTSTRAP_ALLOW_UNENCRYPTED", None) old_deprecated = os.environ.pop("LDR_ALLOW_UNENCRYPTED", None) try: with patch( "local_deep_research.database.encrypted_db.get_sqlcipher_module" ) as mock_get: mock_get.side_effect = ImportError( "No module named 'sqlcipher3'" ) with pytest.raises(RuntimeError) as exc_info: DatabaseManager() error_msg = str(exc_info.value) assert "SQLCipher" in error_msg, ( f"Error should mention SQLCipher. Got: {error_msg}" ) finally: if old_canonical is not None: os.environ["LDR_BOOTSTRAP_ALLOW_UNENCRYPTED"] = old_canonical if old_deprecated is not None: os.environ["LDR_ALLOW_UNENCRYPTED"] = old_deprecated def test_error_message_mentions_workaround(self): """Error message should mention LDR_BOOTSTRAP_ALLOW_UNENCRYPTED workaround.""" from local_deep_research.database.encrypted_db import ( DatabaseManager, ) old_canonical = os.environ.pop("LDR_BOOTSTRAP_ALLOW_UNENCRYPTED", None) old_deprecated = os.environ.pop("LDR_ALLOW_UNENCRYPTED", None) try: with patch( "local_deep_research.database.encrypted_db.get_sqlcipher_module" ) as mock_get: mock_get.side_effect = ImportError( "No module named 'sqlcipher3'" ) with pytest.raises(RuntimeError) as exc_info: DatabaseManager() error_msg = str(exc_info.value) assert "LDR_BOOTSTRAP_ALLOW_UNENCRYPTED" in error_msg, ( f"Error should mention workaround. Got: {error_msg}" ) finally: if old_canonical is not None: os.environ["LDR_BOOTSTRAP_ALLOW_UNENCRYPTED"] = old_canonical if old_deprecated is not None: os.environ["LDR_ALLOW_UNENCRYPTED"] = old_deprecated def test_canonical_workaround_allows_startup_without_encryption(self): """LDR_BOOTSTRAP_ALLOW_UNENCRYPTED=true should allow startup without SQLCipher.""" from local_deep_research.database.encrypted_db import ( DatabaseManager, ) old_canonical = os.environ.get("LDR_BOOTSTRAP_ALLOW_UNENCRYPTED") old_deprecated = os.environ.get("LDR_ALLOW_UNENCRYPTED") _clear_allow_unencrypted_env() os.environ["LDR_BOOTSTRAP_ALLOW_UNENCRYPTED"] = "true" try: with patch( "local_deep_research.database.encrypted_db.get_sqlcipher_module" ) as mock_get: mock_get.side_effect = ImportError( "No module named 'sqlcipher3'" ) # Should NOT raise manager = DatabaseManager() assert manager.has_encryption is False, ( "With workaround and no SQLCipher, has_encryption should be False" ) finally: _clear_allow_unencrypted_env() if old_canonical is not None: os.environ["LDR_BOOTSTRAP_ALLOW_UNENCRYPTED"] = old_canonical if old_deprecated is not None: os.environ["LDR_ALLOW_UNENCRYPTED"] = old_deprecated def test_deprecated_workaround_still_works(self): """Deprecated LDR_ALLOW_UNENCRYPTED=true should still allow startup (backward compat).""" from local_deep_research.database.encrypted_db import ( DatabaseManager, ) old_canonical = os.environ.get("LDR_BOOTSTRAP_ALLOW_UNENCRYPTED") old_deprecated = os.environ.get("LDR_ALLOW_UNENCRYPTED") _clear_allow_unencrypted_env() os.environ["LDR_ALLOW_UNENCRYPTED"] = "true" try: with patch( "local_deep_research.database.encrypted_db.get_sqlcipher_module" ) as mock_get: mock_get.side_effect = ImportError( "No module named 'sqlcipher3'" ) # Should NOT raise (backward compatibility) manager = DatabaseManager() assert manager.has_encryption is False, ( "With deprecated workaround and no SQLCipher, has_encryption should be False" ) finally: _clear_allow_unencrypted_env() if old_canonical is not None: os.environ["LDR_BOOTSTRAP_ALLOW_UNENCRYPTED"] = old_canonical if old_deprecated is not None: os.environ["LDR_ALLOW_UNENCRYPTED"] = old_deprecated def test_db_manager_has_encryption_is_boolean(self): """db_manager.has_encryption should be a boolean.""" from local_deep_research.database.encrypted_db import db_manager assert isinstance(db_manager.has_encryption, bool), ( f"has_encryption should be bool, got {type(db_manager.has_encryption)}" ) class TestAllowUnencryptedEdgeCases: """Edge cases for LDR_BOOTSTRAP_ALLOW_UNENCRYPTED fallback. The allow_unencrypted setting uses the BooleanSetting from the env settings registry, which accepts "true", "1", "yes", "on", "enabled" (case-insensitive) as truthy values and handles deprecated alias fallback automatically. """ def test_canonical_empty_string_does_not_fall_through(self): """Empty string canonical should NOT fall through to deprecated. When LDR_BOOTSTRAP_ALLOW_UNENCRYPTED="" is set, it takes precedence and the check (empty_string or "").lower() == "true" is False. """ from local_deep_research.database.encrypted_db import ( DatabaseManager, ) _clear_allow_unencrypted_env() os.environ["LDR_BOOTSTRAP_ALLOW_UNENCRYPTED"] = "" os.environ["LDR_ALLOW_UNENCRYPTED"] = "true" try: with patch( "local_deep_research.database.encrypted_db.get_sqlcipher_module" ) as mock_get: mock_get.side_effect = ImportError( "No module named 'sqlcipher3'" ) # Should raise because empty string is not "true" with pytest.raises(RuntimeError): DatabaseManager() finally: _clear_allow_unencrypted_env() def test_uppercase_true_not_accepted(self): """'TRUE' (uppercase) is not accepted as true. The code uses .lower() == "true", so "TRUE".lower() == "true" is True. """ from local_deep_research.database.encrypted_db import ( DatabaseManager, ) _clear_allow_unencrypted_env() os.environ["LDR_BOOTSTRAP_ALLOW_UNENCRYPTED"] = "TRUE" try: with patch( "local_deep_research.database.encrypted_db.get_sqlcipher_module" ) as mock_get: mock_get.side_effect = ImportError( "No module named 'sqlcipher3'" ) # Should NOT raise - "TRUE".lower() == "true" is True manager = DatabaseManager() assert manager.has_encryption is False finally: _clear_allow_unencrypted_env() def test_mixed_case_true_accepted(self): """'True' (mixed case) is accepted as true due to .lower().""" from local_deep_research.database.encrypted_db import ( DatabaseManager, ) _clear_allow_unencrypted_env() os.environ["LDR_BOOTSTRAP_ALLOW_UNENCRYPTED"] = "True" try: with patch( "local_deep_research.database.encrypted_db.get_sqlcipher_module" ) as mock_get: mock_get.side_effect = ImportError( "No module named 'sqlcipher3'" ) # Should NOT raise - "True".lower() == "true" is True manager = DatabaseManager() assert manager.has_encryption is False finally: _clear_allow_unencrypted_env() def test_whitespace_padded_true_not_accepted(self): """' true ' (whitespace padded) is not accepted as true. The code doesn't strip whitespace, so " true " != "true". """ from local_deep_research.database.encrypted_db import ( DatabaseManager, ) _clear_allow_unencrypted_env() os.environ["LDR_BOOTSTRAP_ALLOW_UNENCRYPTED"] = " true " try: with patch( "local_deep_research.database.encrypted_db.get_sqlcipher_module" ) as mock_get: mock_get.side_effect = ImportError( "No module named 'sqlcipher3'" ) # Should raise - " true " != "true" with pytest.raises(RuntimeError): DatabaseManager() finally: _clear_allow_unencrypted_env() def test_yes_accepted_as_true(self): """'yes' is accepted as true by BooleanSetting.""" from local_deep_research.database.encrypted_db import ( DatabaseManager, ) _clear_allow_unencrypted_env() os.environ["LDR_BOOTSTRAP_ALLOW_UNENCRYPTED"] = "yes" try: with patch( "local_deep_research.database.encrypted_db.get_sqlcipher_module" ) as mock_get: mock_get.side_effect = ImportError( "No module named 'sqlcipher3'" ) # Should NOT raise - BooleanSetting accepts "yes" manager = DatabaseManager() assert manager.has_encryption is False finally: _clear_allow_unencrypted_env() def test_one_accepted_as_true(self): """'1' is accepted as true by BooleanSetting.""" from local_deep_research.database.encrypted_db import ( DatabaseManager, ) _clear_allow_unencrypted_env() os.environ["LDR_BOOTSTRAP_ALLOW_UNENCRYPTED"] = "1" try: with patch( "local_deep_research.database.encrypted_db.get_sqlcipher_module" ) as mock_get: mock_get.side_effect = ImportError( "No module named 'sqlcipher3'" ) # Should NOT raise - BooleanSetting accepts "1" manager = DatabaseManager() assert manager.has_encryption is False finally: _clear_allow_unencrypted_env() def test_canonical_false_does_not_check_deprecated(self): """canonical='false' should NOT fall through to deprecated='true'. Once canonical is set (to any value), deprecated is not checked. """ from local_deep_research.database.encrypted_db import ( DatabaseManager, ) _clear_allow_unencrypted_env() os.environ["LDR_BOOTSTRAP_ALLOW_UNENCRYPTED"] = "false" os.environ["LDR_ALLOW_UNENCRYPTED"] = "true" try: with patch( "local_deep_research.database.encrypted_db.get_sqlcipher_module" ) as mock_get: mock_get.side_effect = ImportError( "No module named 'sqlcipher3'" ) # Should raise - canonical "false" takes precedence with pytest.raises(RuntimeError): DatabaseManager() finally: _clear_allow_unencrypted_env() def test_empty_canonical_with_deprecated_true_raises(self): """Empty canonical with deprecated='true' - empty takes precedence. When canonical is "", the deprecated is not checked because canonical is "set" (to empty string). """ from local_deep_research.database.encrypted_db import ( DatabaseManager, ) _clear_allow_unencrypted_env() os.environ["LDR_BOOTSTRAP_ALLOW_UNENCRYPTED"] = "" os.environ["LDR_ALLOW_UNENCRYPTED"] = "true" try: with patch( "local_deep_research.database.encrypted_db.get_sqlcipher_module" ) as mock_get: mock_get.side_effect = ImportError( "No module named 'sqlcipher3'" ) # Should raise - empty string canonical takes precedence with pytest.raises(RuntimeError): DatabaseManager() finally: _clear_allow_unencrypted_env() def test_only_deprecated_set_to_true_works(self): """When only deprecated is set to 'true', it should work.""" from local_deep_research.database.encrypted_db import ( DatabaseManager, ) _clear_allow_unencrypted_env() os.environ["LDR_ALLOW_UNENCRYPTED"] = "true" try: with patch( "local_deep_research.database.encrypted_db.get_sqlcipher_module" ) as mock_get: mock_get.side_effect = ImportError( "No module named 'sqlcipher3'" ) # Should NOT raise - deprecated "true" works manager = DatabaseManager() assert manager.has_encryption is False finally: _clear_allow_unencrypted_env() def test_deprecated_uppercase_true_accepted(self): """Deprecated 'TRUE' (uppercase) works due to .lower().""" from local_deep_research.database.encrypted_db import ( DatabaseManager, ) _clear_allow_unencrypted_env() os.environ["LDR_ALLOW_UNENCRYPTED"] = "TRUE" try: with patch( "local_deep_research.database.encrypted_db.get_sqlcipher_module" ) as mock_get: mock_get.side_effect = ImportError( "No module named 'sqlcipher3'" ) # Should NOT raise - "TRUE".lower() == "true" manager = DatabaseManager() assert manager.has_encryption is False finally: _clear_allow_unencrypted_env() def test_neither_set_raises_runtime_error(self): """When neither canonical nor deprecated is set, RuntimeError is raised.""" from local_deep_research.database.encrypted_db import ( DatabaseManager, ) _clear_allow_unencrypted_env() try: with patch( "local_deep_research.database.encrypted_db.get_sqlcipher_module" ) as mock_get: mock_get.side_effect = ImportError( "No module named 'sqlcipher3'" ) with pytest.raises(RuntimeError): DatabaseManager() finally: _clear_allow_unencrypted_env()