""" Extended tests for environment settings type classes. Tests cover: - IntegerSetting min/max validation and edge cases - PathSetting path expansion and validation - EnumSetting case sensitivity and matching - SecretSetting value hiding - Base EnvSetting functionality """ import os from pathlib import Path import pytest from local_deep_research.settings.env_settings import ( BooleanSetting, StringSetting, IntegerSetting, PathSetting, SecretSetting, EnumSetting, SettingsRegistry, ) class TestIntegerSettingValidation: """Tests for IntegerSetting min/max validation.""" @pytest.fixture(autouse=True) def clean_env(self): """Clean environment before each test.""" original_env = { k: v for k, v in os.environ.items() if k.startswith("LDR_TEST_") } for key in list(os.environ.keys()): if key.startswith("LDR_TEST_"): os.environ.pop(key, None) yield for key in list(os.environ.keys()): if key.startswith("LDR_TEST_"): os.environ.pop(key, None) for key, value in original_env.items(): os.environ[key] = value def test_integer_setting_min_value_enforcement(self): """Test that values below min raise ValueError.""" setting = IntegerSetting( key="test.min_value", description="Test setting", default=10, min_value=5, max_value=100, ) os.environ["LDR_TEST_MIN_VALUE"] = "3" # Below min with pytest.raises(ValueError) as exc_info: setting.get_value() assert "below minimum" in str(exc_info.value) def test_integer_setting_max_value_enforcement(self): """Test that values above max raise ValueError.""" setting = IntegerSetting( key="test.max_value", description="Test setting", default=10, min_value=5, max_value=100, ) os.environ["LDR_TEST_MAX_VALUE"] = "200" # Above max with pytest.raises(ValueError) as exc_info: setting.get_value() assert "above maximum" in str(exc_info.value) def test_integer_setting_invalid_value_uses_default(self): """Test that non-numeric values fall back to default.""" setting = IntegerSetting( key="test.invalid", description="Test setting", default=42 ) os.environ["LDR_TEST_INVALID"] = "not_a_number" result = setting.get_value() assert result == 42 def test_integer_setting_float_string_truncates(self): """Test that float strings are converted to int (truncated).""" setting = IntegerSetting( key="test.float", description="Test setting", default=0 ) os.environ["LDR_TEST_FLOAT"] = "3.7" # This should fail since int("3.7") raises ValueError # The implementation should handle this as invalid result = setting.get_value() # Expect default since float string is invalid for int() assert result == 0 def test_integer_setting_empty_string_uses_default(self): """Test that empty string uses default value.""" setting = IntegerSetting( key="test.empty", description="Test setting", default=99 ) os.environ["LDR_TEST_EMPTY"] = "" result = setting.get_value() # Empty string is invalid, should use default assert result == 99 def test_integer_setting_valid_value_in_range(self): """Test that valid value within range is returned.""" setting = IntegerSetting( key="test.valid", description="Test setting", default=10, min_value=5, max_value=100, ) os.environ["LDR_TEST_VALID"] = "50" result = setting.get_value() assert result == 50 def test_integer_setting_boundary_values(self): """Test that boundary values are accepted.""" setting = IntegerSetting( key="test.boundary", description="Test setting", default=10, min_value=5, max_value=100, ) # Test min boundary os.environ["LDR_TEST_BOUNDARY"] = "5" assert setting.get_value() == 5 # Test max boundary os.environ["LDR_TEST_BOUNDARY"] = "100" assert setting.get_value() == 100 def test_integer_setting_negative_value(self): """Test that negative values work correctly.""" setting = IntegerSetting( key="test.negative", description="Test setting", default=0, min_value=-100, max_value=100, ) os.environ["LDR_TEST_NEGATIVE"] = "-50" result = setting.get_value() assert result == -50 class TestPathSettingValidation: """Tests for PathSetting path expansion and validation.""" @pytest.fixture(autouse=True) def clean_env(self): """Clean environment before each test.""" original_env = { k: v for k, v in os.environ.items() if k.startswith("LDR_TEST_") } for key in list(os.environ.keys()): if key.startswith("LDR_TEST_"): os.environ.pop(key, None) yield for key in list(os.environ.keys()): if key.startswith("LDR_TEST_"): os.environ.pop(key, None) for key, value in original_env.items(): os.environ[key] = value def test_path_setting_expands_tilde(self): """Test that ~/path is expanded to full home path.""" setting = PathSetting( key="test.tilde_path", description="Test setting", default=None ) os.environ["LDR_TEST_TILDE_PATH"] = "~/test_dir" result = setting.get_value() assert result.startswith(str(Path.home())) assert "test_dir" in result def test_path_setting_expands_env_vars(self): """Test that $HOME/path is expanded to the correct absolute path.""" setting = PathSetting( key="test.env_path", description="Test setting", default=None ) os.environ["LDR_TEST_ENV_PATH"] = "$HOME/test_env_dir" result = setting.get_value() expected = str(Path(os.environ["HOME"]) / "test_env_dir") assert result == expected def test_path_setting_create_if_missing(self, tmp_path): """Test that directory is created when create_if_missing=True.""" test_dir = tmp_path / "new_directory" setting = PathSetting( key="test.create_path", description="Test setting", default=None, create_if_missing=True, ) os.environ["LDR_TEST_CREATE_PATH"] = str(test_dir) result = setting.get_value() assert test_dir.exists() assert result == str(test_dir) def test_path_setting_must_exist_raises(self, tmp_path): """Test that ValueError is raised when path doesn't exist and must_exist=True.""" nonexistent_path = tmp_path / "nonexistent_dir" setting = PathSetting( key="test.must_exist", description="Test setting", default=None, must_exist=True, ) os.environ["LDR_TEST_MUST_EXIST"] = str(nonexistent_path) with pytest.raises(ValueError) as exc_info: setting.get_value() assert "does not exist" in str(exc_info.value) def test_path_setting_none_returns_none(self): """Test that unset path returns None.""" setting = PathSetting( key="test.unset_path", description="Test setting", default=None ) # Don't set the env var result = setting.get_value() assert result is None def test_path_setting_absolute_path_unchanged(self, tmp_path): """Test that absolute path is returned as-is.""" setting = PathSetting( key="test.absolute", description="Test setting", default=None ) os.environ["LDR_TEST_ABSOLUTE"] = str(tmp_path) result = setting.get_value() assert result == str(tmp_path) def test_path_setting_default_value(self): """Test that default value is used when env var not set.""" setting = PathSetting( key="test.default", description="Test setting", default="/default/path", ) result = setting.get_value() assert "/default/path" in result class TestEnumSettingValidation: """Tests for EnumSetting case sensitivity and matching.""" @pytest.fixture(autouse=True) def clean_env(self): """Clean environment before each test.""" original_env = { k: v for k, v in os.environ.items() if k.startswith("LDR_TEST_") } for key in list(os.environ.keys()): if key.startswith("LDR_TEST_"): os.environ.pop(key, None) yield for key in list(os.environ.keys()): if key.startswith("LDR_TEST_"): os.environ.pop(key, None) for key, value in original_env.items(): os.environ[key] = value def test_enum_setting_case_insensitive(self): """Test that matching is case-insensitive by default.""" setting = EnumSetting( key="test.enum_ci", description="Test setting", allowed_values={"DEBUG", "INFO", "WARNING"}, default="INFO", case_sensitive=False, ) os.environ["LDR_TEST_ENUM_CI"] = "debug" result = setting.get_value() assert result == "DEBUG" # Returns canonical form def test_enum_setting_case_sensitive(self): """Test that case-sensitive matching works.""" setting = EnumSetting( key="test.enum_cs", description="Test setting", allowed_values={"DEBUG", "INFO", "WARNING"}, default="INFO", case_sensitive=True, ) os.environ["LDR_TEST_ENUM_CS"] = "debug" # lowercase with pytest.raises(ValueError) as exc_info: setting.get_value() assert "not in allowed values" in str(exc_info.value) def test_enum_setting_canonical_form(self): """Test that returned value is in canonical form.""" setting = EnumSetting( key="test.enum_canon", description="Test setting", allowed_values={"WAL", "TRUNCATE", "DELETE"}, default="WAL", case_sensitive=False, ) os.environ["LDR_TEST_ENUM_CANON"] = "wal" result = setting.get_value() # Should return uppercase canonical form assert result == "WAL" def test_enum_setting_invalid_uses_default(self): """Test that invalid values return default via registry.""" setting = EnumSetting( key="test.enum_invalid", description="Test setting", allowed_values={"A", "B", "C"}, default="A", ) os.environ["LDR_TEST_ENUM_INVALID"] = "X" # Not in allowed # Direct get_value raises, but through registry returns default with pytest.raises(ValueError): setting.get_value() def test_enum_setting_valid_value(self): """Test that valid value is accepted.""" setting = EnumSetting( key="test.enum_valid", description="Test setting", allowed_values={"OPTION1", "OPTION2", "OPTION3"}, default="OPTION1", ) os.environ["LDR_TEST_ENUM_VALID"] = "OPTION2" result = setting.get_value() assert result == "OPTION2" def test_enum_setting_default_when_not_set(self): """Test that default is used when env var not set.""" setting = EnumSetting( key="test.enum_default", description="Test setting", allowed_values={"X", "Y", "Z"}, default="Y", ) result = setting.get_value() assert result == "Y" class TestSecretSettingHiding: """Tests for SecretSetting value hiding.""" @pytest.fixture(autouse=True) def clean_env(self): """Clean environment before each test.""" original_env = { k: v for k, v in os.environ.items() if k.startswith("LDR_TEST_") } for key in list(os.environ.keys()): if key.startswith("LDR_TEST_"): os.environ.pop(key, None) yield for key in list(os.environ.keys()): if key.startswith("LDR_TEST_"): os.environ.pop(key, None) for key, value in original_env.items(): os.environ[key] = value def test_secret_setting_repr_hides_value(self): """Test that repr() hides the actual value.""" setting = SecretSetting( key="test.secret_repr", description="Test setting", default=None ) os.environ["LDR_TEST_SECRET_REPR"] = "super_secret_value" repr_str = repr(setting) assert "super_secret_value" not in repr_str assert "***" in repr_str def test_secret_setting_str_hides_value(self): """Test that str() hides the actual value.""" setting = SecretSetting( key="test.secret_str", description="Test setting", default=None ) os.environ["LDR_TEST_SECRET_STR"] = "super_secret_value" str_result = str(setting) assert "super_secret_value" not in str_result assert "SET" in str_result def test_secret_setting_get_value_returns_actual(self): """Test that get_value() returns the actual secret.""" setting = SecretSetting( key="test.secret_get", description="Test setting", default=None ) os.environ["LDR_TEST_SECRET_GET"] = "actual_secret_value" result = setting.get_value() assert result == "actual_secret_value" def test_secret_setting_unset_shows_not_set(self): """Test that str() shows NOT SET when unset.""" setting = SecretSetting( key="test.secret_unset", description="Test setting", default=None ) str_result = str(setting) assert "NOT SET" in str_result class TestBaseEnvSetting: """Tests for base EnvSetting functionality.""" @pytest.fixture(autouse=True) def clean_env(self): """Clean environment before each test.""" original_env = { k: v for k, v in os.environ.items() if k.startswith("LDR_TEST_") } for key in list(os.environ.keys()): if key.startswith("LDR_TEST_"): os.environ.pop(key, None) yield for key in list(os.environ.keys()): if key.startswith("LDR_TEST_"): os.environ.pop(key, None) for key, value in original_env.items(): os.environ[key] = value def test_env_setting_env_var_auto_generation(self): """Test that env_var is auto-generated from key.""" setting = BooleanSetting( key="test.nested.setting", description="Test setting", default=False ) assert setting.env_var == "LDR_TEST_NESTED_SETTING" def test_env_setting_is_set_property_true(self): """Test is_set returns True when env var is set.""" setting = BooleanSetting( key="test.is_set", description="Test setting", default=False ) os.environ["LDR_TEST_IS_SET"] = "true" assert setting.is_set is True def test_env_setting_is_set_property_false(self): """Test is_set returns False when env var is not set.""" setting = BooleanSetting( key="test.not_set", description="Test setting", default=False ) assert setting.is_set is False def test_env_setting_required_raises_when_missing(self): """Test that required setting raises when not set.""" setting = StringSetting( key="test.required", description="Test setting", default=None, required=True, ) with pytest.raises(ValueError) as exc_info: setting.get_value() assert "Required environment variable" in str(exc_info.value) def test_env_setting_repr(self): """Test __repr__ method.""" setting = BooleanSetting( key="test.repr", description="Test setting", default=False ) repr_str = repr(setting) assert "BooleanSetting" in repr_str assert "test.repr" in repr_str assert "LDR_TEST_REPR" in repr_str class TestBooleanSettingConversion: """Tests for BooleanSetting value conversion.""" @pytest.fixture(autouse=True) def clean_env(self): """Clean environment before each test.""" original_env = { k: v for k, v in os.environ.items() if k.startswith("LDR_TEST_") } for key in list(os.environ.keys()): if key.startswith("LDR_TEST_"): os.environ.pop(key, None) yield for key in list(os.environ.keys()): if key.startswith("LDR_TEST_"): os.environ.pop(key, None) for key, value in original_env.items(): os.environ[key] = value @pytest.mark.parametrize( "value", ["true", "True", "TRUE", "1", "yes", "on", "enabled"] ) def test_boolean_setting_truthy_values(self, value): """Test that truthy values convert to True.""" setting = BooleanSetting( key="test.bool", description="Test setting", default=False ) os.environ["LDR_TEST_BOOL"] = value assert setting.get_value() is True @pytest.mark.parametrize( "value", ["false", "False", "FALSE", "0", "no", "off", ""] ) def test_boolean_setting_falsy_values(self, value): """Test that falsy values convert to False.""" setting = BooleanSetting( key="test.bool_false", description="Test setting", default=True ) os.environ["LDR_TEST_BOOL_FALSE"] = value assert setting.get_value() is False class TestSettingsRegistryExtended: """Extended tests for SettingsRegistry functionality.""" @pytest.fixture(autouse=True) def clean_env(self): """Clean environment before each test.""" original_env = { k: v for k, v in os.environ.items() if k.startswith("LDR_TEST_") } for key in list(os.environ.keys()): if key.startswith("LDR_TEST_"): os.environ.pop(key, None) yield for key in list(os.environ.keys()): if key.startswith("LDR_TEST_"): os.environ.pop(key, None) for key, value in original_env.items(): os.environ[key] = value def test_registry_register_category(self): """Test registering a category of settings.""" registry = SettingsRegistry() settings = [ BooleanSetting("cat1.setting1", "Test 1", default=False), BooleanSetting("cat1.setting2", "Test 2", default=True), ] registry.register_category("cat1", settings) assert "cat1.setting1" in registry.list_all_settings() assert "cat1.setting2" in registry.list_all_settings() def test_registry_get_returns_default_for_unknown(self): """Test that get() returns default for unknown keys.""" registry = SettingsRegistry() result = registry.get("unknown.key", default="fallback") assert result == "fallback" def test_registry_get_setting_object(self): """Test get_setting_object returns the EnvSetting instance.""" registry = SettingsRegistry() setting = BooleanSetting("test.obj", "Test", default=False) registry.register_category("test", [setting]) result = registry.get_setting_object("test.obj") assert result is setting def test_registry_is_env_only(self): """Test is_env_only returns correct values.""" registry = SettingsRegistry() setting = BooleanSetting("test.env_only", "Test", default=False) registry.register_category("test", [setting]) assert registry.is_env_only("test.env_only") is True assert registry.is_env_only("unknown.key") is False def test_registry_get_env_var(self): """Test get_env_var returns correct env var name.""" registry = SettingsRegistry() setting = BooleanSetting("test.env_var", "Test", default=False) registry.register_category("test", [setting]) result = registry.get_env_var("test.env_var") assert result == "LDR_TEST_ENV_VAR" def test_registry_get_all_env_vars(self): """Test get_all_env_vars returns all registered env vars.""" registry = SettingsRegistry() settings = [ BooleanSetting("test.s1", "Desc 1", default=False), StringSetting("test.s2", "Desc 2", default="val"), ] registry.register_category("test", settings) result = registry.get_all_env_vars() assert "LDR_TEST_S1" in result assert "LDR_TEST_S2" in result assert result["LDR_TEST_S1"] == "Desc 1" def test_registry_list_all_settings(self): """Test list_all_settings returns all registered keys.""" registry = SettingsRegistry() settings = [ BooleanSetting("test.a", "A", default=False), BooleanSetting("test.b", "B", default=False), ] registry.register_category("test", settings) result = registry.list_all_settings() assert "test.a" in result assert "test.b" in result assert len(result) == 2