diff --git a/examples/webhooks.py b/examples/webhooks.py index 2341b54..f34e56f 100644 --- a/examples/webhooks.py +++ b/examples/webhooks.py @@ -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}) ) diff --git a/resend/webhooks/_webhooks.py b/resend/webhooks/_webhooks.py index c159b94..4b5df39 100644 --- a/resend/webhooks/_webhooks.py +++ b/resend/webhooks/_webhooks.py @@ -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 """ @@ -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, @@ -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, diff --git a/tests/webhooks_async_test.py b/tests/webhooks_async_test.py index d615b7d..beb6d9d 100644 --- a/tests/webhooks_async_test.py +++ b/tests/webhooks_async_test.py @@ -1,3 +1,5 @@ +from unittest.mock import AsyncMock + import pytest import resend @@ -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", @@ -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" + ) diff --git a/tests/webhooks_test.py b/tests/webhooks_test.py index 351af02..fd94d17 100644 --- a/tests/webhooks_test.py +++ b/tests/webhooks_test.py @@ -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 @@ -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", @@ -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" + ) + + class TestWebhookVerification: """Test webhook signature verification"""