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