"""``ConfigRegistry`` registration and coercion.""" import decimal import pytest from infrasynth.configs.registry import INVALID_CODE, ConfigRegistry, ConfigType from infrasynth.shared.exceptions import ValidationAppError def _positive(value): if value <= 0: raise ValueError("must be positive") class TestRegistration: @pytest.fixture(autouse=True) def clean_registry(self, clean_config_registry): yield def test_register_and_get(self): definition = ConfigRegistry.register("branding.color", type=ConfigType.STRING, default="#000") assert ConfigRegistry.get("branding.color") is definition assert ConfigRegistry.get("missing") is None def test_all_returns_copy(self): ConfigRegistry.register("a") snapshot = ConfigRegistry.all() snapshot["b"] = None assert "b" not in ConfigRegistry.all() def test_register_accepts_string_type(self): definition = ConfigRegistry.register("n", type="int", default=1) assert definition.type is ConfigType.INT def test_label_defaults_to_key(self): definition = ConfigRegistry.register("some.key") assert definition.label == "some.key" class TestCoerce: @pytest.fixture(autouse=True) def clean_registry(self, clean_config_registry): yield def _definition(self, config_type, **kwargs): return ConfigRegistry.register("k", type=config_type, **kwargs) def test_string(self): definition = self._definition(ConfigType.STRING) assert ConfigRegistry.coerce(definition, "hi") == "hi" def test_string_rejects_int(self): definition = self._definition(ConfigType.STRING) with pytest.raises(ValidationAppError) as exc: ConfigRegistry.coerce(definition, 3) assert exc.value.code == INVALID_CODE def test_int_accepts_numeric_string(self): definition = self._definition(ConfigType.INT) assert ConfigRegistry.coerce(definition, "5") == 5 def test_int_rejects_bool(self): definition = self._definition(ConfigType.INT) with pytest.raises(ValidationAppError): ConfigRegistry.coerce(definition, True) def test_int_rejects_fractional_float(self): definition = self._definition(ConfigType.INT) with pytest.raises(ValidationAppError): ConfigRegistry.coerce(definition, 1.5) def test_float(self): definition = self._definition(ConfigType.FLOAT) assert ConfigRegistry.coerce(definition, "1.5") == 1.5 def test_decimal(self): definition = self._definition(ConfigType.DECIMAL) assert ConfigRegistry.coerce(definition, "1.25") == decimal.Decimal("1.25") def test_bool_accepts_real_bool(self): definition = self._definition(ConfigType.BOOL) assert ConfigRegistry.coerce(definition, False) is False def test_bool_rejects_truthy_string(self): definition = self._definition(ConfigType.BOOL) with pytest.raises(ValidationAppError): ConfigRegistry.coerce(definition, "false") def test_json_passthrough(self): definition = self._definition(ConfigType.JSON) assert ConfigRegistry.coerce(definition, {"a": [1, 2]}) == {"a": [1, 2]} def test_json_rejects_unserializable(self): definition = self._definition(ConfigType.JSON) with pytest.raises(ValidationAppError): ConfigRegistry.coerce(definition, object()) def test_choice_accepts_member(self): definition = self._definition(ConfigType.CHOICE, choices=("a", "b")) assert ConfigRegistry.coerce(definition, "b") == "b" def test_choice_rejects_non_member(self): definition = self._definition(ConfigType.CHOICE, choices=("a", "b")) with pytest.raises(ValidationAppError) as exc: ConfigRegistry.coerce(definition, "c") assert exc.value.code == INVALID_CODE def test_duration_from_int(self): definition = self._definition(ConfigType.DURATION) assert ConfigRegistry.coerce(definition, 90) == 90 @pytest.mark.parametrize( ("raw", "expected"), [("30s", 30), ("5m", 300), ("2h", 7200), ("1d", 86400), ("120", 120)], ) def test_duration_from_string(self, raw, expected): definition = self._definition(ConfigType.DURATION) assert ConfigRegistry.coerce(definition, raw) == expected def test_duration_rejects_garbage(self): definition = self._definition(ConfigType.DURATION) with pytest.raises(ValidationAppError): ConfigRegistry.coerce(definition, "soon") def test_custom_validator_passes(self): definition = self._definition(ConfigType.INT, validator="tests.test_configs.test_registry._positive") assert ConfigRegistry.coerce(definition, 2) == 2 def test_custom_validator_failure_is_typed(self): definition = self._definition(ConfigType.INT, validator="tests.test_configs.test_registry._positive") with pytest.raises(ValidationAppError) as exc: ConfigRegistry.coerce(definition, -1) assert exc.value.code == INVALID_CODE assert exc.value.details[0]["field"] == "k"