"""Config API endpoints, permissions, and masking.""" import pytest from rest_framework import status from infrasynth.configs.models import ConfigValue from infrasynth.configs.registry import ConfigRegistry, ConfigType from infrasynth.security.models import Role CONFIGS_URL = "/api/v1/configs/" DEFINITIONS_URL = "/api/v1/configs/definitions/" @pytest.fixture(autouse=True) def clean_registry(clean_config_registry): yield @pytest.fixture(autouse=True) def envelope_errors(settings): settings.REST_FRAMEWORK = { **settings.REST_FRAMEWORK, "EXCEPTION_HANDLER": "infrasynth.api.exceptions.envelope_exception_handler", } def _values(response): return {entry["key"]: entry["value"] for entry in response.json()["values"]} def _grant_permissions(user, *codenames): role = Role.objects.create(name="Config Manager", slug=f"cfg-mgr-{user.pk}", permissions=list(codenames)) role.users.add(user) class TestConfigListView: def test_lists_effective_values(self, authenticated_client): ConfigRegistry.register("branding.color", type=ConfigType.STRING, default="#000") response = authenticated_client.get(CONFIGS_URL) assert response.status_code == status.HTTP_200_OK assert _values(response)["branding.color"] == "#000" def test_group_filter(self, authenticated_client): ConfigRegistry.register("a", type=ConfigType.INT, default=1, group="branding") ConfigRegistry.register("b", type=ConfigType.INT, default=2, group="limits") response = authenticated_client.get(f"{CONFIGS_URL}?group=branding") assert _values(response) == {"a": 1} def test_keys_filter(self, authenticated_client): ConfigRegistry.register("a", type=ConfigType.INT, default=1) ConfigRegistry.register("b", type=ConfigType.INT, default=2) response = authenticated_client.get(f"{CONFIGS_URL}?keys=a") assert _values(response) == {"a": 1} def test_secrets_masked(self, authenticated_client, tenant): from infrasynth.configs.services import ConfigService ConfigRegistry.register("api.token", type=ConfigType.STRING, is_secret=True) ConfigService().set("api.token", "hunter2", tenant=tenant) response = authenticated_client.get(CONFIGS_URL) assert _values(response)["api.token"] is None def test_requires_auth(self, api_client): assert api_client.get(CONFIGS_URL).status_code == status.HTTP_401_UNAUTHORIZED class TestConfigDetailView: def test_get_returns_value_and_metadata(self, authenticated_client): ConfigRegistry.register("a", type=ConfigType.INT, default=1, group="branding", label="A") response = authenticated_client.get(f"{CONFIGS_URL}a/") assert response.status_code == status.HTTP_200_OK assert response.data["value"] == 1 assert response.data["type"] == "int" assert response.data["is_overridden"] is False def test_put_sets_tenant_override(self, authenticated_client, tenant): ConfigRegistry.register("a", type=ConfigType.INT, default=1) response = authenticated_client.put(f"{CONFIGS_URL}a/", {"value": 9}, format="json") assert response.status_code == status.HTTP_200_OK assert ConfigValue.all_objects.get(tenant=tenant, key="a").value == 9 assert response.data["is_overridden"] is True def test_put_invalid_value_is_400(self, authenticated_client): ConfigRegistry.register("a", type=ConfigType.INT, default=1) response = authenticated_client.put(f"{CONFIGS_URL}a/", {"value": "nope"}, format="json") assert response.status_code == status.HTTP_400_BAD_REQUEST assert response.data["code"] == "VALIDATION_CONFIG_INVALID" def test_delete_resets_override(self, authenticated_client, tenant): ConfigRegistry.register("a", type=ConfigType.INT, default=1) authenticated_client.put(f"{CONFIGS_URL}a/", {"value": 9}, format="json") response = authenticated_client.delete(f"{CONFIGS_URL}a/") assert response.status_code == status.HTTP_204_NO_CONTENT assert not ConfigValue.all_objects.filter(tenant=tenant, key="a").exists() def test_unknown_key_is_404(self, authenticated_client): assert authenticated_client.get(f"{CONFIGS_URL}missing/").status_code == status.HTTP_404_NOT_FOUND assert ( authenticated_client.put(f"{CONFIGS_URL}missing/", {"value": 1}, format="json").status_code == status.HTTP_404_NOT_FOUND ) def test_secret_value_masked_on_get(self, authenticated_client, tenant): from infrasynth.configs.services import ConfigService ConfigRegistry.register("api.token", type=ConfigType.STRING, is_secret=True) ConfigService().set("api.token", "hunter2", tenant=tenant) response = authenticated_client.get(f"{CONFIGS_URL}api.token/") assert response.data["value"] is None def test_requires_auth(self, api_client): ConfigRegistry.register("a", default=1) assert api_client.get(f"{CONFIGS_URL}a/").status_code == status.HTTP_401_UNAUTHORIZED class TestConfigPermissions: def test_non_owner_without_permission_is_403(self, member_client): ConfigRegistry.register("a", type=ConfigType.INT, default=1) response = member_client.put(f"{CONFIGS_URL}a/", {"value": 2}, format="json") assert response.status_code == status.HTTP_403_FORBIDDEN def test_non_owner_with_permission_can_write(self, member_client, member_user): ConfigRegistry.register("a", type=ConfigType.INT, default=1) _grant_permissions(member_user, "configs.manage") assert member_client.put(f"{CONFIGS_URL}a/", {"value": 2}, format="json").status_code == status.HTTP_200_OK def test_owner_can_write(self, authenticated_client): ConfigRegistry.register("a", type=ConfigType.INT, default=1) response = authenticated_client.put(f"{CONFIGS_URL}a/", {"value": 2}, format="json") assert response.status_code == status.HTTP_200_OK def test_global_write_requires_manage_global(self, member_client, member_user): ConfigRegistry.register("a", type=ConfigType.INT, default=1) assert ( member_client.put(f"{CONFIGS_URL}global/a/", {"value": 2}, format="json").status_code == status.HTTP_403_FORBIDDEN ) _grant_permissions(member_user, "configs.manage_global") assert ( member_client.put(f"{CONFIGS_URL}global/a/", {"value": 2}, format="json").status_code == status.HTTP_200_OK ) def test_global_writes_can_be_disabled(self, authenticated_client, settings): ConfigRegistry.register("a", type=ConfigType.INT, default=1) settings.INFRASYNTH_CONFIGS = {**settings.INFRASYNTH_CONFIGS, "ALLOW_GLOBAL_WRITES": False} response = authenticated_client.put(f"{CONFIGS_URL}global/a/", {"value": 2}, format="json") assert response.status_code == status.HTTP_403_FORBIDDEN assert response.data["code"] == "AUTH_CONFIG_GLOBAL_WRITES_DISABLED" class TestDefinitionsView: def test_lists_registered_schema(self, authenticated_client): ConfigRegistry.register("a", type=ConfigType.INT, default=1, group="branding", label="A") response = authenticated_client.get(DEFINITIONS_URL) assert response.status_code == status.HTTP_200_OK entry = response.json()["definitions"][0] assert entry["key"] == "a" assert entry["type"] == "int" assert entry["default"] == 1 assert entry["group"] == "branding" def test_secret_default_masked(self, authenticated_client): ConfigRegistry.register("api.token", type=ConfigType.STRING, default="d", is_secret=True) entry = authenticated_client.get(DEFINITIONS_URL).json()["definitions"][0] assert entry["default"] is None def test_requires_auth(self, api_client): assert api_client.get(DEFINITIONS_URL).status_code == status.HTTP_401_UNAUTHORIZED