1- """Unified tests for webhook (sync + async)."""
1+ """Unified tests for webhook (sync + async).
2+
3+ Webhook CRUD tests bind to a specific already-completed run (actor_run_id) instead of to an actor (actor_id).
4+ This prevents webhooks from firing when other integration tests run the same actor, which would cause
5+ "Webhook was removed" error emails.
6+ """
27
38from __future__ import annotations
49
510from typing import TYPE_CHECKING , cast
611
712if TYPE_CHECKING :
813 from apify_client import ApifyClient , ApifyClientAsync
9- from apify_client ._models import Actor , ListOfWebhookDispatches , ListOfWebhooks , Webhook , WebhookDispatch
14+ from apify_client ._models import ListOfWebhookDispatches , ListOfWebhooks , Run , Webhook , WebhookDispatch
1015
1116
1217from ._utils import maybe_await
13- from apify_client ._models import WebhookEventType
18+ from apify_client ._models import ActorJobStatus , WebhookEventType
1419
1520HELLO_WORLD_ACTOR = 'apify/hello-world'
1621
1722
23+ async def _get_finished_run_id (client : ApifyClient | ApifyClientAsync ) -> str :
24+ """Get the ID of an already-completed run of the hello-world actor.
25+
26+ Using a finished run's ID for webhook conditions ensures the webhook will never actually fire,
27+ since a completed run won't emit new events.
28+ """
29+ runs_page = await maybe_await (client .actor (HELLO_WORLD_ACTOR ).runs ().list (limit = 1 , status = ActorJobStatus .SUCCEEDED ))
30+ assert runs_page is not None
31+ assert len (runs_page .items ) > 0 , 'No completed runs found for hello-world actor'
32+ run = cast ('Run' , runs_page .items [0 ])
33+ return run .id
34+
35+
1836async def test_list_webhooks (client : ApifyClient | ApifyClientAsync ) -> None :
1937 """Test listing webhooks."""
2038 result = await maybe_await (client .webhooks ().list (limit = 10 ))
@@ -38,17 +56,15 @@ async def test_list_webhooks_pagination(client: ApifyClient | ApifyClientAsync)
3856
3957async def test_webhook_create_and_get (client : ApifyClient | ApifyClientAsync ) -> None :
4058 """Test creating a webhook and retrieving it."""
41- # Get actor ID for webhook condition
42- result = await maybe_await (client .actor (HELLO_WORLD_ACTOR ).get ())
43- actor = cast ('Actor' , result )
44- assert actor is not None
59+ run_id = await _get_finished_run_id (client )
4560
46- # Create webhook (use httpbin as dummy endpoint )
61+ # Create webhook bound to a finished run (will never fire )
4762 result = await maybe_await (
4863 client .webhooks ().create (
4964 event_types = [WebhookEventType .ACTOR_RUN_SUCCEEDED ],
5065 request_url = 'https://httpbin.org/post' ,
51- actor_id = actor .id ,
66+ actor_run_id = run_id ,
67+ is_ad_hoc = True ,
5268 )
5369 )
5470 created_webhook = cast ('Webhook' , result )
@@ -69,27 +85,26 @@ async def test_webhook_create_and_get(client: ApifyClient | ApifyClientAsync) ->
6985
7086async def test_webhook_update (client : ApifyClient | ApifyClientAsync ) -> None :
7187 """Test updating a webhook."""
72- result = await maybe_await (client .actor (HELLO_WORLD_ACTOR ).get ())
73- actor = cast ('Actor' , result )
74- assert actor is not None
88+ run_id = await _get_finished_run_id (client )
7589
76- # Create webhook
90+ # Create webhook bound to a finished run
7791 result = await maybe_await (
7892 client .webhooks ().create (
7993 event_types = [WebhookEventType .ACTOR_RUN_SUCCEEDED ],
8094 request_url = 'https://httpbin.org/post' ,
81- actor_id = actor .id ,
95+ actor_run_id = run_id ,
96+ is_ad_hoc = True ,
8297 )
8398 )
8499 created_webhook = cast ('Webhook' , result )
85100 webhook_client = client .webhook (created_webhook .id )
86101
87102 try :
88- # Update webhook (must include actor_id as condition is required)
103+ # Update webhook
89104 result = await maybe_await (
90105 webhook_client .update (
91106 request_url = 'https://httpbin.org/anything' ,
92- actor_id = actor . id ,
107+ actor_run_id = run_id ,
93108 )
94109 )
95110 updated_webhook = cast ('Webhook' , result )
@@ -100,23 +115,22 @@ async def test_webhook_update(client: ApifyClient | ApifyClientAsync) -> None:
100115
101116async def test_webhook_test (client : ApifyClient | ApifyClientAsync ) -> None :
102117 """Test the webhook test endpoint."""
103- result = await maybe_await (client .actor (HELLO_WORLD_ACTOR ).get ())
104- actor = cast ('Actor' , result )
105- assert actor is not None
118+ run_id = await _get_finished_run_id (client )
106119
107- # Create webhook
120+ # Create webhook bound to a finished run
108121 result = await maybe_await (
109122 client .webhooks ().create (
110123 event_types = [WebhookEventType .ACTOR_RUN_SUCCEEDED ],
111124 request_url = 'https://httpbin.org/post' ,
112- actor_id = actor .id ,
125+ actor_run_id = run_id ,
126+ is_ad_hoc = True ,
113127 )
114128 )
115129 created_webhook = cast ('Webhook' , result )
116130 webhook_client = client .webhook (created_webhook .id )
117131
118132 try :
119- # Test webhook (creates a dispatch)
133+ # Test webhook (creates a dispatch with dummy payload )
120134 result = await maybe_await (webhook_client .test ())
121135 dispatch = cast ('WebhookDispatch' , result )
122136 assert dispatch is not None
@@ -127,16 +141,15 @@ async def test_webhook_test(client: ApifyClient | ApifyClientAsync) -> None:
127141
128142async def test_webhook_dispatches (client : ApifyClient | ApifyClientAsync ) -> None :
129143 """Test listing webhook dispatches."""
130- result = await maybe_await (client .actor (HELLO_WORLD_ACTOR ).get ())
131- actor = cast ('Actor' , result )
132- assert actor is not None
144+ run_id = await _get_finished_run_id (client )
133145
134- # Create webhook
146+ # Create webhook bound to a finished run
135147 result = await maybe_await (
136148 client .webhooks ().create (
137149 event_types = [WebhookEventType .ACTOR_RUN_SUCCEEDED ],
138150 request_url = 'https://httpbin.org/post' ,
139- actor_id = actor .id ,
151+ actor_run_id = run_id ,
152+ is_ad_hoc = True ,
140153 )
141154 )
142155 created_webhook = cast ('Webhook' , result )
@@ -158,16 +171,15 @@ async def test_webhook_dispatches(client: ApifyClient | ApifyClientAsync) -> Non
158171
159172async def test_webhook_delete (client : ApifyClient | ApifyClientAsync ) -> None :
160173 """Test deleting a webhook."""
161- result = await maybe_await (client .actor (HELLO_WORLD_ACTOR ).get ())
162- actor = cast ('Actor' , result )
163- assert actor is not None
174+ run_id = await _get_finished_run_id (client )
164175
165- # Create webhook
176+ # Create webhook bound to a finished run
166177 result = await maybe_await (
167178 client .webhooks ().create (
168179 event_types = [WebhookEventType .ACTOR_RUN_SUCCEEDED ],
169180 request_url = 'https://httpbin.org/post' ,
170- actor_id = actor .id ,
181+ actor_run_id = run_id ,
182+ is_ad_hoc = True ,
171183 )
172184 )
173185 created_webhook = cast ('Webhook' , result )
0 commit comments