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)