308 lines
11 KiB
Python
308 lines
11 KiB
Python
from contextlib import contextmanager
|
|
from unittest import mock
|
|
|
|
import pytest
|
|
from django.core.exceptions import ValidationError
|
|
from django.db import transaction
|
|
from django.dispatch import receiver
|
|
from django.test import override_settings
|
|
|
|
from infrasynth.audit.models import SecurityEvent
|
|
from infrasynth.features.models import FeatureFlag, FeatureFlagOverride
|
|
from infrasynth.features.registry import FeatureRegistry
|
|
from infrasynth.features.services import FeatureService
|
|
from infrasynth.notifications.resolvers import VariableResolverRegistry
|
|
from infrasynth.webhooks.models import (
|
|
OutboundDelivery,
|
|
OutboundEndpoint,
|
|
OutboundSubscription,
|
|
)
|
|
from infrasynth.webhooks.registry import EventRegistry
|
|
from infrasynth.webhooks.signals import (
|
|
outbound_delivery_failed,
|
|
outbound_delivery_succeeded,
|
|
)
|
|
from infrasynth.workflows.validators import DataValidatorRegistry
|
|
|
|
pytestmark = pytest.mark.django_db
|
|
|
|
|
|
@contextmanager
|
|
def _emit_and_run_commit_hooks():
|
|
with transaction.atomic():
|
|
yield
|
|
connection = transaction.get_connection()
|
|
pending = list(connection.run_on_commit)
|
|
connection.run_on_commit = []
|
|
for entry in pending:
|
|
func = entry[1]
|
|
func()
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def clean_registries():
|
|
saved_events = dict(EventRegistry._events)
|
|
saved_features = dict(FeatureRegistry._features)
|
|
saved_resolvers = dict(VariableResolverRegistry._resolvers)
|
|
saved_validators = dict(DataValidatorRegistry._validators)
|
|
yield
|
|
EventRegistry._events = saved_events
|
|
FeatureRegistry._features = saved_features
|
|
VariableResolverRegistry._resolvers = saved_resolvers
|
|
DataValidatorRegistry._validators = saved_validators
|
|
|
|
|
|
class TestOutboundDeliverySignalToAuditIntegration:
|
|
def test_outbound_delivery_signal_triggers_audit_event(self, user):
|
|
endpoint = OutboundEndpoint.objects.create(
|
|
name="Test Endpoint",
|
|
url="https://example.com/hook",
|
|
secret="test-secret",
|
|
)
|
|
subscription = OutboundSubscription.objects.create(
|
|
endpoint=endpoint,
|
|
event_name="test.event",
|
|
)
|
|
delivery = OutboundDelivery.objects.create(
|
|
subscription=subscription,
|
|
payload={"key": "value"},
|
|
status=OutboundDelivery.Status.RETRYING,
|
|
)
|
|
|
|
@receiver(outbound_delivery_succeeded)
|
|
def create_audit_on_success(sender, **kwargs):
|
|
SecurityEvent.objects.create(
|
|
event_type="webhook_outbound_success",
|
|
actor=user,
|
|
ip_address="127.0.0.1",
|
|
metadata={
|
|
"delivery_id": kwargs.get("delivery_id"),
|
|
"event_name": kwargs.get("event_name"),
|
|
"status_code": kwargs.get("status_code"),
|
|
},
|
|
request_id="integration-test-001",
|
|
)
|
|
|
|
outbound_delivery_succeeded.send(
|
|
sender=OutboundDelivery,
|
|
delivery_id=delivery.id,
|
|
event_name="test.event",
|
|
status_code=200,
|
|
)
|
|
|
|
assert SecurityEvent.objects.filter(event_type="webhook_outbound_success").exists()
|
|
se = SecurityEvent.objects.get(event_type="webhook_outbound_success")
|
|
assert se.metadata["delivery_id"] == delivery.id
|
|
assert se.metadata["event_name"] == "test.event"
|
|
assert se.metadata["status_code"] == 200
|
|
assert se.actor == user
|
|
|
|
def test_outbound_delivery_failed_signal_triggers_audit_event(self, user):
|
|
endpoint = OutboundEndpoint.objects.create(
|
|
name="Test Endpoint",
|
|
url="https://example.com/hook",
|
|
secret="test-secret",
|
|
)
|
|
subscription = OutboundSubscription.objects.create(
|
|
endpoint=endpoint,
|
|
event_name="test.event",
|
|
)
|
|
delivery = OutboundDelivery.objects.create(
|
|
subscription=subscription,
|
|
payload={"key": "value"},
|
|
status=OutboundDelivery.Status.RETRYING,
|
|
)
|
|
|
|
@receiver(outbound_delivery_failed)
|
|
def create_audit_on_failure(sender, **kwargs):
|
|
SecurityEvent.objects.create(
|
|
event_type="webhook_outbound_failed",
|
|
actor=user,
|
|
ip_address="127.0.0.1",
|
|
metadata={
|
|
"delivery_id": kwargs.get("delivery_id"),
|
|
"event_name": kwargs.get("event_name"),
|
|
"error": kwargs.get("error"),
|
|
},
|
|
request_id="integration-test-002",
|
|
)
|
|
|
|
outbound_delivery_failed.send(
|
|
sender=OutboundDelivery,
|
|
delivery_id=delivery.id,
|
|
event_name="test.event",
|
|
error="Connection timed out",
|
|
)
|
|
|
|
assert SecurityEvent.objects.filter(event_type="webhook_outbound_failed").exists()
|
|
se = SecurityEvent.objects.get(event_type="webhook_outbound_failed")
|
|
assert se.metadata["delivery_id"] == delivery.id
|
|
assert se.metadata["error"] == "Connection timed out"
|
|
assert se.actor == user
|
|
|
|
|
|
class TestEventRegistryEmissionIntegration:
|
|
@override_settings(CELERY_TASK_ALWAYS_EAGER=True)
|
|
def test_event_registry_emission_creates_outbound_deliveries(self):
|
|
EventRegistry.register(
|
|
"integration.order.created",
|
|
description="Order created event",
|
|
example_payload={"order_id": "ord_001"},
|
|
)
|
|
|
|
endpoint = OutboundEndpoint.objects.create(
|
|
name="Integration Endpoint",
|
|
url="https://example.com/hook",
|
|
secret="test-secret",
|
|
)
|
|
subscription = OutboundSubscription.objects.create(
|
|
endpoint=endpoint,
|
|
event_name="integration.order.created",
|
|
)
|
|
|
|
payload = {"order_id": "ord_001", "amount": 100}
|
|
|
|
with mock.patch("infrasynth.webhooks.dispatch.requests.post") as post:
|
|
post.return_value.status_code = 200
|
|
post.return_value.text = "ok"
|
|
with _emit_and_run_commit_hooks():
|
|
EventRegistry.emit("integration.order.created", payload)
|
|
|
|
assert OutboundDelivery.objects.filter(subscription=subscription).exists()
|
|
delivery = OutboundDelivery.objects.get(subscription=subscription)
|
|
assert delivery.status == OutboundDelivery.Status.SUCCESS
|
|
assert delivery.payload is not None
|
|
assert delivery.response_status == 200
|
|
|
|
|
|
class TestFeatureRegistryRegistrationAndDiscovery:
|
|
def test_feature_registry_registration_and_discovery(self, user):
|
|
FeatureRegistry.register(
|
|
"app_a.premium_feature",
|
|
name="Premium Feature",
|
|
description="A premium feature from App A",
|
|
default=False,
|
|
category="app_a",
|
|
)
|
|
FeatureRegistry.register(
|
|
"app_b.analytics",
|
|
name="Analytics",
|
|
description="Analytics feature from App B",
|
|
default=True,
|
|
category="app_b",
|
|
)
|
|
|
|
service = FeatureService()
|
|
|
|
assert service.is_enabled("app_a.premium_feature") is False
|
|
assert service.is_enabled("app_b.analytics") is True
|
|
assert service.is_enabled("nonexistent.feature") is False
|
|
|
|
flag = FeatureFlag.objects.create(
|
|
slug="app_a.premium_feature",
|
|
name="Premium Feature Override",
|
|
is_active=True,
|
|
rollout_percentage=100,
|
|
)
|
|
|
|
assert service.is_enabled("app_a.premium_feature") is True
|
|
|
|
FeatureFlagOverride.objects.create(
|
|
flag=flag,
|
|
user=user,
|
|
is_enabled=False,
|
|
)
|
|
|
|
assert service.is_enabled("app_a.premium_feature", user=user) is False
|
|
|
|
FeatureFlag.objects.create(
|
|
slug="app_c.experimental",
|
|
name="Experimental",
|
|
is_active=False,
|
|
rollout_percentage=100,
|
|
)
|
|
|
|
assert service.is_enabled("app_c.experimental") is False
|
|
|
|
|
|
class TestVariableResolverRegistryCrossApp:
|
|
def test_variable_resolver_registry_cross_app(self):
|
|
@VariableResolverRegistry.register(
|
|
"user_name",
|
|
label="User Name",
|
|
description="Resolves the user's full name",
|
|
namespace="app_a",
|
|
)
|
|
def resolve_user_name(recipient, context, request=None):
|
|
return context.get("user_name", "Unknown")
|
|
|
|
@VariableResolverRegistry.register(
|
|
"order_total",
|
|
label="Order Total",
|
|
description="Resolves the order total",
|
|
namespace="app_b",
|
|
)
|
|
def resolve_order_total(recipient, context, request=None):
|
|
return context.get("order_total", 0)
|
|
|
|
context_a = {"user_name": "Alice"}
|
|
result_a = VariableResolverRegistry.resolve(
|
|
"user_name", "alice@example.com", context_a, namespace="app_a"
|
|
)
|
|
assert result_a == "Alice"
|
|
|
|
context_b = {"order_total": 99.99}
|
|
result_b = VariableResolverRegistry.resolve(
|
|
"order_total", "alice@example.com", context_b, namespace="app_b"
|
|
)
|
|
assert result_b == 99.99
|
|
|
|
result_unknown = VariableResolverRegistry.resolve(
|
|
"nonexistent", "alice@example.com", {}, namespace="app_a"
|
|
)
|
|
assert result_unknown is None
|
|
|
|
available_a = VariableResolverRegistry.get_available_variables(namespace="app_a")
|
|
assert len(available_a) == 1
|
|
assert "app_a:user_name" in available_a
|
|
|
|
available_b = VariableResolverRegistry.get_available_variables(namespace="app_b")
|
|
assert len(available_b) == 1
|
|
assert "app_b:order_total" in available_b
|
|
|
|
available_all = VariableResolverRegistry.get_available_variables()
|
|
assert len(available_all) == 2
|
|
|
|
|
|
class TestDataValidatorRegistryCrossApp:
|
|
def test_data_validator_registry_cross_app(self):
|
|
class AppAValidator:
|
|
def validate(self, node, data, context):
|
|
return {"validated": True, **data}
|
|
|
|
class AppBValidator:
|
|
def validate(self, node, data, context):
|
|
if not data.get("required_field"):
|
|
raise ValidationError({"required_field": "This field is required."})
|
|
return data
|
|
|
|
DataValidatorRegistry.register("app_a_workflow", AppAValidator())
|
|
DataValidatorRegistry.register("app_b_workflow", AppBValidator())
|
|
|
|
validator_a = DataValidatorRegistry.get("app_a_workflow")
|
|
assert validator_a is not None
|
|
result = validator_a.validate(None, {"key": "val"}, {})
|
|
assert result["validated"] is True
|
|
assert result["key"] == "val"
|
|
|
|
validator_b = DataValidatorRegistry.get("app_b_workflow")
|
|
assert validator_b is not None
|
|
result_valid = validator_b.validate(None, {"required_field": "present"}, {})
|
|
assert result_valid == {"required_field": "present"}
|
|
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
validator_b.validate(None, {}, {})
|
|
assert "required_field" in exc_info.value.message_dict
|
|
|
|
missing = DataValidatorRegistry.get("nonexistent_workflow")
|
|
assert missing is None
|