infrasynth-backend-kit/tests/test_features/test_signals.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

101 lines
3.9 KiB
Python

"""Feature flag/override mutation signals + cache invalidation (Part B1)."""
import pytest
from django.core.cache import cache
from infrasynth.features.models import FeatureFlag
from infrasynth.features.registry import FeatureRegistry
from infrasynth.features.services import FeatureService
from infrasynth.features.signals import (
flag_created,
flag_deleted,
flag_toggled,
override_created,
override_deleted,
)
FLAGS_URL = "/api/v1/features/"
OVERRIDES_URL = "/api/v1/features/overrides/"
pytestmark = pytest.mark.django_db
@pytest.fixture(autouse=True)
def clean_registry(clean_feature_registry):
cache.clear()
yield
cache.clear()
def _capture(signal):
received = []
receiver = lambda **kwargs: received.append(kwargs) # noqa: E731
signal.connect(receiver, weak=False)
return received, receiver
class TestFlagSignals:
def test_create_emits_flag_created(self, authenticated_client):
received, receiver = _capture(flag_created)
try:
response = authenticated_client.post(
FLAGS_URL, {"slug": "new-flag", "name": "New", "is_active": True}, format="json"
)
finally:
flag_created.disconnect(receiver)
assert response.status_code == 201
assert len(received) == 1
assert received[0]["flag_slug"] == "new-flag"
assert received[0]["is_active"] is True
assert received[0]["actor_id"] is not None
def test_toggle_emits_flag_toggled_only_on_change(self, authenticated_client):
flag = FeatureFlag.objects.create(slug="toggle.flag", name="Toggle", is_active=True)
received, receiver = _capture(flag_toggled)
try:
authenticated_client.patch(f"{FLAGS_URL}{flag.pk}/", {"name": "Renamed"}, format="json")
assert received == []
authenticated_client.patch(f"{FLAGS_URL}{flag.pk}/", {"is_active": False}, format="json")
finally:
flag_toggled.disconnect(receiver)
assert len(received) == 1
assert received[0]["flag_slug"] == "toggle.flag"
assert received[0]["is_active"] is False
assert received[0]["previous_is_active"] is True
def test_delete_emits_flag_deleted(self, authenticated_client):
flag = FeatureFlag.objects.create(slug="delete.flag", name="Delete")
received, receiver = _capture(flag_deleted)
try:
response = authenticated_client.delete(f"{FLAGS_URL}{flag.pk}/")
finally:
flag_deleted.disconnect(receiver)
assert response.status_code == 204
assert len(received) == 1
assert received[0]["flag_slug"] == "delete.flag"
def test_mutation_busts_cache(self, authenticated_client):
FeatureRegistry.register("cache-bust", default=False)
service = FeatureService()
assert service.is_enabled("cache-bust") is False
authenticated_client.post(FLAGS_URL, {"slug": "cache-bust", "name": "Cache", "is_active": True}, format="json")
assert service.is_enabled("cache-bust") is True
class TestOverrideSignals:
def test_create_and_delete_emit(self, authenticated_client, user):
flag = FeatureFlag.objects.create(slug="ov.flag", name="Override", is_active=True)
created, rec_create = _capture(override_created)
deleted, rec_delete = _capture(override_deleted)
try:
response = authenticated_client.post(
OVERRIDES_URL, {"flag": flag.pk, "user": user.pk, "is_enabled": True}, format="json"
)
override_id = response.json()["id"]
authenticated_client.delete(f"{OVERRIDES_URL}{override_id}/")
finally:
override_created.disconnect(rec_create)
override_deleted.disconnect(rec_delete)
assert created[0]["flag_slug"] == "ov.flag"
assert created[0]["user_id"] == user.pk
assert deleted[0]["flag_slug"] == "ov.flag"