port-ocean 0.19.3__py3-none-any.whl → 0.20.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- port_ocean/context/ocean.py +2 -7
- port_ocean/core/handlers/webhook/abstract_webhook_processor.py +18 -2
- port_ocean/core/handlers/webhook/processor_manager.py +107 -65
- port_ocean/core/handlers/webhook/webhook_event.py +71 -8
- port_ocean/core/integrations/mixins/live_events.py +88 -0
- port_ocean/ocean.py +4 -2
- port_ocean/tests/core/handlers/mixins/test_live_events.py +404 -0
- port_ocean/tests/core/handlers/webhook/test_abstract_webhook_processor.py +88 -97
- port_ocean/tests/core/handlers/webhook/test_processor_manager.py +1161 -288
- port_ocean/tests/core/handlers/webhook/test_webhook_event.py +97 -56
- {port_ocean-0.19.3.dist-info → port_ocean-0.20.0.dist-info}/METADATA +1 -1
- {port_ocean-0.19.3.dist-info → port_ocean-0.20.0.dist-info}/RECORD +15 -13
- {port_ocean-0.19.3.dist-info → port_ocean-0.20.0.dist-info}/LICENSE.md +0 -0
- {port_ocean-0.19.3.dist-info → port_ocean-0.20.0.dist-info}/WHEEL +0 -0
- {port_ocean-0.19.3.dist-info → port_ocean-0.20.0.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,404 @@
|
|
1
|
+
from typing import Any
|
2
|
+
from httpx import Response
|
3
|
+
import pytest
|
4
|
+
from unittest.mock import AsyncMock, MagicMock, patch
|
5
|
+
from port_ocean.clients.port.client import PortClient
|
6
|
+
from port_ocean.clients.port.types import UserAgentType
|
7
|
+
from port_ocean.context.ocean import PortOceanContext
|
8
|
+
from port_ocean.core.handlers.entities_state_applier.port.applier import (
|
9
|
+
HttpEntitiesStateApplier,
|
10
|
+
)
|
11
|
+
from port_ocean.core.handlers.entity_processor.jq_entity_processor import (
|
12
|
+
JQEntityProcessor,
|
13
|
+
)
|
14
|
+
from port_ocean.core.handlers.webhook.webhook_event import WebhookEventRawResults
|
15
|
+
from port_ocean.core.integrations.mixins.live_events import LiveEventsMixin
|
16
|
+
from port_ocean.core.handlers.port_app_config.models import (
|
17
|
+
EntityMapping,
|
18
|
+
MappingsConfig,
|
19
|
+
PortAppConfig,
|
20
|
+
PortResourceConfig,
|
21
|
+
ResourceConfig,
|
22
|
+
Selector,
|
23
|
+
)
|
24
|
+
from port_ocean.core.models import Entity
|
25
|
+
from port_ocean.core.ocean_types import CalculationResult, EntitySelectorDiff
|
26
|
+
from port_ocean.ocean import Ocean
|
27
|
+
|
28
|
+
entity = Entity(
|
29
|
+
identifier="repo-one",
|
30
|
+
blueprint="service",
|
31
|
+
title="repo-one",
|
32
|
+
team=[],
|
33
|
+
properties={
|
34
|
+
"url": "https://example.com/repo-one",
|
35
|
+
"defaultBranch": "main",
|
36
|
+
},
|
37
|
+
relations={},
|
38
|
+
)
|
39
|
+
|
40
|
+
expected_entities = [
|
41
|
+
Entity(
|
42
|
+
identifier="repo-one",
|
43
|
+
blueprint="service",
|
44
|
+
title="repo-one",
|
45
|
+
team=[],
|
46
|
+
properties={
|
47
|
+
"url": "https://example.com/repo-one",
|
48
|
+
"defaultBranch": "main",
|
49
|
+
},
|
50
|
+
relations={},
|
51
|
+
),
|
52
|
+
Entity(
|
53
|
+
identifier="repo-two",
|
54
|
+
blueprint="service",
|
55
|
+
title="repo-two",
|
56
|
+
team=[],
|
57
|
+
properties={
|
58
|
+
"url": "https://example.com/repo-two",
|
59
|
+
"defaultBranch": "develop",
|
60
|
+
},
|
61
|
+
relations={},
|
62
|
+
),
|
63
|
+
Entity(
|
64
|
+
identifier="repo-three",
|
65
|
+
blueprint="service",
|
66
|
+
title="repo-three",
|
67
|
+
team=[],
|
68
|
+
properties={
|
69
|
+
"url": "https://example.com/repo-three",
|
70
|
+
"defaultBranch": "master",
|
71
|
+
},
|
72
|
+
relations={},
|
73
|
+
),
|
74
|
+
]
|
75
|
+
|
76
|
+
event_data_for_three_entities_for_repository_resource = [
|
77
|
+
{
|
78
|
+
"name": "repo-one",
|
79
|
+
"links": {"html": {"href": "https://example.com/repo-one"}},
|
80
|
+
"main_branch": "main",
|
81
|
+
},
|
82
|
+
{
|
83
|
+
"name": "repo-two",
|
84
|
+
"links": {"html": {"href": "https://example.com/repo-two"}},
|
85
|
+
"main_branch": "develop",
|
86
|
+
},
|
87
|
+
{
|
88
|
+
"name": "repo-three",
|
89
|
+
"links": {"html": {"href": "https://example.com/repo-three"}},
|
90
|
+
"main_branch": "master",
|
91
|
+
},
|
92
|
+
]
|
93
|
+
|
94
|
+
|
95
|
+
one_webhook_event_raw_results_for_creation = WebhookEventRawResults(
|
96
|
+
updated_raw_results=[
|
97
|
+
{
|
98
|
+
"name": "repo-one",
|
99
|
+
"links": {"html": {"href": "https://example.com/repo-one"}},
|
100
|
+
"main_branch": "main",
|
101
|
+
}
|
102
|
+
],
|
103
|
+
deleted_raw_results=[],
|
104
|
+
)
|
105
|
+
one_webhook_event_raw_results_for_creation.resource = ResourceConfig(
|
106
|
+
kind="repository",
|
107
|
+
selector=Selector(query="true"),
|
108
|
+
port=PortResourceConfig(
|
109
|
+
entity=MappingsConfig(
|
110
|
+
mappings=EntityMapping(
|
111
|
+
identifier=".name",
|
112
|
+
title=".name",
|
113
|
+
blueprint='"service"',
|
114
|
+
properties={
|
115
|
+
"url": ".links.html.href",
|
116
|
+
"defaultBranch": ".main_branch",
|
117
|
+
},
|
118
|
+
relations={},
|
119
|
+
)
|
120
|
+
)
|
121
|
+
),
|
122
|
+
)
|
123
|
+
one_webhook_event_raw_results_for_deletion = WebhookEventRawResults(
|
124
|
+
deleted_raw_results=[
|
125
|
+
{
|
126
|
+
"name": "repo-one",
|
127
|
+
"links": {"html": {"href": "https://example.com/repo-one"}},
|
128
|
+
"main_branch": "main",
|
129
|
+
}
|
130
|
+
],
|
131
|
+
updated_raw_results=[],
|
132
|
+
)
|
133
|
+
one_webhook_event_raw_results_for_deletion.resource = ResourceConfig(
|
134
|
+
kind="repository",
|
135
|
+
selector=Selector(query="true"),
|
136
|
+
port=PortResourceConfig(
|
137
|
+
entity=MappingsConfig(
|
138
|
+
mappings=EntityMapping(
|
139
|
+
identifier=".name",
|
140
|
+
title=".name",
|
141
|
+
blueprint='"service"',
|
142
|
+
properties={
|
143
|
+
"url": ".links.html.href",
|
144
|
+
"defaultBranch": ".main_branch",
|
145
|
+
},
|
146
|
+
relations={},
|
147
|
+
)
|
148
|
+
)
|
149
|
+
),
|
150
|
+
)
|
151
|
+
|
152
|
+
|
153
|
+
@pytest.fixture
|
154
|
+
def mock_context(monkeypatch: Any) -> PortOceanContext:
|
155
|
+
mock_context = AsyncMock()
|
156
|
+
monkeypatch.setattr(PortOceanContext, "app", mock_context)
|
157
|
+
return mock_context
|
158
|
+
|
159
|
+
|
160
|
+
@pytest.fixture
|
161
|
+
def mock_entity_processor(mock_context: PortOceanContext) -> JQEntityProcessor:
|
162
|
+
return JQEntityProcessor(mock_context)
|
163
|
+
|
164
|
+
|
165
|
+
@pytest.fixture
|
166
|
+
def mock_entities_state_applier(
|
167
|
+
mock_context: PortOceanContext,
|
168
|
+
) -> HttpEntitiesStateApplier:
|
169
|
+
return HttpEntitiesStateApplier(mock_context)
|
170
|
+
|
171
|
+
|
172
|
+
@pytest.fixture
|
173
|
+
def mock_repository_resource_config() -> ResourceConfig:
|
174
|
+
return ResourceConfig(
|
175
|
+
kind="repository",
|
176
|
+
selector=Selector(query="true"),
|
177
|
+
port=PortResourceConfig(
|
178
|
+
entity=MappingsConfig(
|
179
|
+
mappings=EntityMapping(
|
180
|
+
identifier=".name",
|
181
|
+
title=".name",
|
182
|
+
blueprint='"service"',
|
183
|
+
properties={
|
184
|
+
"url": ".links.html.href",
|
185
|
+
"defaultBranch": ".main_branch",
|
186
|
+
},
|
187
|
+
relations={},
|
188
|
+
)
|
189
|
+
)
|
190
|
+
),
|
191
|
+
)
|
192
|
+
|
193
|
+
|
194
|
+
@pytest.fixture
|
195
|
+
def mock_repository_resource_config_not_passong_selector() -> ResourceConfig:
|
196
|
+
return ResourceConfig(
|
197
|
+
kind="repository",
|
198
|
+
selector=Selector(query="false"),
|
199
|
+
port=PortResourceConfig(
|
200
|
+
entity=MappingsConfig(
|
201
|
+
mappings=EntityMapping(
|
202
|
+
identifier=".name",
|
203
|
+
title=".name",
|
204
|
+
blueprint='"service"',
|
205
|
+
properties={
|
206
|
+
"url": ".links.html.href",
|
207
|
+
"defaultBranch": ".main_branch",
|
208
|
+
},
|
209
|
+
relations={},
|
210
|
+
)
|
211
|
+
)
|
212
|
+
),
|
213
|
+
)
|
214
|
+
|
215
|
+
|
216
|
+
@pytest.fixture
|
217
|
+
def mock_port_app_config_with_repository_resource(
|
218
|
+
mock_repository_resource_config: ResourceConfig,
|
219
|
+
) -> PortAppConfig:
|
220
|
+
return PortAppConfig(
|
221
|
+
enable_merge_entity=True,
|
222
|
+
delete_dependent_entities=True,
|
223
|
+
create_missing_related_entities=False,
|
224
|
+
resources=[mock_repository_resource_config],
|
225
|
+
entity_deletion_threshold=0.5,
|
226
|
+
)
|
227
|
+
|
228
|
+
|
229
|
+
@pytest.fixture
|
230
|
+
def mock_port_app_config_with_repository_resource_not_passing_selector(
|
231
|
+
mock_repository_resource_config_not_passong_selector: ResourceConfig,
|
232
|
+
) -> PortAppConfig:
|
233
|
+
return PortAppConfig(
|
234
|
+
enable_merge_entity=True,
|
235
|
+
delete_dependent_entities=True,
|
236
|
+
create_missing_related_entities=False,
|
237
|
+
resources=[mock_repository_resource_config_not_passong_selector],
|
238
|
+
entity_deletion_threshold=0.5,
|
239
|
+
)
|
240
|
+
|
241
|
+
|
242
|
+
@pytest.fixture
|
243
|
+
def mock_port_app_config_handler(
|
244
|
+
mock_port_app_config_with_repository_resource: PortAppConfig,
|
245
|
+
) -> MagicMock:
|
246
|
+
handler = MagicMock()
|
247
|
+
handler.get_port_app_config = AsyncMock(
|
248
|
+
return_value=mock_port_app_config_with_repository_resource
|
249
|
+
)
|
250
|
+
return handler
|
251
|
+
|
252
|
+
|
253
|
+
@pytest.fixture
|
254
|
+
def mock_port_client(mock_http_client: MagicMock) -> PortClient:
|
255
|
+
mock_port_client = PortClient(
|
256
|
+
MagicMock(), MagicMock(), MagicMock(), MagicMock(), MagicMock(), MagicMock()
|
257
|
+
)
|
258
|
+
mock_port_client.auth = AsyncMock()
|
259
|
+
mock_port_client.auth.headers = AsyncMock(
|
260
|
+
return_value={
|
261
|
+
"Authorization": "test",
|
262
|
+
"User-Agent": "test",
|
263
|
+
}
|
264
|
+
)
|
265
|
+
|
266
|
+
mock_port_client.search_entities = AsyncMock(return_value=[]) # type: ignore
|
267
|
+
mock_port_client.client = mock_http_client
|
268
|
+
return mock_port_client
|
269
|
+
|
270
|
+
|
271
|
+
@pytest.fixture
|
272
|
+
def mock_ocean(mock_port_client: PortClient) -> Ocean:
|
273
|
+
with patch("port_ocean.ocean.Ocean.__init__", return_value=None):
|
274
|
+
ocean_mock = Ocean(
|
275
|
+
MagicMock(), MagicMock(), MagicMock(), MagicMock(), MagicMock()
|
276
|
+
)
|
277
|
+
ocean_mock.config = MagicMock()
|
278
|
+
ocean_mock.config.port = MagicMock()
|
279
|
+
ocean_mock.config.port.port_app_config_cache_ttl = 60
|
280
|
+
ocean_mock.port_client = mock_port_client
|
281
|
+
|
282
|
+
return ocean_mock
|
283
|
+
|
284
|
+
|
285
|
+
@pytest.fixture
|
286
|
+
def mock_live_events_mixin(
|
287
|
+
mock_entity_processor: JQEntityProcessor,
|
288
|
+
mock_entities_state_applier: HttpEntitiesStateApplier,
|
289
|
+
mock_port_app_config_handler: MagicMock,
|
290
|
+
) -> LiveEventsMixin:
|
291
|
+
mixin = LiveEventsMixin()
|
292
|
+
mixin._entity_processor = mock_entity_processor
|
293
|
+
mixin._entities_state_applier = mock_entities_state_applier
|
294
|
+
mixin._port_app_config_handler = mock_port_app_config_handler
|
295
|
+
return mixin
|
296
|
+
|
297
|
+
|
298
|
+
@pytest.fixture
|
299
|
+
def mock_http_client() -> MagicMock:
|
300
|
+
mock_http_client = MagicMock()
|
301
|
+
mock_upserted_entities = []
|
302
|
+
|
303
|
+
async def post(url: str, *args: Any, **kwargs: Any) -> Response:
|
304
|
+
entity = kwargs.get("json", {})
|
305
|
+
if entity.get("properties", {}).get("mock_is_to_fail", {}):
|
306
|
+
return Response(
|
307
|
+
404, headers=MagicMock(), json={"ok": False, "error": "not_found"}
|
308
|
+
)
|
309
|
+
|
310
|
+
mock_upserted_entities.append(
|
311
|
+
f"{entity.get('identifier')}-{entity.get('blueprint')}"
|
312
|
+
)
|
313
|
+
return Response(
|
314
|
+
200,
|
315
|
+
json={
|
316
|
+
"entity": {
|
317
|
+
"identifier": entity.get("identifier"),
|
318
|
+
"blueprint": entity.get("blueprint"),
|
319
|
+
}
|
320
|
+
},
|
321
|
+
)
|
322
|
+
|
323
|
+
mock_http_client.post = AsyncMock(side_effect=post)
|
324
|
+
return mock_http_client
|
325
|
+
|
326
|
+
|
327
|
+
@pytest.mark.asyncio
|
328
|
+
async def test_parse_raw_event_results_to_entities_creation(
|
329
|
+
mock_live_events_mixin: LiveEventsMixin,
|
330
|
+
) -> None:
|
331
|
+
"""Test parsing raw event results for entity creation"""
|
332
|
+
mock_live_events_mixin.entity_processor.parse_items = AsyncMock() # type: ignore
|
333
|
+
|
334
|
+
calculation_result = CalculationResult(
|
335
|
+
entity_selector_diff=EntitySelectorDiff(passed=[entity], failed=[]),
|
336
|
+
errors=[],
|
337
|
+
misonfigured_entity_keys={},
|
338
|
+
)
|
339
|
+
mock_live_events_mixin.entity_processor.parse_items.return_value = (
|
340
|
+
calculation_result
|
341
|
+
)
|
342
|
+
|
343
|
+
entities_to_create, entities_to_delete = (
|
344
|
+
await mock_live_events_mixin._parse_raw_event_results_to_entities(
|
345
|
+
[one_webhook_event_raw_results_for_creation]
|
346
|
+
)
|
347
|
+
)
|
348
|
+
|
349
|
+
assert entities_to_create == [entity]
|
350
|
+
assert entities_to_delete == []
|
351
|
+
mock_live_events_mixin.entity_processor.parse_items.assert_called_once()
|
352
|
+
|
353
|
+
|
354
|
+
@pytest.mark.asyncio
|
355
|
+
async def test_parse_raw_event_results_to_entities_deletion(
|
356
|
+
mock_live_events_mixin: LiveEventsMixin,
|
357
|
+
) -> None:
|
358
|
+
"""Test parsing raw event results for entity deletion"""
|
359
|
+
mock_live_events_mixin.entity_processor.parse_items = AsyncMock() # type: ignore
|
360
|
+
|
361
|
+
calculation_result = CalculationResult(
|
362
|
+
entity_selector_diff=EntitySelectorDiff(passed=[entity], failed=[]),
|
363
|
+
errors=[],
|
364
|
+
misonfigured_entity_keys={},
|
365
|
+
)
|
366
|
+
mock_live_events_mixin.entity_processor.parse_items.return_value = (
|
367
|
+
calculation_result
|
368
|
+
)
|
369
|
+
|
370
|
+
entities_to_create, entities_to_delete = (
|
371
|
+
await mock_live_events_mixin._parse_raw_event_results_to_entities(
|
372
|
+
[one_webhook_event_raw_results_for_deletion]
|
373
|
+
)
|
374
|
+
)
|
375
|
+
|
376
|
+
assert entities_to_create == []
|
377
|
+
assert entities_to_delete == [entity]
|
378
|
+
mock_live_events_mixin.entity_processor.parse_items.assert_called_once()
|
379
|
+
|
380
|
+
|
381
|
+
@pytest.mark.asyncio
|
382
|
+
async def test_sync_raw_results_one_raw_result_entity_upserted(
|
383
|
+
mock_live_events_mixin: LiveEventsMixin,
|
384
|
+
) -> None:
|
385
|
+
"""Test synchronizing raw webhook event results"""
|
386
|
+
# Setup mocks
|
387
|
+
mock_live_events_mixin._parse_raw_event_results_to_entities = AsyncMock(return_value=([entity], [])) # type: ignore
|
388
|
+
mock_live_events_mixin.entities_state_applier.upsert = AsyncMock() # type: ignore
|
389
|
+
mock_live_events_mixin.entities_state_applier.delete = AsyncMock() # type: ignore
|
390
|
+
mock_live_events_mixin._delete_entities = AsyncMock() # type: ignore
|
391
|
+
|
392
|
+
# Call the method
|
393
|
+
await mock_live_events_mixin.sync_raw_results(
|
394
|
+
[one_webhook_event_raw_results_for_creation]
|
395
|
+
)
|
396
|
+
|
397
|
+
# Verify the method calls
|
398
|
+
mock_live_events_mixin._parse_raw_event_results_to_entities.assert_called_once_with(
|
399
|
+
[one_webhook_event_raw_results_for_creation]
|
400
|
+
)
|
401
|
+
mock_live_events_mixin.entities_state_applier.upsert.assert_called_once_with(
|
402
|
+
[entity], UserAgentType.exporter
|
403
|
+
)
|
404
|
+
mock_live_events_mixin.entities_state_applier.delete.assert_not_called()
|
@@ -1,115 +1,106 @@
|
|
1
|
-
from fastapi import APIRouter
|
2
1
|
import pytest
|
3
|
-
|
2
|
+
from port_ocean.core.handlers.port_app_config.models import ResourceConfig
|
4
3
|
from port_ocean.core.handlers.webhook.abstract_webhook_processor import (
|
5
4
|
AbstractWebhookProcessor,
|
6
5
|
)
|
7
|
-
from port_ocean.exceptions.webhook_processor import RetryableError
|
8
6
|
from port_ocean.core.handlers.webhook.webhook_event import (
|
9
7
|
EventHeaders,
|
10
8
|
EventPayload,
|
11
9
|
WebhookEvent,
|
10
|
+
WebhookEventRawResults,
|
12
11
|
)
|
13
|
-
from port_ocean.
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
class
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
self
|
22
|
-
|
23
|
-
|
24
|
-
fail_count: int = 0,
|
25
|
-
max_retries: int = 3,
|
26
|
-
) -> None:
|
27
|
-
super().__init__(event)
|
28
|
-
self.authenticated = False
|
29
|
-
self.validated = False
|
30
|
-
self.handled = False
|
31
|
-
self.should_fail = should_fail
|
32
|
-
self.fail_count = fail_count
|
33
|
-
self.current_fails = 0
|
34
|
-
self.error_handler_called = False
|
35
|
-
self.cancelled = False
|
36
|
-
self.max_retries = max_retries
|
12
|
+
from port_ocean.exceptions.webhook_processor import RetryableError
|
13
|
+
|
14
|
+
|
15
|
+
class ConcreteWebhookProcessor(AbstractWebhookProcessor):
|
16
|
+
"""Concrete implementation for testing the abstract class"""
|
17
|
+
|
18
|
+
def __init__(self, webhook_event: WebhookEvent) -> None:
|
19
|
+
super().__init__(webhook_event)
|
20
|
+
self.before_processing_called = False
|
21
|
+
self.after_processing_called = False
|
22
|
+
self.cancel_called = False
|
37
23
|
|
38
24
|
async def authenticate(self, payload: EventPayload, headers: EventHeaders) -> bool:
|
39
|
-
self.authenticated = True
|
40
25
|
return True
|
41
26
|
|
42
27
|
async def validate_payload(self, payload: EventPayload) -> bool:
|
43
|
-
self.validated = True
|
44
28
|
return True
|
45
29
|
|
46
|
-
async def handle_event(
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
30
|
+
async def handle_event(
|
31
|
+
self, payload: EventPayload, resource: ResourceConfig
|
32
|
+
) -> WebhookEventRawResults:
|
33
|
+
return WebhookEventRawResults(updated_raw_results=[{}], deleted_raw_results=[])
|
34
|
+
|
35
|
+
def should_process_event(self, webhook_event: WebhookEvent) -> bool:
|
36
|
+
return True
|
37
|
+
|
38
|
+
async def before_processing(self) -> None:
|
39
|
+
await super().before_processing()
|
40
|
+
self.before_processing_called = True
|
41
|
+
|
42
|
+
async def after_processing(self) -> None:
|
43
|
+
await super().after_processing()
|
44
|
+
self.after_processing_called = True
|
51
45
|
|
52
46
|
async def cancel(self) -> None:
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
@pytest.
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
)
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
assert processor.retry_count == processor.max_retries
|
114
|
-
assert processor.error_handler_called
|
115
|
-
assert not processor.handled
|
47
|
+
await super().cancel()
|
48
|
+
self.cancel_called = True
|
49
|
+
|
50
|
+
def get_matching_kinds(self, event: WebhookEvent) -> list[str]:
|
51
|
+
return ["test"]
|
52
|
+
|
53
|
+
|
54
|
+
@pytest.fixture
|
55
|
+
def webhook_event() -> WebhookEvent:
|
56
|
+
return WebhookEvent(payload={}, headers={}, trace_id="test-trace-id")
|
57
|
+
|
58
|
+
|
59
|
+
@pytest.fixture
|
60
|
+
def processor(webhook_event: WebhookEvent) -> ConcreteWebhookProcessor:
|
61
|
+
return ConcreteWebhookProcessor(webhook_event)
|
62
|
+
|
63
|
+
|
64
|
+
async def test_init_finishedSuccessfully(webhook_event: WebhookEvent) -> None:
|
65
|
+
processor = ConcreteWebhookProcessor(webhook_event)
|
66
|
+
assert processor.event == webhook_event
|
67
|
+
assert processor.retry_count == 0
|
68
|
+
|
69
|
+
|
70
|
+
@pytest.mark.asyncio
|
71
|
+
async def test_calculateRetryDelay_delayCalculatedCorrectly(
|
72
|
+
processor: ConcreteWebhookProcessor,
|
73
|
+
) -> None:
|
74
|
+
assert processor.calculate_retry_delay() == 1.0
|
75
|
+
|
76
|
+
processor.retry_count = 1
|
77
|
+
assert processor.calculate_retry_delay() == 2.0
|
78
|
+
|
79
|
+
processor.retry_count = 10
|
80
|
+
assert processor.calculate_retry_delay() == 30.0
|
81
|
+
|
82
|
+
|
83
|
+
@pytest.mark.asyncio
|
84
|
+
async def test_shouldRetry_returnsTrueOnRetryableError(
|
85
|
+
processor: ConcreteWebhookProcessor,
|
86
|
+
) -> None:
|
87
|
+
assert processor.should_retry(RetryableError("test")) is True
|
88
|
+
assert processor.should_retry(ValueError("test")) is False
|
89
|
+
|
90
|
+
|
91
|
+
@pytest.mark.asyncio
|
92
|
+
async def test_lifecycleHooks_callsCorrectly(
|
93
|
+
processor: ConcreteWebhookProcessor,
|
94
|
+
) -> None:
|
95
|
+
assert not processor.before_processing_called
|
96
|
+
assert not processor.after_processing_called
|
97
|
+
assert not processor.cancel_called
|
98
|
+
|
99
|
+
await processor.before_processing()
|
100
|
+
assert processor.before_processing_called
|
101
|
+
|
102
|
+
await processor.after_processing()
|
103
|
+
assert processor.after_processing_called
|
104
|
+
|
105
|
+
await processor.cancel()
|
106
|
+
assert processor.cancel_called
|