infrasynth-backend-kit/tests/test_configs/test_registry.py
jcv-dev a2930426b4 feat: tenant configs, live signals, and automatic permission management
- infrasynth.configs: typed multi-tenant config store (registry, service,
  secrets, cache) + public config_changed/config_reset signals and API
- emit the declared-but-dead signals (features flags/overrides, scheduler
  task_completed/task_failed, tenancy tenant_updated, audit model_changed)
  and per-model audit field exclusions
- security: permission catalog (security_permission), Django-style
  model-derived AutoPermission, PermissionRegistry, RoleAssignment,
  global-or-tenant Grant/Revoke, catalog API
- consolidate the permission surface: PermissionRegistry only (drop the
  settings dict), IsAuthenticatedAndPermitted aliases HybridPermission,
  require_permission replaced by required_permissions + require_all
- packaging: add [build-system]; add Forgejo publish workflow (.forgejo)
2026-09-29 17:06:54 -05:00

135 lines
5.1 KiB
Python

"""``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"