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

76 lines
3.1 KiB
Python

"""Tenant isolation for configuration values (TENANCY.md §10)."""
import pytest
from infrasynth.configs.models import ConfigValue
from infrasynth.configs.registry import ConfigRegistry, ConfigType
from infrasynth.configs.services import ConfigService
from infrasynth.tenancy.context import tenant_context
from infrasynth.tenancy.models import Tenant
@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",
}
@pytest.fixture
def service():
return ConfigService()
@pytest.fixture
def other_tenant():
return Tenant.objects.create(slug="other-config-ws", name="Other Workspace")
class TestServiceIsolation:
def test_tenant_a_cannot_read_tenant_b(self, service, tenant, other_tenant):
ConfigRegistry.register("k", type=ConfigType.STRING, default="default")
service.set("k", "b-secret", tenant=other_tenant)
assert service.get("k", tenant=tenant) == "default"
def test_absent_override_falls_back_to_global_not_other_tenant(self, service, tenant, other_tenant):
ConfigRegistry.register("k", type=ConfigType.STRING, default="default")
service.set_global("k", "global")
service.set("k", "b-secret", tenant=other_tenant)
assert service.get("k", tenant=tenant) == "global"
def test_reset_only_affects_own_tenant(self, service, tenant, other_tenant):
ConfigRegistry.register("k", type=ConfigType.STRING, default="default")
service.set("k", "b-value", tenant=other_tenant)
assert service.reset("k", tenant=tenant) is False
assert service.get("k", tenant=other_tenant) == "b-value"
class TestManagerIsolation:
def test_scoped_manager_hides_other_tenant(self, tenant, other_tenant):
ConfigValue.all_objects.create(tenant=other_tenant, key="other.only", value="x")
with tenant_context(tenant):
assert ConfigValue.objects.count() == 0
assert ConfigValue.objects.filter(key="other.only").delete()[0] == 0
with tenant_context(other_tenant):
assert ConfigValue.objects.filter(key="other.only").exists()
class TestApiIsolation:
def test_other_tenant_key_is_404_not_403(self, authenticated_client, tenant, other_tenant):
ConfigValue.all_objects.create(tenant=other_tenant, key="other.only", value="x")
response = authenticated_client.get("/api/v1/configs/other.only/")
assert response.status_code == 404
def test_list_never_exposes_other_tenant_values(self, authenticated_client, tenant, other_tenant):
ConfigRegistry.register("shared.key", type=ConfigType.STRING, default="default")
ConfigValue.all_objects.create(tenant=other_tenant, key="shared.key", value="b-secret")
response = authenticated_client.get("/api/v1/configs/")
assert response.status_code == 200
values = {entry["key"]: entry["value"] for entry in response.json()["values"]}
assert values["shared.key"] == "default"