""" Comprehensive tests for settings logger module. Tests cover: - log_settings function with all log levels - redact_sensitive_keys function - create_settings_summary function - get_settings_log_level function """ import os import pytest from unittest.mock import patch # Import the functions we want to test # We need to patch the module-level SETTINGS_LOG_LEVEL before importing # So we'll import the module itself and access functions through it @pytest.fixture(autouse=True) def _restore_settings_logger_module(): """Restore the pre-test module state after each test. The tests reload settings.logger under a patched LDR_LOG_SETTINGS to rebuild SETTINGS_LOG_LEVEL; without a restore, the last test's level (e.g. "debug") would stay in the module for the rest of the process, silently defeating the quiet-by-default contract for later tests. """ from local_deep_research.settings import logger as settings_logger snapshot = dict(settings_logger.__dict__) yield settings_logger.__dict__.clear() settings_logger.__dict__.update(snapshot) class TestLogSettingsNoneLevel: """Tests for log_settings with none level.""" def test_log_settings_none_level_skips_logging(self): """Test that log_settings does nothing when level is 'none'.""" with patch.dict(os.environ, {"LDR_LOG_SETTINGS": "none"}): # Re-import to get new SETTINGS_LOG_LEVEL import importlib from local_deep_research.settings import logger as settings_logger importlib.reload(settings_logger) with patch.object(settings_logger.logger, "info") as mock_info: with patch.object( settings_logger.logger, "debug" ) as mock_debug: settings_logger.log_settings({"key": "value"}) mock_info.assert_not_called() mock_debug.assert_not_called() class TestLogSettingsSummaryLevel: """Tests for log_settings with summary level.""" def test_log_settings_summary_level(self): """Test that log_settings outputs summary at INFO level.""" with patch.dict(os.environ, {"LDR_LOG_SETTINGS": "summary"}): import importlib from local_deep_research.settings import logger as settings_logger importlib.reload(settings_logger) with patch.object(settings_logger.logger, "info") as mock_info: settings_logger.log_settings( {"key": "value"}, message="Test message" ) mock_info.assert_called_once() call_args = mock_info.call_args[0][0] assert "Test message" in call_args class TestLogSettingsDebugLevel: """Tests for log_settings with debug level.""" def test_log_settings_debug_level(self): """Test that log_settings outputs full settings with redaction at DEBUG level.""" with patch.dict(os.environ, {"LDR_LOG_SETTINGS": "debug"}): import importlib from local_deep_research.settings import logger as settings_logger importlib.reload(settings_logger) with patch.object(settings_logger.logger, "debug") as mock_debug: settings_logger.log_settings( {"api_key": "secret123", "normal": "value"}, message="Test message", ) mock_debug.assert_called_once() call_args = mock_debug.call_args[0][0] assert "redacted" in call_args.lower() class TestLogSettingsDebugUnsafeLevel: """Tests for debug_unsafe deprecation — maps to 'none'.""" def test_debug_unsafe_maps_to_none(self): """Test that debug_unsafe is deprecated and maps to 'none'.""" import warnings with patch.dict(os.environ, {"LDR_LOG_SETTINGS": "debug_unsafe"}): import importlib from local_deep_research.settings import logger as settings_logger with warnings.catch_warnings(record=True) as w: warnings.simplefilter("always") importlib.reload(settings_logger) # Should have emitted a DeprecationWarning deprecation_warnings = [ x for x in w if issubclass(x.category, DeprecationWarning) ] assert len(deprecation_warnings) >= 1 assert ( "deprecated" in str(deprecation_warnings[0].message).lower() ) assert settings_logger.get_settings_log_level() == "none" @pytest.mark.parametrize("env_value", ["unsafe", "raw"]) def test_unsafe_aliases_map_to_none(self, env_value): """Test that 'unsafe' and 'raw' also map to 'none'.""" import warnings with patch.dict(os.environ, {"LDR_LOG_SETTINGS": env_value}): import importlib from local_deep_research.settings import logger as settings_logger with warnings.catch_warnings(record=True): warnings.simplefilter("always") importlib.reload(settings_logger) assert settings_logger.get_settings_log_level() == "none" def test_debug_unsafe_does_not_log_secrets(self): """Test that debug_unsafe no longer logs unredacted settings.""" import warnings with patch.dict(os.environ, {"LDR_LOG_SETTINGS": "debug_unsafe"}): import importlib from local_deep_research.settings import logger as settings_logger with warnings.catch_warnings(record=True): warnings.simplefilter("always") importlib.reload(settings_logger) with patch.object(settings_logger.logger, "debug") as mock_debug: settings_logger.log_settings( {"api_key": "secret123"}, message="Test message" ) # Should NOT have logged anything (level is "none") mock_debug.assert_not_called() class TestLogSettingsForcedLevel: """Tests for log_settings with forced level override.""" def test_log_settings_force_level_overrides_env(self): """Test that force_level parameter overrides environment setting.""" with patch.dict(os.environ, {"LDR_LOG_SETTINGS": "none"}): import importlib from local_deep_research.settings import logger as settings_logger importlib.reload(settings_logger) with patch.object(settings_logger.logger, "info") as mock_info: settings_logger.log_settings( {"key": "value"}, force_level="summary" ) # Should have logged despite env being 'none' mock_info.assert_called_once() class TestLogSettingsEdgeCases: """Tests for edge cases in log_settings.""" def test_log_settings_with_empty_settings(self): """Test log_settings handles empty dict gracefully.""" with patch.dict(os.environ, {"LDR_LOG_SETTINGS": "summary"}): import importlib from local_deep_research.settings import logger as settings_logger importlib.reload(settings_logger) with patch.object(settings_logger.logger, "info") as mock_info: # Should not raise settings_logger.log_settings({}) mock_info.assert_called_once() call_args = mock_info.call_args[0][0] assert "0 total settings" in call_args def test_log_settings_with_non_dict_settings(self): """Test log_settings handles non-dict input.""" with patch.dict(os.environ, {"LDR_LOG_SETTINGS": "summary"}): import importlib from local_deep_research.settings import logger as settings_logger importlib.reload(settings_logger) with patch.object(settings_logger.logger, "info") as mock_info: # Should not raise settings_logger.log_settings("not a dict") mock_info.assert_called_once() class TestRedactSensitiveKeys: """Tests for redact_sensitive_keys function.""" @pytest.fixture(autouse=True) def setup_module(self): """Import the module for tests.""" with patch.dict(os.environ, {"LDR_LOG_SETTINGS": "none"}): import importlib from local_deep_research.settings import logger as settings_logger importlib.reload(settings_logger) self.settings_logger = settings_logger def test_redact_api_key_pattern(self): """Test that 'api_key' pattern is redacted.""" result = self.settings_logger.redact_sensitive_keys( {"api_key": "secret123"} ) assert result["api_key"] == "***REDACTED***" def test_redact_password_pattern(self): """Test that 'password' pattern is redacted.""" result = self.settings_logger.redact_sensitive_keys( {"password": "mypassword", "db_password": "dbpass"} ) assert result["password"] == "***REDACTED***" assert result["db_password"] == "***REDACTED***" def test_redact_token_pattern(self): """Test that 'token' pattern is redacted.""" result = self.settings_logger.redact_sensitive_keys( {"token": "abc123", "access_token": "xyz789"} ) assert result["token"] == "***REDACTED***" assert result["access_token"] == "***REDACTED***" def test_redact_secret_pattern(self): """Test that 'secret' pattern is redacted.""" result = self.settings_logger.redact_sensitive_keys( {"secret": "shh", "secret_key": "shhh"} ) assert result["secret"] == "***REDACTED***" assert result["secret_key"] == "***REDACTED***" def test_redact_nested_sensitive_keys(self): """Test that nested sensitive keys are redacted.""" result = self.settings_logger.redact_sensitive_keys( {"outer": {"api_key": "nested_secret", "normal": "value"}} ) assert result["outer"]["api_key"] == "***REDACTED***" assert result["outer"]["normal"] == "value" def test_redact_preserves_non_sensitive(self): """Test that non-sensitive keys are preserved.""" result = self.settings_logger.redact_sensitive_keys( {"app_name": "MyApp", "debug": True, "count": 42} ) assert result["app_name"] == "MyApp" assert result["debug"] is True assert result["count"] == 42 def test_redact_setting_dict_with_value_key(self): """Test redaction of settings format with 'value' key.""" result = self.settings_logger.redact_sensitive_keys( {"api_key": {"value": "secret", "type": "string"}} ) assert result["api_key"]["value"] == "***REDACTED***" assert result["api_key"]["type"] == "string" class TestCreateSettingsSummary: """Tests for create_settings_summary function.""" @pytest.fixture(autouse=True) def setup_module(self): """Import the module for tests.""" with patch.dict(os.environ, {"LDR_LOG_SETTINGS": "none"}): import importlib from local_deep_research.settings import logger as settings_logger importlib.reload(settings_logger) self.settings_logger = settings_logger def test_create_settings_summary_counts(self): """Test that summary correctly counts different setting types.""" result = self.settings_logger.create_settings_summary( { "search.engine.google": True, "search.engine.bing": True, "llm.provider": "openai", "llm.temperature": 0.7, "app.debug": False, } ) assert "5 total settings" in result assert "search engines: 2" in result assert "LLM: 2" in result def test_create_settings_summary_empty_dict(self): """Test summary of empty settings.""" result = self.settings_logger.create_settings_summary({}) assert "0 total settings" in result def test_create_settings_summary_non_dict(self): """Test summary of non-dict input.""" result = self.settings_logger.create_settings_summary("string_settings") assert "str" in result def test_create_settings_summary_with_object(self): """Test summary with object input.""" class CustomSettings: pass result = self.settings_logger.create_settings_summary(CustomSettings()) assert "CustomSettings" in result class TestGetSettingsLogLevel: """Tests for get_settings_log_level function.""" def test_get_settings_log_level_returns_current(self): """Test that get_settings_log_level returns current level.""" with patch.dict(os.environ, {"LDR_LOG_SETTINGS": "debug"}): import importlib from local_deep_research.settings import logger as settings_logger importlib.reload(settings_logger) result = settings_logger.get_settings_log_level() assert result == "debug" def test_get_settings_log_level_default(self): """Test that get_settings_log_level returns 'none' by default.""" # Remove the env var if set env = os.environ.copy() env.pop("LDR_LOG_SETTINGS", None) with patch.dict(os.environ, env, clear=True): import importlib from local_deep_research.settings import logger as settings_logger importlib.reload(settings_logger) result = settings_logger.get_settings_log_level() assert result == "none" class TestLogLevelMapping: """Tests for log level value mapping.""" @pytest.mark.parametrize( "env_value,expected", [ ("false", "none"), ("0", "none"), ("no", "none"), ("off", "none"), ("none", "none"), ("true", "summary"), ("1", "summary"), ("yes", "summary"), ("info", "summary"), ("summary", "summary"), ("debug", "debug"), ("full", "debug"), ("all", "debug"), ("debug_unsafe", "none"), ("unsafe", "none"), ("raw", "none"), ("invalid_value", "none"), # Invalid defaults to none ], ) def test_log_level_mapping(self, env_value, expected): """Test that various env values map to correct log levels.""" import warnings with patch.dict(os.environ, {"LDR_LOG_SETTINGS": env_value}): import importlib from local_deep_research.settings import logger as settings_logger with warnings.catch_warnings(): warnings.simplefilter("ignore", DeprecationWarning) importlib.reload(settings_logger) result = settings_logger.get_settings_log_level() assert result == expected, ( f"Expected {env_value} to map to {expected}, got {result}" ) class TestRedactCaseSensitivity: """Tests for case sensitivity in redaction.""" @pytest.fixture(autouse=True) def setup_module(self): """Import the module for tests.""" with patch.dict(os.environ, {"LDR_LOG_SETTINGS": "none"}): import importlib from local_deep_research.settings import logger as settings_logger importlib.reload(settings_logger) self.settings_logger = settings_logger def test_redact_case_insensitive_api_key(self): """Test that API_KEY (uppercase) is also redacted.""" result = self.settings_logger.redact_sensitive_keys( {"API_KEY": "secret", "Api_Key": "secret2"} ) assert result["API_KEY"] == "***REDACTED***" assert result["Api_Key"] == "***REDACTED***" def test_redact_case_insensitive_password(self): """Test that PASSWORD (uppercase) is also redacted.""" result = self.settings_logger.redact_sensitive_keys( {"PASSWORD": "secret", "Password": "secret2"} ) assert result["PASSWORD"] == "***REDACTED***" assert result["Password"] == "***REDACTED***"