Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions examples/webhooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,11 @@
)
print(f"Retrieved webhook event: {webhook_event['id']}")

replayed: resend.Webhooks.ReplayEventResponse = resend.Webhooks.replay_event(
webhook["id"], event_id
)
print(f"Replayed webhook event: {replayed['id']}")

attempts: resend.Webhooks.ListEventAttemptsResponse = (
resend.Webhooks.list_event_attempts(webhook["id"], event_id, {"limit": 10})
)
Expand Down
48 changes: 48 additions & 0 deletions resend/webhooks/_webhooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,16 @@ class GetEventResponse(BaseResponse):
The webhook event payload.
"""

class ReplayEventResponse(BaseResponse):
object: str
"""
The object type, always "webhook_event".
"""
id: str
"""
The webhook event ID.
"""

class WebhookEventAttempt(TypedDict):
id: str
"""
Expand Down Expand Up @@ -399,6 +409,24 @@ def get_event(cls, webhook_id: str, event_id: str) -> GetEventResponse:
path=path, params={}, verb="get"
).perform_with_content()

@classmethod
def replay_event(cls, webhook_id: str, event_id: str) -> ReplayEventResponse:
"""
Queue one more delivery of a webhook event to its webhook.
see more: https://resend.com/docs/api-reference/webhooks/replay-event

Args:
webhook_id (str): The webhook ID
event_id (str): The webhook event ID

Returns:
ReplayEventResponse: The replayed webhook event
"""
path = f"/webhooks/{webhook_id}/events/{event_id}/replay"
return request.Request[Webhooks.ReplayEventResponse](
path=path, params={}, verb="post"
).perform_with_content()

@classmethod
def list_event_attempts(
cls,
Expand Down Expand Up @@ -658,6 +686,26 @@ async def get_event_async(cls, webhook_id: str, event_id: str) -> GetEventRespon
path=path, params={}, verb="get"
).perform_with_content()

@classmethod
async def replay_event_async(
cls, webhook_id: str, event_id: str
) -> ReplayEventResponse:
"""
Queue one more delivery of a webhook event to its webhook (async).
see more: https://resend.com/docs/api-reference/webhooks/replay-event

Args:
webhook_id (str): The webhook ID
event_id (str): The webhook event ID

Returns:
ReplayEventResponse: The replayed webhook event
"""
path = f"/webhooks/{webhook_id}/events/{event_id}/replay"
return await AsyncRequest[Webhooks.ReplayEventResponse](
path=path, params={}, verb="post"
).perform_with_content()

@classmethod
async def list_event_attempts_async(
cls,
Expand Down
41 changes: 41 additions & 0 deletions tests/webhooks_async_test.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from unittest.mock import AsyncMock

import pytest

import resend
Expand Down Expand Up @@ -160,6 +162,15 @@ async def test_webhooks_get_event_async(self) -> None:
url="https://api.resend.com/webhooks/wh_123/events/msg_1srOrx2ZWZBpBUvZwXKQmoEYga2"
)

async def test_replay_event_async_raises_exception_when_no_content(
self,
) -> None:
self.set_mock_json(None)
with pytest.raises(NoContentError):
_ = await resend.Webhooks.replay_event_async(
"wh_123", "msg_1srOrx2ZWZBpBUvZwXKQmoEYga2"
)

async def test_webhooks_list_event_attempts_async(self) -> None:
response = {
"object": "list",
Expand Down Expand Up @@ -208,3 +219,33 @@ async def test_should_remove_webhooks_async_raise_exception_when_no_content(
self.set_mock_json(None)
with pytest.raises(NoContentError):
_ = await resend.Webhooks.remove_async("wh_123")


class TestWebhooksRequestAsync:
def setup_method(self) -> None:
resend.api_key = "re_123"
self.mock_client = AsyncMock()
self.mock_client.request.return_value = (
b'{"object": "webhook_event", "id": "msg_1srOrx2ZWZBpBUvZwXKQmoEYga2"}',
200,
{"content-type": "application/json"},
)
self.previous_async_http_client = resend.default_async_http_client
resend.default_async_http_client = self.mock_client

def teardown_method(self) -> None:
resend.default_async_http_client = self.previous_async_http_client

async def test_replay_event_async_posts_to_the_replay_path(self) -> None:
event = await resend.Webhooks.replay_event_async(
"wh_123", "msg_1srOrx2ZWZBpBUvZwXKQmoEYga2"
)

assert event["object"] == "webhook_event"
assert event["id"] == "msg_1srOrx2ZWZBpBUvZwXKQmoEYga2"
_, kwargs = self.mock_client.request.call_args
assert kwargs["method"] == "post"
assert (
kwargs["url"]
== "https://api.resend.com/webhooks/wh_123/events/msg_1srOrx2ZWZBpBUvZwXKQmoEYga2/replay"
)
42 changes: 42 additions & 0 deletions tests/webhooks_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,14 @@
import time
from hashlib import sha256
from typing import Any, Dict
from unittest import TestCase
from unittest.mock import create_autospec

import pytest

import resend
from resend.exceptions import NoContentError
from resend.http_client import HTTPClient
from tests.conftest import ResendBaseTest


Expand Down Expand Up @@ -132,6 +136,13 @@ def test_webhooks_get_event(self) -> None:
url="https://api.resend.com/webhooks/wh_123/events/msg_1srOrx2ZWZBpBUvZwXKQmoEYga2"
)

def test_replay_event_raises_exception_when_no_content(self) -> None:
self.set_mock_json(None)
with pytest.raises(NoContentError):
_ = resend.Webhooks.replay_event(
"wh_123", "msg_1srOrx2ZWZBpBUvZwXKQmoEYga2"
)

def test_webhooks_list_event_attempts(self) -> None:
response = {
"object": "list",
Expand Down Expand Up @@ -169,6 +180,37 @@ def test_webhooks_remove(self) -> None:
assert result["deleted"] is True


class TestWebhooksRequest(TestCase):
def setUp(self) -> None:
resend.api_key = "re_123"
self.mock_client = create_autospec(HTTPClient, instance=True)
self.mock_client.name = "mock"
self.mock_client.request.return_value = (
b'{"object": "webhook_event", "id": "msg_1srOrx2ZWZBpBUvZwXKQmoEYga2"}',
200,
{"Content-Type": "application/json"},
)
self.previous_http_client = resend.default_http_client
resend.default_http_client = self.mock_client

def tearDown(self) -> None:
resend.default_http_client = self.previous_http_client

def test_replay_event_posts_to_the_replay_path(self) -> None:
event = resend.Webhooks.replay_event(
"wh_123", "msg_1srOrx2ZWZBpBUvZwXKQmoEYga2"
)

assert event["object"] == "webhook_event"
assert event["id"] == "msg_1srOrx2ZWZBpBUvZwXKQmoEYga2"
_, kwargs = self.mock_client.request.call_args
assert kwargs["method"] == "post"
assert (
kwargs["url"]
== "https://api.resend.com/webhooks/wh_123/events/msg_1srOrx2ZWZBpBUvZwXKQmoEYga2/replay"
)
Comment thread
gabrielmfern marked this conversation as resolved.


class TestWebhookVerification:
"""Test webhook signature verification"""

Expand Down
Loading