"""Tests for database/sqlcipher_utils.py and env settings deprecated alias support.""" import os import pytest from unittest.mock import Mock, patch class TestEnvSettingDeprecatedAlias: """Tests for EnvSetting deprecated_env_var support in the settings infrastructure.""" def test_canonical_env_var_used_when_set(self): """Test that canonical env var is used when set.""" from local_deep_research.settings.env_settings import StringSetting setting = StringSetting( key="test.canonical", description="test", default="default_val", deprecated_env_var="OLD_TEST_VAR", ) with patch.dict( os.environ, { "LDR_TEST_CANONICAL": "canonical_value", "OLD_TEST_VAR": "deprecated_value", }, clear=True, ): assert setting.get_value() == "canonical_value" def test_deprecated_fallback_when_canonical_not_set(self): """Test that deprecated env var is used when canonical not set.""" from local_deep_research.settings.env_settings import StringSetting setting = StringSetting( key="test.canonical", description="test", default="default_val", deprecated_env_var="OLD_TEST_VAR", ) with patch.dict( os.environ, {"OLD_TEST_VAR": "deprecated_value"}, clear=True ): assert setting.get_value() == "deprecated_value" def test_default_when_neither_set(self): """Test that default is returned when neither env var is set.""" from local_deep_research.settings.env_settings import StringSetting setting = StringSetting( key="test.canonical", description="test", default="default_val", deprecated_env_var="OLD_TEST_VAR", ) with patch.dict(os.environ, {}, clear=True): assert setting.get_value() == "default_val" def test_canonical_takes_precedence(self): """Test that canonical value takes precedence even when both are set.""" from local_deep_research.settings.env_settings import StringSetting setting = StringSetting( key="test.canonical", description="test", default="default_val", deprecated_env_var="OLD_TEST_VAR", ) with patch.dict( os.environ, {"LDR_TEST_CANONICAL": "canonical", "OLD_TEST_VAR": "deprecated"}, clear=True, ): assert setting.get_value() == "canonical" def test_empty_string_canonical_takes_precedence(self): """Empty string canonical is treated as 'set' and takes precedence.""" from local_deep_research.settings.env_settings import StringSetting setting = StringSetting( key="test.canonical", description="test", default="default_val", deprecated_env_var="OLD_TEST_VAR", ) with patch.dict( os.environ, {"LDR_TEST_CANONICAL": "", "OLD_TEST_VAR": "deprecated_value"}, clear=True, ): assert setting.get_value() == "" def test_deprecated_warning_logged(self, loguru_caplog): """Using deprecated env var logs a deprecation warning.""" from local_deep_research.settings.env_settings import StringSetting setting = StringSetting( key="test.canonical", description="test", default="default_val", deprecated_env_var="OLD_TEST_VAR", ) loguru_caplog.set_level("WARNING") with patch.dict(os.environ, {"OLD_TEST_VAR": "value"}, clear=True): setting.get_value() assert "OLD_TEST_VAR" in loguru_caplog.text assert "deprecated" in loguru_caplog.text.lower() assert "LDR_TEST_CANONICAL" in loguru_caplog.text def test_canonical_no_warning(self, loguru_caplog): """Using canonical env var does not log a warning.""" from local_deep_research.settings.env_settings import StringSetting setting = StringSetting( key="test.canonical", description="test", default="default_val", deprecated_env_var="OLD_TEST_VAR", ) loguru_caplog.set_level("WARNING") with patch.dict( os.environ, {"LDR_TEST_CANONICAL": "value"}, clear=True ): setting.get_value() assert "deprecated" not in loguru_caplog.text.lower() def test_default_no_warning(self, loguru_caplog): """Using default (neither set) does not log a warning.""" from local_deep_research.settings.env_settings import StringSetting setting = StringSetting( key="test.canonical", description="test", default="default_val", deprecated_env_var="OLD_TEST_VAR", ) loguru_caplog.set_level("WARNING") with patch.dict(os.environ, {}, clear=True): setting.get_value() assert "deprecated" not in loguru_caplog.text.lower() def test_integer_setting_with_deprecated_alias(self): """IntegerSetting handles deprecated alias correctly.""" from local_deep_research.settings.env_settings import IntegerSetting setting = IntegerSetting( key="test.count", description="test", default=42, min_value=1, max_value=1000, deprecated_env_var="OLD_COUNT", ) with patch.dict(os.environ, {"OLD_COUNT": "100"}, clear=True): assert setting.get_value() == 100 def test_enum_setting_with_deprecated_alias(self): """EnumSetting handles deprecated alias correctly.""" from local_deep_research.settings.env_settings import EnumSetting setting = EnumSetting( key="test.mode", description="test", allowed_values={"FAST", "SLOW"}, default="FAST", case_sensitive=False, deprecated_env_var="OLD_MODE", ) with patch.dict(os.environ, {"OLD_MODE": "slow"}, clear=True): assert setting.get_value() == "SLOW" def test_boolean_setting_with_deprecated_alias(self): """BooleanSetting handles deprecated alias correctly.""" from local_deep_research.settings.env_settings import BooleanSetting setting = BooleanSetting( key="test.flag", description="test", default=False, deprecated_env_var="OLD_FLAG", ) with patch.dict(os.environ, {"OLD_FLAG": "true"}, clear=True): assert setting.get_value() is True class TestGetSqlcipherSettings: """Tests for get_sqlcipher_settings function.""" def test_returns_default_values(self): """Test that default values are returned when no env vars set.""" from local_deep_research.database.sqlcipher_utils import ( get_sqlcipher_settings, DEFAULT_KDF_ITERATIONS, DEFAULT_PAGE_SIZE, DEFAULT_HMAC_ALGORITHM, DEFAULT_KDF_ALGORITHM, ) with patch.dict(os.environ, {}, clear=True): settings = get_sqlcipher_settings() assert settings["kdf_iterations"] == DEFAULT_KDF_ITERATIONS assert settings["page_size"] == DEFAULT_PAGE_SIZE assert settings["hmac_algorithm"] == DEFAULT_HMAC_ALGORITHM assert settings["kdf_algorithm"] == DEFAULT_KDF_ALGORITHM def test_respects_canonical_env_var_kdf_iterations(self): """Test that canonical LDR_DB_CONFIG_KDF_ITERATIONS env var is respected.""" from local_deep_research.database.sqlcipher_utils import ( get_sqlcipher_settings, ) with patch.dict(os.environ, {"LDR_DB_CONFIG_KDF_ITERATIONS": "100000"}): settings = get_sqlcipher_settings() assert settings["kdf_iterations"] == 100000 def test_respects_deprecated_env_var_kdf_iterations(self): """Test that deprecated LDR_DB_KDF_ITERATIONS env var still works (backward compat).""" from local_deep_research.database.sqlcipher_utils import ( get_sqlcipher_settings, ) with patch.dict( os.environ, {"LDR_DB_KDF_ITERATIONS": "100000"}, clear=True ): settings = get_sqlcipher_settings() assert settings["kdf_iterations"] == 100000 def test_respects_canonical_env_var_page_size(self): """Test that canonical LDR_DB_CONFIG_PAGE_SIZE env var is respected.""" from local_deep_research.database.sqlcipher_utils import ( get_sqlcipher_settings, ) with patch.dict(os.environ, {"LDR_DB_CONFIG_PAGE_SIZE": "8192"}): settings = get_sqlcipher_settings() assert settings["page_size"] == 8192 def test_respects_deprecated_env_var_page_size(self): """Test that deprecated LDR_DB_PAGE_SIZE env var still works (backward compat).""" from local_deep_research.database.sqlcipher_utils import ( get_sqlcipher_settings, ) with patch.dict(os.environ, {"LDR_DB_PAGE_SIZE": "8192"}, clear=True): settings = get_sqlcipher_settings() assert settings["page_size"] == 8192 def test_respects_canonical_env_var_hmac_algorithm(self): """Test that canonical LDR_DB_CONFIG_HMAC_ALGORITHM env var is respected.""" from local_deep_research.database.sqlcipher_utils import ( get_sqlcipher_settings, ) with patch.dict( os.environ, {"LDR_DB_CONFIG_HMAC_ALGORITHM": "HMAC_SHA256"} ): settings = get_sqlcipher_settings() assert settings["hmac_algorithm"] == "HMAC_SHA256" def test_respects_deprecated_env_var_hmac_algorithm(self): """Test that deprecated LDR_DB_HMAC_ALGORITHM env var still works (backward compat).""" from local_deep_research.database.sqlcipher_utils import ( get_sqlcipher_settings, ) with patch.dict( os.environ, {"LDR_DB_HMAC_ALGORITHM": "HMAC_SHA256"}, clear=True ): settings = get_sqlcipher_settings() assert settings["hmac_algorithm"] == "HMAC_SHA256" def test_canonical_takes_precedence_over_deprecated(self): """Test that canonical env var takes precedence over deprecated.""" from local_deep_research.database.sqlcipher_utils import ( get_sqlcipher_settings, ) with patch.dict( os.environ, { "LDR_DB_CONFIG_KDF_ITERATIONS": "200000", "LDR_DB_KDF_ITERATIONS": "100000", # Should be ignored }, ): settings = get_sqlcipher_settings() assert settings["kdf_iterations"] == 200000 def test_returns_dict_type(self): """Test that settings returns a dictionary.""" from local_deep_research.database.sqlcipher_utils import ( get_sqlcipher_settings, ) settings = get_sqlcipher_settings() assert isinstance(settings, dict) assert "kdf_iterations" in settings assert "page_size" in settings assert "hmac_algorithm" in settings assert "kdf_algorithm" in settings class TestSetSqlcipherKey: """Tests for set_sqlcipher_key function.""" def test_executes_pragma_key_command(self): """Test that PRAGMA key is executed with hex-encoded password.""" from local_deep_research.database.sqlcipher_utils import ( set_sqlcipher_key, ) mock_cursor = Mock() with patch( "local_deep_research.database.sqlcipher_utils._get_key_from_password" ) as mock_get_key: mock_get_key.return_value = b"\x01\x02\x03" set_sqlcipher_key(mock_cursor, "testpass") # Check that execute was called with PRAGMA key mock_cursor.execute.assert_called_once() call_args = mock_cursor.execute.call_args[0][0] assert "PRAGMA key" in call_args assert "x'" in call_args class TestSetSqlcipherKeyFromHex: """Tests for set_sqlcipher_key_from_hex function.""" def test_executes_pragma_key_with_hex(self): """Test that PRAGMA key is executed with pre-derived hex key.""" from local_deep_research.database.sqlcipher_utils import ( set_sqlcipher_key_from_hex, ) mock_cursor = Mock() set_sqlcipher_key_from_hex(mock_cursor, "abcdef0123456789") mock_cursor.execute.assert_called_once() call_args = mock_cursor.execute.call_args[0][0] assert "PRAGMA key" in call_args assert "abcdef0123456789" in call_args class TestApplySqlcipherPragmas: """Tests for apply_sqlcipher_pragmas function.""" def test_applies_core_pragmas_existing_db(self): """Test that cipher_* and kdf_iter PRAGMAs are applied for existing DBs.""" from local_deep_research.database.sqlcipher_utils import ( apply_sqlcipher_pragmas, ) mock_cursor = Mock() apply_sqlcipher_pragmas(mock_cursor, creation_mode=False) call_args_list = [ call[0][0] for call in mock_cursor.execute.call_args_list ] # For existing DBs, cipher_* pragmas go AFTER the key assert any("cipher_page_size" in arg for arg in call_args_list) assert any("cipher_hmac_algorithm" in arg for arg in call_args_list) assert any("cipher_kdf_algorithm" in arg for arg in call_args_list) assert any("kdf_iter" in arg for arg in call_args_list) def test_creation_mode_only_sets_kdf_iter(self): """Test that creation mode only sets kdf_iter (defaults already set before key).""" from local_deep_research.database.sqlcipher_utils import ( apply_sqlcipher_pragmas, ) mock_cursor = Mock() apply_sqlcipher_pragmas(mock_cursor, creation_mode=True) call_args_list = [ call[0][0] for call in mock_cursor.execute.call_args_list ] assert any("kdf_iter" in arg for arg in call_args_list) # cipher_* should NOT be set in creation mode (handled by apply_cipher_defaults_before_key) assert not any("cipher_page_size" in arg for arg in call_args_list) assert not any("cipher_hmac_algorithm" in arg for arg in call_args_list) assert not any("cipher_kdf_algorithm" in arg for arg in call_args_list) class TestApplyCipherDefaultsBeforeKey: """Tests for apply_cipher_defaults_before_key function (SQLCipher 4.x).""" def test_applies_cipher_default_settings_for_new_database(self): """Test that cipher_default_* pragmas are set for new databases.""" from local_deep_research.database.sqlcipher_utils import ( apply_cipher_defaults_before_key, ) mock_cursor = Mock() apply_cipher_defaults_before_key(mock_cursor) call_args_list = [ call[0][0] for call in mock_cursor.execute.call_args_list ] # Should use cipher_default_* prefix for new databases assert any("cipher_default_page_size" in arg for arg in call_args_list) assert any( "cipher_default_hmac_algorithm" in arg for arg in call_args_list ) assert any( "cipher_default_kdf_algorithm" in arg for arg in call_args_list ) def test_uses_settings_from_get_sqlcipher_settings(self): """Test that settings values come from get_sqlcipher_settings.""" from local_deep_research.database.sqlcipher_utils import ( apply_cipher_defaults_before_key, ) mock_cursor = Mock() with patch.dict(os.environ, {"LDR_DB_PAGE_SIZE": "4096"}): apply_cipher_defaults_before_key(mock_cursor) call_args_list = [ call[0][0] for call in mock_cursor.execute.call_args_list ] page_size_call = [arg for arg in call_args_list if "page_size" in arg][ 0 ] assert "4096" in page_size_call def test_backwards_compat_alias(self): """Test that apply_cipher_settings_before_key alias works.""" from local_deep_research.database.sqlcipher_utils import ( apply_cipher_settings_before_key, apply_cipher_defaults_before_key, ) assert ( apply_cipher_settings_before_key is apply_cipher_defaults_before_key ) class TestApplyPerformancePragmas: """Tests for apply_performance_pragmas function.""" def test_applies_default_performance_pragmas(self): """Test that default performance pragmas are applied.""" from local_deep_research.database.sqlcipher_utils import ( apply_performance_pragmas, ) mock_cursor = Mock() apply_performance_pragmas(mock_cursor) call_args_list = [ call[0][0] for call in mock_cursor.execute.call_args_list ] assert any("temp_store = MEMORY" in arg for arg in call_args_list) assert any("busy_timeout" in arg for arg in call_args_list) assert any("cache_size" in arg for arg in call_args_list) assert any("journal_mode" in arg for arg in call_args_list) assert any("synchronous" in arg for arg in call_args_list) def test_respects_canonical_cache_size_env_var(self): """Test that canonical LDR_DB_CONFIG_CACHE_SIZE_MB env var is respected.""" from local_deep_research.database.sqlcipher_utils import ( apply_performance_pragmas, ) mock_cursor = Mock() with patch.dict(os.environ, {"LDR_DB_CONFIG_CACHE_SIZE_MB": "128"}): apply_performance_pragmas(mock_cursor) call_args_list = [ call[0][0] for call in mock_cursor.execute.call_args_list ] # 128 MB = -131072 KB (negative for KB interpretation) cache_call = [arg for arg in call_args_list if "cache_size" in arg][ 0 ] assert "-131072" in cache_call def test_respects_deprecated_cache_size_env_var(self): """Test that deprecated LDR_DB_CACHE_SIZE_MB env var still works (backward compat).""" from local_deep_research.database.sqlcipher_utils import ( apply_performance_pragmas, ) mock_cursor = Mock() with patch.dict( os.environ, {"LDR_DB_CACHE_SIZE_MB": "128"}, clear=True ): apply_performance_pragmas(mock_cursor) call_args_list = [ call[0][0] for call in mock_cursor.execute.call_args_list ] cache_call = [arg for arg in call_args_list if "cache_size" in arg][ 0 ] assert "-131072" in cache_call def test_respects_canonical_journal_mode_env_var(self): """Test that canonical LDR_DB_CONFIG_JOURNAL_MODE env var is respected.""" from local_deep_research.database.sqlcipher_utils import ( apply_performance_pragmas, ) mock_cursor = Mock() with patch.dict(os.environ, {"LDR_DB_CONFIG_JOURNAL_MODE": "DELETE"}): apply_performance_pragmas(mock_cursor) call_args_list = [ call[0][0] for call in mock_cursor.execute.call_args_list ] journal_call = [ arg for arg in call_args_list if "journal_mode" in arg ][0] assert "DELETE" in journal_call def test_respects_deprecated_journal_mode_env_var(self): """Test that deprecated LDR_DB_JOURNAL_MODE env var still works (backward compat).""" from local_deep_research.database.sqlcipher_utils import ( apply_performance_pragmas, ) mock_cursor = Mock() with patch.dict( os.environ, {"LDR_DB_JOURNAL_MODE": "DELETE"}, clear=True ): apply_performance_pragmas(mock_cursor) call_args_list = [ call[0][0] for call in mock_cursor.execute.call_args_list ] journal_call = [ arg for arg in call_args_list if "journal_mode" in arg ][0] assert "DELETE" in journal_call class TestVerifySqlcipherConnection: """Tests for verify_sqlcipher_connection function.""" def test_returns_true_for_valid_connection(self): """Test that True is returned for valid connection.""" from local_deep_research.database.sqlcipher_utils import ( verify_sqlcipher_connection, ) mock_cursor = Mock() mock_cursor.fetchone.return_value = (1,) result = verify_sqlcipher_connection(mock_cursor) assert result is True def test_returns_false_for_invalid_connection(self): """Test that False is returned for invalid connection.""" from local_deep_research.database.sqlcipher_utils import ( verify_sqlcipher_connection, ) mock_cursor = Mock() mock_cursor.execute.side_effect = Exception("Connection error") result = verify_sqlcipher_connection(mock_cursor) assert result is False def test_returns_false_for_wrong_result(self): """Test that False is returned when SELECT 1 returns wrong value.""" from local_deep_research.database.sqlcipher_utils import ( verify_sqlcipher_connection, ) mock_cursor = Mock() mock_cursor.fetchone.return_value = (0,) result = verify_sqlcipher_connection(mock_cursor) assert result is False class TestGetSqlcipherVersion: """Tests for get_sqlcipher_version function.""" def test_returns_version_string(self): """Test that version string is returned.""" from local_deep_research.database.sqlcipher_utils import ( get_sqlcipher_version, ) mock_cursor = Mock() mock_cursor.fetchone.return_value = ("4.6.1 community",) result = get_sqlcipher_version(mock_cursor) assert result == "4.6.1 community" def test_returns_none_on_error(self): """Test that None is returned on error.""" from local_deep_research.database.sqlcipher_utils import ( get_sqlcipher_version, ) mock_cursor = Mock() mock_cursor.execute.side_effect = Exception("Not supported") result = get_sqlcipher_version(mock_cursor) assert result is None class TestConstants: """Tests for module constants.""" def test_default_kdf_iterations_is_reasonable(self): """Test that default KDF iterations is a reasonable security value.""" from local_deep_research.database.sqlcipher_utils import ( DEFAULT_KDF_ITERATIONS, ) # Should be at least 100000 for security assert DEFAULT_KDF_ITERATIONS >= 100000 assert isinstance(DEFAULT_KDF_ITERATIONS, int) def test_default_page_size_is_power_of_two(self): """Test that default page size is a power of 2.""" from local_deep_research.database.sqlcipher_utils import ( DEFAULT_PAGE_SIZE, ) # Page size should be a power of 2 assert DEFAULT_PAGE_SIZE > 0 assert (DEFAULT_PAGE_SIZE & (DEFAULT_PAGE_SIZE - 1)) == 0 def test_pbkdf2_placeholder_salt_exists(self): """Test that the PBKDF2 placeholder salt is defined.""" from local_deep_research.database.sqlcipher_utils import ( PBKDF2_PLACEHOLDER_SALT, ) assert PBKDF2_PLACEHOLDER_SALT is not None assert isinstance(PBKDF2_PLACEHOLDER_SALT, bytes) class TestCreateSqlcipherConnection: """Tests for create_sqlcipher_connection function.""" def test_raises_import_error_when_sqlcipher_unavailable(self): """Test that ImportError is raised when sqlcipher3 not available.""" from local_deep_research.database.sqlcipher_utils import ( create_sqlcipher_connection, ) with patch( "local_deep_research.database.sqlcipher_compat.get_sqlcipher_module", side_effect=ImportError("No module"), ): with pytest.raises( ImportError, match="sqlcipher3 is not available" ): create_sqlcipher_connection("/tmp/test.db", password="password") def test_creates_connection_with_correct_password(self): """Test that connection is created with correct password handling.""" from local_deep_research.database.sqlcipher_utils import ( create_sqlcipher_connection, ) mock_sqlcipher = Mock() mock_conn = Mock() mock_cursor = Mock() mock_cursor.fetchone.return_value = (1,) mock_conn.cursor.return_value = mock_cursor mock_sqlcipher.connect.return_value = mock_conn with patch( "local_deep_research.database.sqlcipher_compat.get_sqlcipher_module", return_value=mock_sqlcipher, ): with patch( "local_deep_research.database.sqlcipher_utils.set_sqlcipher_key" ) as mock_set_key: create_sqlcipher_connection( "/tmp/test.db", password="mypassword" ) mock_sqlcipher.connect.assert_called_once_with("/tmp/test.db") mock_set_key.assert_called_once() def test_raises_value_error_on_verification_failure(self): """Test that ValueError is raised when connection verification fails.""" from local_deep_research.database.sqlcipher_utils import ( create_sqlcipher_connection, ) mock_sqlcipher = Mock() mock_conn = Mock() mock_cursor = Mock() mock_cursor.fetchone.return_value = (0,) # Wrong result mock_conn.cursor.return_value = mock_cursor mock_sqlcipher.connect.return_value = mock_conn with patch( "local_deep_research.database.sqlcipher_compat.get_sqlcipher_module", return_value=mock_sqlcipher, ): with patch( "local_deep_research.database.sqlcipher_utils.set_sqlcipher_key" ): with pytest.raises(ValueError, match="Failed to establish"): create_sqlcipher_connection( "/tmp/test.db", password="badpassword" ) def test_closes_conn_on_failure(self): """Test that connection is closed when setup fails.""" from local_deep_research.database.sqlcipher_utils import ( create_sqlcipher_connection, ) mock_sqlcipher = Mock() mock_conn = Mock() mock_cursor = Mock() mock_cursor.fetchone.return_value = (0,) mock_conn.cursor.return_value = mock_cursor mock_sqlcipher.connect.return_value = mock_conn with patch( "local_deep_research.database.sqlcipher_compat.get_sqlcipher_module", return_value=mock_sqlcipher, ): with patch( "local_deep_research.database.sqlcipher_utils.set_sqlcipher_key" ): with pytest.raises(ValueError): create_sqlcipher_connection("/tmp/test.db", password="bad") mock_conn.close.assert_called_once() def test_accepts_hex_key(self): """Test that hex_key parameter works.""" from local_deep_research.database.sqlcipher_utils import ( create_sqlcipher_connection, ) mock_sqlcipher = Mock() mock_conn = Mock() mock_cursor = Mock() mock_cursor.fetchone.return_value = (1,) mock_conn.cursor.return_value = mock_cursor mock_sqlcipher.connect.return_value = mock_conn with patch( "local_deep_research.database.sqlcipher_compat.get_sqlcipher_module", return_value=mock_sqlcipher, ): with patch( "local_deep_research.database.sqlcipher_utils.set_sqlcipher_key_from_hex" ) as mock_hex_key: create_sqlcipher_connection("/tmp/test.db", hex_key="abcdef") mock_hex_key.assert_called_once() class TestPragmaOrder: """Tests for correct PRAGMA execution order.""" def test_sqlcipher_4x_pragma_order_existing_db(self): """Verify correct order for opening existing DB: key -> cipher_* -> kdf_iter -> verify.""" from local_deep_research.database.sqlcipher_utils import ( create_sqlcipher_connection, ) execution_order = [] def track_execute(sql, *args, **kwargs): execution_order.append(sql) mock_cursor = Mock() mock_cursor.execute = Mock(side_effect=track_execute) mock_cursor.fetchone.return_value = (1,) mock_conn = Mock() mock_conn.cursor.return_value = mock_cursor mock_sqlcipher = Mock() mock_sqlcipher.connect.return_value = mock_conn with patch( "local_deep_research.database.sqlcipher_compat.get_sqlcipher_module", return_value=mock_sqlcipher, ): with patch( "local_deep_research.database.sqlcipher_utils._get_key_from_password", return_value=b"\x01\x02\x03", ): create_sqlcipher_connection("/tmp/test.db", password="password") # Find indices of key operations key_idx = next( i for i, sql in enumerate(execution_order) if "PRAGMA key" in sql ) cipher_page_idx = next( i for i, sql in enumerate(execution_order) if "cipher_page_size" in sql ) kdf_iter_idx = next( i for i, sql in enumerate(execution_order) if "kdf_iter" in sql ) verify_idx = next( i for i, sql in enumerate(execution_order) if "SELECT 1" in sql ) # For existing DB: key BEFORE cipher_page_size (cipher_* come after key) assert key_idx < cipher_page_idx, ( "PRAGMA key must be set BEFORE cipher_page_size for existing DBs" ) # kdf_iter comes AFTER the key assert key_idx < kdf_iter_idx, "kdf_iter must be set AFTER key" # Verify (SELECT 1) must be AFTER all pragmas assert verify_idx > kdf_iter_idx, "Verification must be after kdf_iter" def test_sqlcipher_4x_pragma_order_new_db(self): """Verify correct order for new DB: cipher_default_* -> key -> kdf_iter -> verify.""" from local_deep_research.database.sqlcipher_utils import ( create_sqlcipher_connection, ) execution_order = [] def track_execute(sql, *args, **kwargs): execution_order.append(sql) mock_cursor = Mock() mock_cursor.execute = Mock(side_effect=track_execute) mock_cursor.fetchone.return_value = (1,) mock_conn = Mock() mock_conn.cursor.return_value = mock_cursor mock_sqlcipher = Mock() mock_sqlcipher.connect.return_value = mock_conn with patch( "local_deep_research.database.sqlcipher_compat.get_sqlcipher_module", return_value=mock_sqlcipher, ): with patch( "local_deep_research.database.sqlcipher_utils._get_key_from_password", return_value=b"\x01\x02\x03", ): create_sqlcipher_connection( "/tmp/test.db", password="password", creation_mode=True ) # Find indices key_idx = next( i for i, sql in enumerate(execution_order) if "PRAGMA key" in sql ) default_page_idx = next( i for i, sql in enumerate(execution_order) if "cipher_default_page_size" in sql ) # For new DB: cipher_default_* BEFORE key assert default_page_idx < key_idx, ( "cipher_default_page_size must be set BEFORE key for new DBs" ) def test_all_cipher_pragmas_present_in_correct_functions(self): """Verify cipher pragmas are in correct functions for SQLCipher 4.x. - apply_cipher_defaults_before_key(): cipher_default_* (for creation) - apply_sqlcipher_pragmas(creation_mode=False): cipher_* + kdf_iter (for existing) - apply_sqlcipher_pragmas(creation_mode=True): only kdf_iter """ from local_deep_research.database.sqlcipher_utils import ( apply_cipher_defaults_before_key, apply_sqlcipher_pragmas, ) # Test apply_cipher_defaults_before_key - should have cipher_default_* settings mock_cursor_before = Mock() apply_cipher_defaults_before_key(mock_cursor_before) before_key_args = [ call[0][0] for call in mock_cursor_before.execute.call_args_list ] assert any("cipher_default_page_size" in arg for arg in before_key_args) assert any( "cipher_default_hmac_algorithm" in arg for arg in before_key_args ) assert any( "cipher_default_kdf_algorithm" in arg for arg in before_key_args ) # Test apply_sqlcipher_pragmas(creation_mode=False) - should have cipher_* + kdf_iter mock_cursor_existing = Mock() apply_sqlcipher_pragmas(mock_cursor_existing, creation_mode=False) existing_args = [ call[0][0] for call in mock_cursor_existing.execute.call_args_list ] assert any("cipher_page_size" in arg for arg in existing_args) assert any("cipher_hmac_algorithm" in arg for arg in existing_args) assert any("cipher_kdf_algorithm" in arg for arg in existing_args) assert any("kdf_iter" in arg for arg in existing_args) # Test apply_sqlcipher_pragmas(creation_mode=True) - should only have kdf_iter mock_cursor_create = Mock() apply_sqlcipher_pragmas(mock_cursor_create, creation_mode=True) create_args = [ call[0][0] for call in mock_cursor_create.execute.call_args_list ] assert any("kdf_iter" in arg for arg in create_args) assert not any("cipher_page_size" in arg for arg in create_args) class TestGetSqlcipherSettingsValidation: """Tests for type conversion and validation in get_sqlcipher_settings. The registry's typed settings (IntegerSetting, EnumSetting) handle validation and fall back to defaults for invalid values. """ def test_non_numeric_kdf_iterations_uses_default(self): """Non-numeric KDF iterations should fall back to default.""" from local_deep_research.database.sqlcipher_utils import ( get_sqlcipher_settings, DEFAULT_KDF_ITERATIONS, ) with patch.dict( os.environ, {"LDR_DB_CONFIG_KDF_ITERATIONS": "abc"}, clear=True ): settings = get_sqlcipher_settings() assert settings["kdf_iterations"] == DEFAULT_KDF_ITERATIONS def test_empty_string_kdf_iterations_uses_default(self): """Empty string KDF iterations should fall back to default.""" from local_deep_research.database.sqlcipher_utils import ( get_sqlcipher_settings, DEFAULT_KDF_ITERATIONS, ) with patch.dict( os.environ, {"LDR_DB_CONFIG_KDF_ITERATIONS": ""}, clear=True ): settings = get_sqlcipher_settings() assert settings["kdf_iterations"] == DEFAULT_KDF_ITERATIONS def test_invalid_hmac_algorithm_uses_default(self): """Invalid HMAC algorithm via deprecated name is validated and replaced with default.""" from local_deep_research.database.sqlcipher_utils import ( get_sqlcipher_settings, DEFAULT_HMAC_ALGORITHM, ) with patch.dict( os.environ, {"LDR_DB_HMAC_ALGORITHM": "INVALID_ALGO"}, clear=True, ): settings = get_sqlcipher_settings() assert settings["hmac_algorithm"] == DEFAULT_HMAC_ALGORITHM def test_invalid_page_size_uses_default(self): """Non-power-of-2 page size uses default.""" from local_deep_research.database.sqlcipher_utils import ( get_sqlcipher_settings, DEFAULT_PAGE_SIZE, ) with patch.dict( os.environ, {"LDR_DB_CONFIG_PAGE_SIZE": "3000"}, clear=True ): settings = get_sqlcipher_settings() assert settings["page_size"] == DEFAULT_PAGE_SIZE def test_valid_deprecated_kdf_algorithm_accepted(self): """Valid KDF algorithm via deprecated name passes validation.""" from local_deep_research.database.sqlcipher_utils import ( get_sqlcipher_settings, ) with patch.dict( os.environ, {"LDR_DB_KDF_ALGORITHM": "PBKDF2_HMAC_SHA256"}, clear=True, ): settings = get_sqlcipher_settings() assert settings["kdf_algorithm"] == "PBKDF2_HMAC_SHA256" class TestCombinedSettingsScenarios: """Tests for multiple settings used together.""" def test_all_canonical_names_set(self): """All settings use canonical names.""" from local_deep_research.database.sqlcipher_utils import ( get_sqlcipher_settings, ) with patch.dict( os.environ, { "LDR_DB_CONFIG_KDF_ITERATIONS": "500000", "LDR_DB_CONFIG_PAGE_SIZE": "8192", "LDR_DB_CONFIG_HMAC_ALGORITHM": "HMAC_SHA256", "LDR_DB_CONFIG_KDF_ALGORITHM": "PBKDF2_HMAC_SHA256", }, clear=True, ): settings = get_sqlcipher_settings() assert settings["kdf_iterations"] == 500000 assert settings["page_size"] == 8192 assert settings["hmac_algorithm"] == "HMAC_SHA256" assert settings["kdf_algorithm"] == "PBKDF2_HMAC_SHA256" def test_all_deprecated_names_set(self): """All settings use deprecated names (backward compat).""" from local_deep_research.database.sqlcipher_utils import ( get_sqlcipher_settings, ) with patch.dict( os.environ, { "LDR_DB_KDF_ITERATIONS": "500000", "LDR_DB_PAGE_SIZE": "8192", "LDR_DB_HMAC_ALGORITHM": "HMAC_SHA256", "LDR_DB_KDF_ALGORITHM": "PBKDF2_HMAC_SHA256", }, clear=True, ): settings = get_sqlcipher_settings() assert settings["kdf_iterations"] == 500000 assert settings["page_size"] == 8192 assert settings["hmac_algorithm"] == "HMAC_SHA256" assert settings["kdf_algorithm"] == "PBKDF2_HMAC_SHA256" def test_mixed_canonical_and_deprecated(self): """Some settings canonical, some deprecated.""" from local_deep_research.database.sqlcipher_utils import ( get_sqlcipher_settings, ) with patch.dict( os.environ, { "LDR_DB_CONFIG_KDF_ITERATIONS": "400000", "LDR_DB_CONFIG_HMAC_ALGORITHM": "HMAC_SHA512", "LDR_DB_PAGE_SIZE": "4096", "LDR_DB_KDF_ALGORITHM": "PBKDF2_HMAC_SHA512", }, clear=True, ): settings = get_sqlcipher_settings() assert settings["kdf_iterations"] == 400000 assert settings["page_size"] == 4096 assert settings["hmac_algorithm"] == "HMAC_SHA512" assert settings["kdf_algorithm"] == "PBKDF2_HMAC_SHA512" def test_canonical_overrides_deprecated_for_each_setting(self): """Canonical takes precedence for each individual setting.""" from local_deep_research.database.sqlcipher_utils import ( get_sqlcipher_settings, ) with patch.dict( os.environ, { "LDR_DB_CONFIG_KDF_ITERATIONS": "300000", "LDR_DB_CONFIG_PAGE_SIZE": "16384", "LDR_DB_KDF_ITERATIONS": "100000", "LDR_DB_PAGE_SIZE": "4096", }, clear=True, ): settings = get_sqlcipher_settings() assert settings["kdf_iterations"] == 300000 assert settings["page_size"] == 16384 def test_performance_pragmas_with_mixed_settings(self): """Test apply_performance_pragmas with mixed canonical/deprecated.""" from local_deep_research.database.sqlcipher_utils import ( apply_performance_pragmas, ) mock_cursor = Mock() with patch.dict( os.environ, { "LDR_DB_CONFIG_CACHE_SIZE_MB": "256", "LDR_DB_JOURNAL_MODE": "DELETE", "LDR_DB_CONFIG_SYNCHRONOUS": "OFF", }, clear=True, ): apply_performance_pragmas(mock_cursor) call_args_list = [ call[0][0] for call in mock_cursor.execute.call_args_list ] cache_call = [arg for arg in call_args_list if "cache_size" in arg][ 0 ] assert "-262144" in cache_call journal_call = [ arg for arg in call_args_list if "journal_mode" in arg ][0] assert "DELETE" in journal_call sync_call = [arg for arg in call_args_list if "synchronous" in arg][ 0 ] assert "OFF" in sync_call class TestSetSqlcipherRekey: """Tests for set_sqlcipher_rekey function.""" def test_rekey_uses_pbkdf2_derivation(self): """Verify rekey uses get_key_from_password (PBKDF2), not raw hex encoding.""" from local_deep_research.database.sqlcipher_utils import ( set_sqlcipher_rekey, ) mock_conn = Mock() # Make it look like a raw connection (raises TypeError on text()) mock_conn.execute.side_effect = [TypeError("not sqlalchemy"), None] with patch( "local_deep_research.database.sqlcipher_utils.get_key_from_password", return_value=b"\xab\xcd\xef", ) as mock_get_key: set_sqlcipher_rekey(mock_conn, "new_password") mock_get_key.assert_called_once_with("new_password", db_path=None) def test_rekey_does_not_use_raw_hex(self): """Verify rekey does NOT use raw password.encode().hex().""" from local_deep_research.database.sqlcipher_utils import ( set_sqlcipher_rekey, ) mock_conn = Mock() mock_conn.execute.side_effect = [TypeError("not sqlalchemy"), None] with patch( "local_deep_research.database.sqlcipher_utils.get_key_from_password", return_value=b"\xab\xcd\xef", ): set_sqlcipher_rekey(mock_conn, "test_password") # The SQL should contain the PBKDF2-derived hex, not raw password hex rekey_call = mock_conn.execute.call_args_list[-1][0][0] assert "test_password".encode().hex() not in rekey_call assert "abcdef" in rekey_call def test_rekey_works_with_sqlalchemy(self): """Verify rekey works through SQLAlchemy connection (text() wrapper).""" from local_deep_research.database.sqlcipher_utils import ( set_sqlcipher_rekey, ) mock_conn = Mock() with patch( "local_deep_research.database.sqlcipher_utils.get_key_from_password", return_value=b"\xab\xcd\xef", ): set_sqlcipher_rekey(mock_conn, "new_password") # Should have called execute with text() wrapped SQL mock_conn.execute.assert_called_once() class TestCIAwareKDFMinimum: """Tests for CI-aware MIN_KDF_ITERATIONS enforcement.""" def test_production_enforces_minimum(self): """Test that production enforces MIN_KDF_ITERATIONS_PRODUCTION.""" from local_deep_research.database.sqlcipher_utils import ( get_sqlcipher_settings, DEFAULT_KDF_ITERATIONS, ) # Without CI/TESTING env vars, low KDF should be rejected env = { "LDR_DB_KDF_ITERATIONS": "100", } # Clear CI vars for key in ["LDR_TEST_MODE", "PYTEST_CURRENT_TEST"]: env[key] = "" with patch.dict(os.environ, env, clear=True): settings = get_sqlcipher_settings() # Should fall back to default since 100 < 100_000 assert settings["kdf_iterations"] == DEFAULT_KDF_ITERATIONS def test_testing_allows_low_kdf(self): """Test that LDR_TEST_MODE env var allows low KDF iterations.""" from local_deep_research.database.sqlcipher_utils import ( get_sqlcipher_settings, ) with patch.dict( os.environ, {"LDR_DB_KDF_ITERATIONS": "1000", "LDR_TEST_MODE": "true"}, clear=True, ): settings = get_sqlcipher_settings() assert settings["kdf_iterations"] == 1000 def test_falsey_test_mode_does_not_relax_kdf(self): """LDR_TEST_MODE is parsed as a boolean: an explicit '0'/'false' must NOT relax the KDF floor (a bare truthiness check would treat the non-empty string 'false' as enabled and silently weaken encryption). The low requested value must clamp to the default. """ from local_deep_research.database.sqlcipher_utils import ( get_sqlcipher_settings, DEFAULT_KDF_ITERATIONS, ) # 50000 sits below the production floor (100000) but well above the # registry's own min_value (1000), so this exercises the KDF-floor # clamp rather than the registry's range validation — and stays # robust if that min_value is ever raised. for falsey in ("0", "false", "no", "off"): with patch.dict( os.environ, {"LDR_DB_KDF_ITERATIONS": "50000", "LDR_TEST_MODE": falsey}, clear=True, ): settings = get_sqlcipher_settings() assert settings["kdf_iterations"] == DEFAULT_KDF_ITERATIONS, ( f"LDR_TEST_MODE={falsey!r} should not relax the KDF floor" ) class TestCipherMemorySecurityEnvVar: """Tests for configurable cipher_memory_security.""" def test_defaults_to_off(self): """Test that cipher_memory_security defaults to OFF in both modes.""" from local_deep_research.database.sqlcipher_utils import ( apply_sqlcipher_pragmas, ) for mode in (True, False): mock_cursor = Mock() with patch.dict(os.environ, {}, clear=False): os.environ.pop("LDR_DB_CONFIG_CIPHER_MEMORY_SECURITY", None) apply_sqlcipher_pragmas(mock_cursor, creation_mode=mode) call_args_list = [ call[0][0] for call in mock_cursor.execute.call_args_list ] mem_calls = [ arg for arg in call_args_list if "cipher_memory_security" in arg ] assert len(mem_calls) == 1, f"Failed for creation_mode={mode}" assert "OFF" in mem_calls[0], f"Failed for creation_mode={mode}" def test_respects_on_env_var(self): """Test that LDR_DB_CONFIG_CIPHER_MEMORY_SECURITY=ON is respected in both modes.""" from local_deep_research.database.sqlcipher_utils import ( apply_sqlcipher_pragmas, ) for mode in (True, False): mock_cursor = Mock() with patch.dict( os.environ, {"LDR_DB_CONFIG_CIPHER_MEMORY_SECURITY": "ON"} ): apply_sqlcipher_pragmas(mock_cursor, creation_mode=mode) call_args_list = [ call[0][0] for call in mock_cursor.execute.call_args_list ] mem_calls = [ arg for arg in call_args_list if "cipher_memory_security" in arg ] assert len(mem_calls) == 1, f"Failed for creation_mode={mode}" assert "ON" in mem_calls[0], f"Failed for creation_mode={mode}" def test_invalid_value_falls_back_to_off(self): """Test that invalid value falls back to OFF (default) in both modes.""" from local_deep_research.database.sqlcipher_utils import ( apply_sqlcipher_pragmas, ) for mode in (True, False): mock_cursor = Mock() with patch.dict( os.environ, {"LDR_DB_CONFIG_CIPHER_MEMORY_SECURITY": "INVALID"}, ): apply_sqlcipher_pragmas(mock_cursor, creation_mode=mode) call_args_list = [ call[0][0] for call in mock_cursor.execute.call_args_list ] mem_calls = [ arg for arg in call_args_list if "cipher_memory_security" in arg ] assert len(mem_calls) == 1, f"Failed for creation_mode={mode}" assert "OFF" in mem_calls[0], f"Failed for creation_mode={mode}" class TestWeakKdfStartupWarning: """warn_if_weak_kdf_with_existing_databases() — fire only when the effective KDF is below the production floor AND user DBs already exist.""" @staticmethod def _make_user_db(data_dir, username="alice"): # Generate the filename via the REAL generator so this fixture tracks # production naming (ldr_user_.db) instead of a brittle literal. from local_deep_research.config.paths import ( get_user_database_filename, ) (data_dir / get_user_database_filename(username)).write_bytes(b"") def test_warns_when_weak_kdf_and_databases_exist(self, tmp_path): from local_deep_research.database.sqlcipher_utils import ( warn_if_weak_kdf_with_existing_databases, ) self._make_user_db(tmp_path) with patch.dict( os.environ, { "LDR_TEST_MODE": "1", "LDR_DB_CONFIG_KDF_ITERATIONS": "1000", }, clear=True, ): assert warn_if_weak_kdf_with_existing_databases(tmp_path) is True def test_silent_when_no_databases_exist(self, tmp_path): """Fresh deployment: nothing to mismatch, even with a weak KDF.""" from local_deep_research.database.sqlcipher_utils import ( warn_if_weak_kdf_with_existing_databases, ) with patch.dict( os.environ, { "LDR_TEST_MODE": "1", "LDR_DB_CONFIG_KDF_ITERATIONS": "1000", }, clear=True, ): assert warn_if_weak_kdf_with_existing_databases(tmp_path) is False def test_silent_at_production_floor(self, tmp_path): """Effective KDF at/above the floor is safe even with DBs present.""" from local_deep_research.database.sqlcipher_utils import ( warn_if_weak_kdf_with_existing_databases, ) self._make_user_db(tmp_path) # No test mode → effective KDF is the 256000 default → not weak. with patch.dict(os.environ, {}, clear=True): assert warn_if_weak_kdf_with_existing_databases(tmp_path) is False def test_silent_when_test_mode_but_default_kdf(self, tmp_path): """LDR_TEST_MODE merely *set* (no low iterations requested) keeps the 256000 default, which is not weak — must not warn.""" from local_deep_research.database.sqlcipher_utils import ( warn_if_weak_kdf_with_existing_databases, ) self._make_user_db(tmp_path) with patch.dict(os.environ, {"LDR_TEST_MODE": "1"}, clear=True): assert warn_if_weak_kdf_with_existing_databases(tmp_path) is False def test_warning_message_is_rendered_with_count( self, tmp_path, loguru_caplog ): """Force the message to render (loguru is lazy — only then is the {}-placeholder/arg count exercised) and assert the actual content, including the user-DB count. Without this, a broken format string ships green and is then swallowed by the boot-time try/except.""" from local_deep_research.database.sqlcipher_utils import ( warn_if_weak_kdf_with_existing_databases, ) self._make_user_db(tmp_path, "alice") self._make_user_db(tmp_path, "bob") loguru_caplog.set_level("WARNING") with patch.dict( os.environ, {"LDR_TEST_MODE": "1", "LDR_DB_CONFIG_KDF_ITERATIONS": "1000"}, clear=True, ): assert warn_if_weak_kdf_with_existing_databases(tmp_path) is True text = loguru_caplog.text # "1000 iterations", not bare "1000" — "1000" is a substring of the # "100000" floor, so the bare check would pass without proving the # effective KDF actually rendered. assert "1000 iterations" in text # effective KDF assert "100000" in text # production floor assert "2 user database" in text # count rendered for 2 DBs assert "PR #4775" in text def test_silent_case_logs_nothing(self, tmp_path, loguru_caplog): """A non-firing call must emit NO warning, not merely return False.""" from local_deep_research.database.sqlcipher_utils import ( warn_if_weak_kdf_with_existing_databases, ) self._make_user_db(tmp_path) loguru_caplog.set_level("WARNING") with patch.dict(os.environ, {}, clear=True): # production floor assert warn_if_weak_kdf_with_existing_databases(tmp_path) is False assert "SQLCipher KDF" not in loguru_caplog.text