infrasynth-backend-kit/tests/test_configs/test_services.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

180 lines
7.3 KiB
Python

"""``ConfigService`` precedence, typing, secrets, and caching."""
import decimal
import pytest
from infrasynth.configs.models import ConfigValue
from infrasynth.configs.registry import ConfigRegistry, ConfigType
from infrasynth.configs.services import ConfigService
from infrasynth.shared.exceptions import AuthError, NotFoundError, ValidationAppError
@pytest.fixture(autouse=True)
def clean_registry(clean_config_registry):
yield
@pytest.fixture
def service():
return ConfigService()
class TestPrecedence:
def test_registry_default(self, service, tenant):
ConfigRegistry.register("k", type=ConfigType.STRING, default="default")
assert service.get("k", tenant=tenant) == "default"
def test_global_overrides_default(self, service, tenant):
ConfigRegistry.register("k", type=ConfigType.STRING, default="default")
service.set_global("k", "global")
assert service.get("k", tenant=tenant) == "global"
def test_tenant_overrides_global(self, service, tenant):
ConfigRegistry.register("k", type=ConfigType.STRING, default="default")
service.set_global("k", "global")
service.set("k", "tenant", tenant=tenant)
assert service.get("k", tenant=tenant) == "tenant"
def test_reset_falls_back_to_global(self, service, tenant):
ConfigRegistry.register("k", type=ConfigType.STRING, default="default")
service.set_global("k", "global")
service.set("k", "tenant", tenant=tenant)
assert service.reset("k", tenant=tenant) is True
assert service.get("k", tenant=tenant) == "global"
assert service.is_overridden("k", tenant=tenant) is False
def test_fail_closed_without_tenant_reads_global_only(self, service, tenant):
from infrasynth.tenancy.context import tenant_context
ConfigRegistry.register("k", type=ConfigType.STRING, default="default")
service.set("k", "tenant-only", tenant=tenant)
with tenant_context(None):
assert service.get("k") == "default"
# A registered key resolves to its registry default, not the arg.
assert service.get("k", default="fallback") == "default"
class TestUnknownKeys:
def test_unknown_key_raises(self, service, tenant):
with pytest.raises(NotFoundError):
service.get("missing", tenant=tenant)
def test_default_argument_wins(self, service, tenant):
assert service.get("missing", tenant=tenant, default=42) == 42
def test_set_unknown_key_raises(self, service, tenant):
with pytest.raises(NotFoundError):
service.set("missing", "x", tenant=tenant)
def test_set_requires_tenant(self, service, tenant):
from infrasynth.tenancy.context import tenant_context
ConfigRegistry.register("k", default="d")
with tenant_context(None), pytest.raises(ValidationAppError):
service.set("k", "x", tenant=None)
class TestTypedRoundTrips:
@pytest.mark.parametrize(
("config_type", "value", "expected"),
[
(ConfigType.STRING, "hola", "hola"),
(ConfigType.INT, 7, 7),
(ConfigType.FLOAT, 1.5, 1.5),
(ConfigType.BOOL, True, True),
(ConfigType.JSON, {"a": [1]}, {"a": [1]}),
(ConfigType.DECIMAL, "3.50", decimal.Decimal("3.50")),
(ConfigType.DURATION, "15m", 900),
],
)
def test_round_trip(self, service, tenant, config_type, value, expected):
ConfigRegistry.register("k", type=config_type)
service.set("k", value, tenant=tenant)
result = service.get("k", tenant=tenant)
assert result == expected
assert isinstance(result, type(expected))
class TestSecrets:
def test_secret_is_encrypted_at_rest_and_decrypted_on_read(self, service, tenant):
ConfigRegistry.register("api.token", type=ConfigType.STRING, is_secret=True)
service.set("api.token", "hunter2", tenant=tenant)
row = ConfigValue.all_objects.get(tenant=tenant, key="api.token")
assert row.value != "hunter2"
assert "hunter2" not in str(row.value)
assert service.get("api.token", tenant=tenant) == "hunter2"
def test_secret_metadata_masks_default(self, service, tenant):
ConfigRegistry.register("api.token", type=ConfigType.STRING, default="d", is_secret=True)
meta = service.get_metadata("api.token", tenant=tenant)
assert meta["is_secret"] is True
assert meta["default"] is None
class TestCaching:
def test_second_read_is_cached_until_invalidated(self, service, tenant):
ConfigRegistry.register("k", type=ConfigType.INT, default=0)
service.set("k", 1, tenant=tenant)
assert service.get("k", tenant=tenant) == 1
# Bypass the service: the cache must still hold the old value.
ConfigValue.all_objects.filter(tenant=tenant, key="k").update(value=2)
assert service.get("k", tenant=tenant) == 1
service.invalidate(tenant, "k")
assert service.get("k", tenant=tenant) == 2
def test_write_busts_cache(self, service, tenant):
ConfigRegistry.register("k", type=ConfigType.INT, default=0)
service.set("k", 1, tenant=tenant)
assert service.get("k", tenant=tenant) == 1
service.set("k", 5, tenant=tenant)
assert service.get("k", tenant=tenant) == 5
def test_global_write_busts_global_fallback_cache(self, service, tenant):
ConfigRegistry.register("k", type=ConfigType.INT, default=0)
service.set_global("k", 1)
assert service.get("k", tenant=tenant) == 1
service.set_global("k", 2)
assert service.get("k", tenant=tenant) == 2
class TestGlobalWrites:
def test_allowed_by_default(self, service, tenant):
ConfigRegistry.register("k", default="d")
service.set_global("k", "global")
assert service.get("k", tenant=tenant) == "global"
def test_disabled_raises(self, service, tenant, settings):
ConfigRegistry.register("k", default="d")
settings.INFRASYNTH_CONFIGS = {**settings.INFRASYNTH_CONFIGS, "ALLOW_GLOBAL_WRITES": False}
with pytest.raises(AuthError):
service.set_global("k", "global")
class TestIntrospection:
def test_get_many_skips_unknown(self, service, tenant):
ConfigRegistry.register("a", type=ConfigType.INT, default=1)
ConfigRegistry.register("b", type=ConfigType.INT, default=2)
assert service.get_many(["a", "b", "missing"], tenant=tenant) == {"a": 1, "b": 2}
def test_get_all_filters_by_group(self, service, tenant):
ConfigRegistry.register("a", type=ConfigType.INT, default=1, group="branding")
ConfigRegistry.register("b", type=ConfigType.INT, default=2, group="limits")
assert service.get_all(tenant=tenant, group="branding") == {"a": 1}
def test_get_metadata(self, service, tenant):
ConfigRegistry.register("a", type=ConfigType.INT, default=1, group="branding", label="A")
meta = service.get_metadata("a", tenant=tenant)
assert meta["type"] == "int"
assert meta["group"] == "branding"
assert meta["label"] == "A"
assert meta["is_overridden"] is False
service.set("a", 9, tenant=tenant)
assert service.get_metadata("a", tenant=tenant)["is_overridden"] is True
def test_metadata_unknown_key_raises(self, service, tenant):
with pytest.raises(NotFoundError):
service.get_metadata("missing", tenant=tenant)