143 lines
5.5 KiB
Python
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)
|