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