infrasynth-backend-kit/tests/test_webhooks/test_dispatch.py
2026-08-28 14:38:47 -05:00

143 lines
5.5 KiB
Python

from unittest import mock
import pytest
from celery.exceptions import Retry
from infrasynth.webhooks.dispatch import _build_payload, _handle_failure, deliver_webhook
from infrasynth.webhooks.models import (
OutboundDelivery,
OutboundEndpoint,
OutboundSubscription,
)
pytestmark = pytest.mark.django_db
@pytest.fixture
def subscription():
endpoint = OutboundEndpoint.objects.create(
name="Target",
url="https://example.com/hook",
secret="test-secret",
)
return OutboundSubscription.objects.create(endpoint=endpoint, event_name="evt.test")
class TestDeliverWebhook:
def test_successful_delivery(self, subscription):
with mock.patch("infrasynth.webhooks.dispatch.requests.post") as post:
post.return_value.status_code = 200
post.return_value.text = "ok"
delivery_id = deliver_webhook.run(subscription.id, "evt.test", {"a": 1}, "")
delivery = OutboundDelivery.objects.get(pk=delivery_id)
assert delivery.status == OutboundDelivery.Status.SUCCESS
assert delivery.response_status == 200
assert delivery.completed_at is not None
request = post.call_args
assert request.args[0] == "https://example.com/hook"
assert request.kwargs["data"] == '{"a": 1}'
signature_header = request.kwargs["headers"]["X-Webhook-Signature"]
from infrasynth.webhooks.signature import verify_signature
assert verify_signature("test-secret", '{"a": 1}', signature_header)
def test_success_sends_signal(self, subscription):
from infrasynth.webhooks.signals import outbound_delivery_succeeded
sent = []
receiver = lambda **kw: sent.append(kw) # noqa: E731
outbound_delivery_succeeded.connect(receiver, weak=False)
with mock.patch("infrasynth.webhooks.dispatch.requests.post") as post:
post.return_value.status_code = 200
post.return_value.text = "ok"
deliver_webhook.run(subscription.id, "evt.test", {}, "")
assert sent
assert sent[0]["event_name"] == "evt.test"
assert sent[0]["status_code"] == 200
outbound_delivery_succeeded.disconnect(receiver)
def test_http_error_gives_up_after_retries(self, subscription):
endpoint = subscription.endpoint
endpoint.retry_policy = {"max_retries": 0}
endpoint.save(update_fields=["retry_policy"])
from infrasynth.webhooks.signals import outbound_delivery_failed
failed = []
receiver = lambda **kw: failed.append(kw) # noqa: E731
outbound_delivery_failed.connect(receiver, weak=False)
with mock.patch("infrasynth.webhooks.dispatch.requests.post") as post:
post.return_value.status_code = 500
post.return_value.text = "boom"
result = deliver_webhook.run(subscription.id, "evt.test", {}, "")
assert result is None
delivery = OutboundDelivery.objects.get(subscription=subscription)
assert delivery.status == OutboundDelivery.Status.FAILED
assert delivery.response_body == "boom"
assert failed
assert "500" in failed[0]["error"]
outbound_delivery_failed.disconnect(receiver)
def test_missing_subscription_returns_none(self):
assert deliver_webhook.run(999999, "evt", {}, "") is None
def test_build_payload_with_template(self, subscription):
body = _build_payload({"type": "created"}, '{"event": "{{ event.type }}", "extra": "x"}')
assert body == '{"event": "created", "extra": "x"}'
def test_build_payload_json_fallback(self, subscription):
body = _build_payload({"a": 1}, "")
assert body == '{"a": 1}'
def test_build_payload_invalid_template_falls_back(self, subscription):
body = _build_payload({"a": 1}, "{{ event|unknown_filter_xyz }}")
assert body == '{"a": 1}'
class _FakeTask:
def __init__(self, retries):
self.request = type("R", (), {"retries": retries})()
def retry(self, exc, countdown):
raise Retry()
class TestHandleFailure:
@pytest.fixture
def delivery(self, subscription):
return OutboundDelivery.objects.create(subscription=subscription, payload={})
def test_schedules_retry_when_under_max(self, subscription, delivery):
endpoint = subscription.endpoint
endpoint.retry_policy = {"max_retries": 5}
endpoint.save(update_fields=["retry_policy"])
fake_task = _FakeTask(retries=1)
with pytest.raises(Retry):
_handle_failure(fake_task, delivery, subscription, endpoint, "conn refused")
delivery.refresh_from_db()
assert delivery.status == OutboundDelivery.Status.RETRYING
assert delivery.next_retry_at is not None
assert "conn refused" in delivery.response_body
def test_marks_failed_after_max_retries(self, subscription, delivery):
endpoint = subscription.endpoint
endpoint.retry_policy = {"max_retries": 3}
endpoint.save(update_fields=["retry_policy"])
from infrasynth.webhooks.signals import outbound_delivery_failed
failed = []
receiver = lambda **kw: failed.append(kw) # noqa: E731
outbound_delivery_failed.connect(receiver, weak=False)
fake_task = _FakeTask(retries=3)
result = _handle_failure(fake_task, delivery, subscription, endpoint, "gave up")
delivery.refresh_from_db()
assert result is None
assert delivery.status == OutboundDelivery.Status.FAILED
assert delivery.completed_at is not None
assert failed
outbound_delivery_failed.disconnect(receiver)