Skip to content

Commit 7cc58dd

Browse files
vdusekclaude
andcommitted
Fix webhook integration tests to not cause spurious error emails
Bind webhooks to a specific already-completed run (actor_run_id + is_ad_hoc) instead of to the actor (actor_id). A finished run won't emit new events, so the webhooks will never fire when other tests run the same actor. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 33c2edb commit 7cc58dd

1 file changed

Lines changed: 44 additions & 32 deletions

File tree

tests/integration/test_webhook.py

Lines changed: 44 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,38 @@
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

38
from __future__ import annotations
49

510
from typing import TYPE_CHECKING, cast
611

712
if 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

1217
from ._utils import maybe_await
13-
from apify_client._models import WebhookEventType
18+
from apify_client._models import ActorJobStatus, WebhookEventType
1419

1520
HELLO_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+
1836
async 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

3957
async 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

7086
async 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

101116
async 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

128142
async 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

159172
async 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

Comments
 (0)