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.
@@ -1,18 +1,85 @@
1
- import asyncio
2
1
  import pytest
3
- from fastapi import APIRouter
4
- from typing import Dict, Any
5
-
6
- from port_ocean.exceptions.webhook_processor import RetryableError
7
- from port_ocean.core.handlers.webhook.processor_manager import WebhookProcessorManager
2
+ from port_ocean.core.handlers.webhook.processor_manager import (
3
+ LiveEventsProcessorManager,
4
+ )
8
5
  from port_ocean.core.handlers.webhook.abstract_webhook_processor import (
9
6
  AbstractWebhookProcessor,
10
7
  )
11
8
  from port_ocean.core.handlers.webhook.webhook_event import (
9
+ EventHeaders,
12
10
  WebhookEvent,
11
+ WebhookEventRawResults,
12
+ EventPayload,
13
13
  )
14
- from port_ocean.core.handlers.queue import LocalQueue
14
+ from fastapi import APIRouter
15
15
  from port_ocean.utils.signal import SignalHandler
16
+ from typing import Dict, Any
17
+ import asyncio
18
+ from fastapi.testclient import TestClient
19
+ from fastapi import FastAPI
20
+ from port_ocean.context.ocean import PortOceanContext
21
+ from unittest.mock import AsyncMock
22
+ from port_ocean.context.event import event_context, EventType
23
+ from port_ocean.context.ocean import ocean
24
+ from unittest.mock import MagicMock, patch
25
+ from httpx import Response
26
+ from port_ocean.clients.port.client import PortClient
27
+ from port_ocean import Ocean
28
+ from port_ocean.core.integrations.base import BaseIntegration
29
+ from port_ocean.core.handlers.port_app_config.models import (
30
+ EntityMapping,
31
+ MappingsConfig,
32
+ PortAppConfig,
33
+ PortResourceConfig,
34
+ ResourceConfig,
35
+ Selector,
36
+ )
37
+ from port_ocean.core.integrations.mixins.live_events import LiveEventsMixin
38
+ from port_ocean.core.models import Entity
39
+ from port_ocean.exceptions.webhook_processor import RetryableError
40
+ from port_ocean.core.handlers.queue import LocalQueue
41
+
42
+
43
+ class MockProcessor(AbstractWebhookProcessor):
44
+ async def authenticate(
45
+ self, payload: Dict[str, Any], headers: Dict[str, str]
46
+ ) -> bool:
47
+ return True
48
+
49
+ async def validate_payload(self, payload: Dict[str, Any]) -> bool:
50
+ return True
51
+
52
+ async def handle_event(
53
+ self, payload: EventPayload, resource: ResourceConfig
54
+ ) -> WebhookEventRawResults:
55
+ return WebhookEventRawResults(updated_raw_results=[], deleted_raw_results=[])
56
+
57
+ def should_process_event(self, event: WebhookEvent) -> bool:
58
+ return True
59
+
60
+ def get_matching_kinds(self, event: WebhookEvent) -> list[str]:
61
+ return ["repository"]
62
+
63
+
64
+ class MockProcessorFalse(AbstractWebhookProcessor):
65
+ async def authenticate(
66
+ self, payload: Dict[str, Any], headers: Dict[str, str]
67
+ ) -> bool:
68
+ return True
69
+
70
+ async def validate_payload(self, payload: Dict[str, Any]) -> bool:
71
+ return True
72
+
73
+ async def handle_event(
74
+ self, payload: EventPayload, resource: ResourceConfig
75
+ ) -> WebhookEventRawResults:
76
+ return WebhookEventRawResults(updated_raw_results=[], deleted_raw_results=[])
77
+
78
+ def should_process_event(self, event: WebhookEvent) -> bool:
79
+ return False
80
+
81
+ def get_matching_kinds(self, event: WebhookEvent) -> list[str]:
82
+ return ["repository"]
16
83
 
17
84
 
18
85
  class MockWebhookProcessor(AbstractWebhookProcessor):
@@ -32,360 +99,1166 @@ class MockWebhookProcessor(AbstractWebhookProcessor):
32
99
  async def validate_payload(self, payload: Dict[str, Any]) -> bool:
33
100
  return True
34
101
 
35
- async def handle_event(self, payload: Dict[str, Any]) -> None:
102
+ async def handle_event(
103
+ self, payload: EventPayload, resource: ResourceConfig
104
+ ) -> WebhookEventRawResults:
36
105
  if self.error_to_raise:
37
106
  raise self.error_to_raise
38
107
  self.processed = True
108
+ return WebhookEventRawResults(updated_raw_results=[], deleted_raw_results=[])
39
109
 
40
110
  async def cancel(self) -> None:
41
111
  self.cancel_called = True
42
112
 
113
+ def should_process_event(self, event: WebhookEvent) -> bool:
114
+ return True
43
115
 
44
- class RetryableProcessor(MockWebhookProcessor):
45
- def __init__(self, event: WebhookEvent) -> None:
46
- super().__init__(event)
47
- self.attempt_count = 0
48
-
49
- async def handle_event(self, payload: Dict[str, Any]) -> None:
50
- self.attempt_count += 1
51
- if self.attempt_count < 3: # Succeed on third attempt
52
- raise RetryableError("Temporary failure")
53
- self.processed = True
54
-
55
-
56
- class TestableWebhookProcessorManager(WebhookProcessorManager):
57
- __test__ = False
58
-
59
- def __init__(self, *args: Any, **kwargs: Any) -> None:
60
- super().__init__(*args, **kwargs)
61
- self.running_processors: list[AbstractWebhookProcessor] = []
62
- self.no_matching_processors: bool = False
63
-
64
- def _extract_matching_processors(
65
- self, event: WebhookEvent, path: str
66
- ) -> list[AbstractWebhookProcessor]:
67
- try:
68
- return super()._extract_matching_processors(event, path)
69
- except ValueError:
70
- self.no_matching_processors = True
71
- raise
72
-
73
- async def _process_single_event(
74
- self, processor: AbstractWebhookProcessor, path: str
75
- ) -> None:
76
- self.running_processors.append(processor)
77
- await super()._process_single_event(processor, path)
78
-
79
-
80
- class TestWebhookProcessorManager:
81
- @pytest.fixture
82
- def router(self) -> APIRouter:
83
- return APIRouter()
84
-
85
- @pytest.fixture
86
- def signal_handler(self) -> SignalHandler:
87
- return SignalHandler()
88
-
89
- @pytest.fixture
90
- def processor_manager(
91
- self, router: APIRouter, signal_handler: SignalHandler
92
- ) -> TestableWebhookProcessorManager:
93
- return TestableWebhookProcessorManager(router, signal_handler)
94
-
95
- @pytest.fixture
96
- def mock_event(self) -> WebhookEvent:
97
- return WebhookEvent.from_dict(
98
- {
99
- "payload": {"test": "data"},
100
- "headers": {"content-type": "application/json"},
101
- "trace_id": "test-trace",
102
- }
103
- )
116
+ def get_matching_kinds(self, event: WebhookEvent) -> list[str]:
117
+ return ["test"]
104
118
 
105
- @staticmethod
106
- def assert_event_processed_successfully(
107
- processor: MockWebhookProcessor,
108
- ) -> None:
109
- """Assert that a processor's event was processed successfully"""
110
- assert processor.processed, "Event was not processed successfully"
111
119
 
112
- @staticmethod
113
- def assert_event_processed_with_error(processor: MockWebhookProcessor) -> None:
114
- """Assert that an event was processed with an error"""
115
- assert not processor.processed, "Event did not fail as expected"
120
+ class MockWebhookHandlerForProcessWebhookRequest(AbstractWebhookProcessor):
121
+ """Concrete implementation for testing."""
116
122
 
117
- @pytest.mark.skip(reason="Temporarily ignoring this test")
118
- async def test_register_handler(
119
- self, processor_manager: TestableWebhookProcessorManager
120
- ) -> None:
121
- """Test registering a processor for a path."""
122
- processor_manager.register_processor("/test", MockWebhookProcessor)
123
- assert "/test" in processor_manager._processors
124
- assert len(processor_manager._processors["/test"]) == 1
125
- assert isinstance(processor_manager._event_queues["/test"], LocalQueue)
126
-
127
- @pytest.mark.skip(reason="Temporarily ignoring this test")
128
- async def test_register_multiple_handlers_with_filters(
129
- self, processor_manager: TestableWebhookProcessorManager
123
+ def __init__(
124
+ self,
125
+ event: WebhookEvent,
126
+ should_fail: bool = False,
127
+ fail_count: int = 0,
128
+ max_retries: int = 3,
130
129
  ) -> None:
131
- """Test registering multiple processors with different filters."""
130
+ super().__init__(event)
131
+ self.authenticated = False
132
+ self.validated = False
133
+ self.handled = False
134
+ self.should_fail = should_fail
135
+ self.fail_count = fail_count
136
+ self.current_fails = 0
137
+ self.error_handler_called = False
138
+ self.cancelled = False
139
+ self.max_retries = max_retries
140
+
141
+ async def authenticate(self, payload: EventPayload, headers: EventHeaders) -> bool:
142
+ self.authenticated = True
143
+ return True
132
144
 
133
- def filter1(e: WebhookEvent) -> bool:
134
- return e.payload.get("type") == "type1"
145
+ async def validate_payload(self, payload: EventPayload) -> bool:
146
+ self.validated = True
147
+ return True
135
148
 
136
- def filter2(e: WebhookEvent) -> bool:
137
- return e.payload.get("type") == "type2"
149
+ async def handle_event(
150
+ self, payload: EventPayload, resource: ResourceConfig
151
+ ) -> WebhookEventRawResults:
152
+ if self.should_fail and self.current_fails < self.fail_count:
153
+ self.current_fails += 1
154
+ raise RetryableError("Temporary failure")
155
+ self.handled = True
156
+ return WebhookEventRawResults(updated_raw_results=[], deleted_raw_results=[])
138
157
 
139
- processor_manager.register_processor("/test", MockWebhookProcessor, filter1)
140
- processor_manager.register_processor("/test", MockWebhookProcessor, filter2)
158
+ def get_matching_kinds(self, event: WebhookEvent) -> list[str]:
159
+ return ["repository"]
141
160
 
142
- assert len(processor_manager._processors["/test"]) == 2
161
+ def should_process_event(self, event: WebhookEvent) -> bool:
162
+ """Filter the event data before processing."""
163
+ return True
143
164
 
144
- @pytest.mark.skip(reason="Temporarily ignoring this test")
145
- async def test_successful_event_processing(
146
- self,
147
- processor_manager: TestableWebhookProcessorManager,
148
- mock_event: WebhookEvent,
149
- ) -> None:
150
- """Test successful processing of an event."""
151
- processed_events: list[MockWebhookProcessor] = []
165
+ async def cancel(self) -> None:
166
+ self.cancelled = True
167
+
168
+ async def on_error(self, error: Exception) -> None:
169
+ self.error_handler_called = True
170
+ await super().on_error(error)
171
+
172
+
173
+ @pytest.fixture
174
+ def processor_manager() -> LiveEventsProcessorManager:
175
+ router = APIRouter()
176
+ signal_handler = SignalHandler()
177
+ return LiveEventsProcessorManager(
178
+ router, signal_handler, max_event_processing_seconds=3
179
+ )
180
+
181
+
182
+ @pytest.fixture
183
+ def webhook_event() -> WebhookEvent:
184
+ return WebhookEvent(payload={}, headers={}, trace_id="test-trace")
185
+
186
+
187
+ @pytest.fixture
188
+ def webhook_event_for_process_webhook_request() -> WebhookEvent:
189
+ return WebhookEvent(
190
+ trace_id="test-trace",
191
+ payload={"test": "data"},
192
+ headers={"content-type": "application/json"},
193
+ )
194
+
195
+
196
+ @pytest.fixture
197
+ def processor_manager_for_process_webhook_request() -> LiveEventsProcessorManager:
198
+ return LiveEventsProcessorManager(APIRouter(), SignalHandler())
199
+
200
+
201
+ @pytest.fixture
202
+ def processor(
203
+ webhook_event_for_process_webhook_request: WebhookEvent,
204
+ ) -> MockWebhookHandlerForProcessWebhookRequest:
205
+ return MockWebhookHandlerForProcessWebhookRequest(
206
+ webhook_event_for_process_webhook_request
207
+ )
208
+
209
+
210
+ @pytest.fixture
211
+ def mock_port_app_config() -> PortAppConfig:
212
+ return PortAppConfig(
213
+ enable_merge_entity=True,
214
+ delete_dependent_entities=True,
215
+ create_missing_related_entities=False,
216
+ resources=[
217
+ ResourceConfig(
218
+ kind="repository",
219
+ selector=Selector(query="true"),
220
+ port=PortResourceConfig(
221
+ entity=MappingsConfig(
222
+ mappings=EntityMapping(
223
+ identifier=".name",
224
+ title=".name",
225
+ blueprint='"service"',
226
+ properties={
227
+ "url": ".links.html.href",
228
+ "defaultBranch": ".main_branch",
229
+ },
230
+ relations={},
231
+ )
232
+ )
233
+ ),
234
+ )
235
+ ],
236
+ )
237
+
238
+
239
+ @pytest.fixture
240
+ def mock_http_client() -> MagicMock:
241
+ mock_http_client = MagicMock()
242
+ mock_upserted_entities = []
243
+
244
+ async def post(url: str, *args: Any, **kwargs: Any) -> Response:
245
+ entity = kwargs.get("json", {})
246
+ if entity.get("properties", {}).get("mock_is_to_fail", {}):
247
+ return Response(
248
+ 404, headers=MagicMock(), json={"ok": False, "error": "not_found"}
249
+ )
250
+
251
+ mock_upserted_entities.append(
252
+ f"{entity.get('identifier')}-{entity.get('blueprint')}"
253
+ )
254
+ return Response(
255
+ 200,
256
+ json={
257
+ "entity": {
258
+ "identifier": entity.get("identifier"),
259
+ "blueprint": entity.get("blueprint"),
260
+ }
261
+ },
262
+ )
152
263
 
153
- class SuccessProcessor(MockWebhookProcessor):
154
- async def handle_event(self, payload: Dict[str, Any]) -> None:
155
- self.processed = True
156
- processed_events.append(self)
264
+ mock_http_client.post = AsyncMock(side_effect=post)
265
+ return mock_http_client
266
+
267
+
268
+ @pytest.fixture
269
+ def mock_port_client(mock_http_client: MagicMock) -> PortClient:
270
+ mock_port_client = PortClient(
271
+ MagicMock(), MagicMock(), MagicMock(), MagicMock(), MagicMock(), MagicMock()
272
+ )
273
+ mock_port_client.auth = AsyncMock()
274
+ mock_port_client.auth.headers = AsyncMock(
275
+ return_value={
276
+ "Authorization": "test",
277
+ "User-Agent": "test",
278
+ }
279
+ )
280
+
281
+ mock_port_client.search_entities = AsyncMock(return_value=[]) # type: ignore
282
+ mock_port_client.client = mock_http_client
283
+ return mock_port_client
284
+
285
+
286
+ @pytest.fixture
287
+ def mock_ocean(mock_port_client: PortClient) -> Ocean:
288
+ with patch("port_ocean.ocean.Ocean.__init__", return_value=None):
289
+ ocean_mock = Ocean(
290
+ MagicMock(), MagicMock(), MagicMock(), MagicMock(), MagicMock()
291
+ )
292
+ ocean_mock.config = MagicMock()
293
+ ocean_mock.config.port = MagicMock()
294
+ ocean_mock.config.port.port_app_config_cache_ttl = 60
295
+ ocean_mock.port_client = mock_port_client
296
+ ocean_mock.integration_router = APIRouter()
297
+ ocean_mock.fast_api_app = FastAPI()
298
+ return ocean_mock
299
+
300
+
301
+ @pytest.fixture
302
+ def mock_context(mock_ocean: Ocean) -> PortOceanContext:
303
+ context = PortOceanContext(mock_ocean)
304
+ ocean._app = context.app
305
+ return context
306
+
307
+
308
+ entity = Entity(
309
+ identifier="repo-one",
310
+ blueprint="service",
311
+ title="repo-one",
312
+ team=[],
313
+ properties={
314
+ "url": "https://example.com/repo-one",
315
+ "defaultBranch": "main",
316
+ },
317
+ relations={},
318
+ )
157
319
 
158
- processor_manager.register_processor("/test", SuccessProcessor)
159
320
 
160
- await processor_manager.start_processing_event_messages()
161
- await processor_manager._event_queues["/test"].put(mock_event)
321
+ @pytest.mark.asyncio
322
+ async def test_extractMatchingProcessors_processorMatch(
323
+ processor_manager: LiveEventsProcessorManager,
324
+ webhook_event: WebhookEvent,
325
+ mock_port_app_config: PortAppConfig,
326
+ ) -> None:
327
+ test_path = "/test"
328
+ processor_manager.register_processor(test_path, MockProcessor)
162
329
 
163
- # Allow time for processing
164
- await asyncio.sleep(0.1)
330
+ async with event_context(EventType.HTTP_REQUEST, trigger_type="request") as event:
331
+ event.port_app_config = mock_port_app_config
332
+ processors = processor_manager._extract_matching_processors(
333
+ webhook_event, test_path
334
+ )
165
335
 
166
- # Verify at least one processor ran and completed successfully
167
- assert len(processed_events) > 0
168
- for processor in processed_events:
169
- self.assert_event_processed_successfully(processor)
336
+ assert len(processors) == 1
337
+ config, processor = processors[0]
338
+ assert isinstance(processor, MockProcessor)
339
+ assert config.kind == "repository"
340
+ assert processor.event != webhook_event
341
+ assert processor.event.payload == webhook_event.payload
342
+
343
+
344
+ @pytest.mark.asyncio
345
+ async def test_extractMatchingProcessors_noMatch(
346
+ processor_manager: LiveEventsProcessorManager,
347
+ webhook_event: WebhookEvent,
348
+ mock_port_app_config: PortAppConfig,
349
+ ) -> None:
350
+ test_path = "/test"
351
+ processor_manager.register_processor(test_path, MockProcessorFalse)
352
+
353
+ with pytest.raises(ValueError, match="No matching processors found"):
354
+ async with event_context(
355
+ EventType.HTTP_REQUEST, trigger_type="request"
356
+ ) as event:
357
+ event.port_app_config = mock_port_app_config
358
+ processor_manager._extract_matching_processors(webhook_event, test_path)
359
+
360
+
361
+ @pytest.mark.asyncio
362
+ async def test_extractMatchingProcessors_multipleMatches(
363
+ processor_manager: LiveEventsProcessorManager,
364
+ webhook_event: WebhookEvent,
365
+ mock_port_app_config: PortAppConfig,
366
+ ) -> None:
367
+ test_path = "/test"
368
+ processor_manager.register_processor(test_path, MockProcessor)
369
+ processor_manager.register_processor(test_path, MockProcessor)
370
+
371
+ async with event_context(EventType.HTTP_REQUEST, trigger_type="request") as event:
372
+ event.port_app_config = mock_port_app_config
373
+ processors = processor_manager._extract_matching_processors(
374
+ webhook_event, test_path
375
+ )
170
376
 
171
- @pytest.mark.skip(reason="Temporarily ignoring this test")
172
- async def test_graceful_shutdown(
173
- self,
174
- processor_manager: TestableWebhookProcessorManager,
175
- mock_event: WebhookEvent,
176
- ) -> None:
177
- """Test graceful shutdown with in-flight requests"""
178
- processor_manager.register_processor("/test", MockWebhookProcessor)
377
+ assert len(processors) == 2
378
+ assert all(isinstance(p, MockProcessor) for _, p in processors)
379
+ assert all(p.event != webhook_event for _, p in processors)
380
+
381
+
382
+ @pytest.mark.asyncio
383
+ async def test_extractMatchingProcessors_onlyOneMatches(
384
+ processor_manager: LiveEventsProcessorManager,
385
+ webhook_event: WebhookEvent,
386
+ mock_port_app_config: PortAppConfig,
387
+ ) -> None:
388
+ test_path = "/test"
389
+ processor_manager.register_processor(test_path, MockProcessor)
390
+ processor_manager.register_processor(test_path, MockProcessorFalse)
391
+
392
+ async with event_context(EventType.HTTP_REQUEST, trigger_type="request") as event:
393
+ event.port_app_config = mock_port_app_config
394
+ processors = processor_manager._extract_matching_processors(
395
+ webhook_event, test_path
396
+ )
179
397
 
180
- await processor_manager.start_processing_event_messages()
181
- await processor_manager._event_queues["/test"].put(mock_event)
398
+ assert len(processors) == 1
399
+ config, processor = processors[0]
400
+ assert isinstance(processor, MockProcessor)
401
+ assert config.kind == "repository"
402
+ assert processor.event != webhook_event
403
+ assert processor.event.payload == webhook_event.payload
404
+
405
+
406
+ def test_registerProcessor_registrationWorks(
407
+ processor_manager: LiveEventsProcessorManager,
408
+ ) -> None:
409
+ processor_manager.register_processor("/test", MockWebhookProcessor)
410
+ assert "/test" in processor_manager._processors_classes
411
+ assert len(processor_manager._processors_classes["/test"]) == 1
412
+ assert isinstance(processor_manager._event_queues["/test"], LocalQueue)
413
+
414
+
415
+ def test_registerProcessor_multipleHandlers_allRegistered(
416
+ processor_manager: LiveEventsProcessorManager,
417
+ ) -> None:
418
+ processor_manager.register_processor("/test", MockWebhookProcessor)
419
+ processor_manager.register_processor("/test", MockWebhookProcessor)
420
+
421
+ assert len(processor_manager._processors_classes["/test"]) == 2
422
+
423
+
424
+ def test_registerProcessor_invalidHandlerRegistration_throwsError(
425
+ processor_manager: LiveEventsProcessorManager,
426
+ ) -> None:
427
+ """Test registration of invalid processor type."""
428
+
429
+ with pytest.raises(ValueError):
430
+ processor_manager.register_processor("/test", object) # type: ignore
431
+
432
+
433
+ @pytest.mark.asyncio
434
+ async def test_processWebhookRequest_successfulProcessing(
435
+ processor: MockWebhookHandlerForProcessWebhookRequest,
436
+ processor_manager_for_process_webhook_request: LiveEventsProcessorManager,
437
+ mock_port_app_config: PortAppConfig,
438
+ ) -> None:
439
+ """Test successful webhook processing flow."""
440
+ await processor_manager_for_process_webhook_request._process_webhook_request(
441
+ processor, mock_port_app_config.resources[0]
442
+ )
443
+
444
+ assert processor.authenticated
445
+ assert processor.validated
446
+ assert processor.handled
447
+ assert not processor.error_handler_called
448
+
449
+
450
+ @pytest.mark.asyncio
451
+ async def test_processWebhookRequest_retryTwoTimesThenSuccessfulProcessing(
452
+ webhook_event_for_process_webhook_request: WebhookEvent,
453
+ processor_manager_for_process_webhook_request: LiveEventsProcessorManager,
454
+ mock_port_app_config: PortAppConfig,
455
+ ) -> None:
456
+ """Test retry mechanism with temporary failures."""
457
+ processor = MockWebhookHandlerForProcessWebhookRequest(
458
+ webhook_event_for_process_webhook_request, should_fail=True, fail_count=2
459
+ )
460
+
461
+ await processor_manager_for_process_webhook_request._process_webhook_request(
462
+ processor, mock_port_app_config.resources[0]
463
+ )
464
+
465
+ assert processor.handled
466
+ assert processor.current_fails == 2
467
+ assert processor.retry_count == 2
468
+ assert processor.error_handler_called
469
+
470
+
471
+ @pytest.mark.asyncio
472
+ async def test_processWebhookRequest_maxRetriesExceeded_exceptionRaised(
473
+ webhook_event: WebhookEvent,
474
+ processor_manager_for_process_webhook_request: LiveEventsProcessorManager,
475
+ mock_port_app_config: PortAppConfig,
476
+ ) -> None:
477
+ """Test behavior when max retries are exceeded."""
478
+ processor = MockWebhookHandlerForProcessWebhookRequest(
479
+ webhook_event, should_fail=True, fail_count=2, max_retries=1
480
+ )
481
+
482
+ with pytest.raises(RetryableError):
483
+ await processor_manager_for_process_webhook_request._process_webhook_request(
484
+ processor, mock_port_app_config.resources[0]
485
+ )
182
486
 
183
- # Start shutdown
184
- await processor_manager.shutdown()
487
+ assert processor.retry_count == processor.max_retries
488
+ assert processor.error_handler_called
489
+ assert not processor.handled
185
490
 
186
- # Verify all tasks are cleaned up
187
- assert len(processor_manager._webhook_processor_tasks) == 0
188
- self.assert_event_processed_successfully(
189
- processor_manager.running_processors[0] # type: ignore
190
- )
191
491
 
192
- @pytest.mark.skip(reason="Temporarily ignoring this test")
193
- async def test_handler_filter_matching(
194
- self, processor_manager: TestableWebhookProcessorManager
492
+ @pytest.mark.asyncio
493
+ @patch(
494
+ "port_ocean.core.handlers.entities_state_applier.port.applier.HttpEntitiesStateApplier.upsert"
495
+ )
496
+ @patch(
497
+ "port_ocean.core.handlers.entities_state_applier.port.applier.HttpEntitiesStateApplier.delete"
498
+ )
499
+ async def test_integrationTest_postRequestSent_webhookEventRawResultProcessed_entityUpserted(
500
+ mock_delete: AsyncMock,
501
+ mock_upsert: AsyncMock,
502
+ mock_context: PortOceanContext,
503
+ mock_port_app_config: PortAppConfig,
504
+ monkeypatch: pytest.MonkeyPatch,
505
+ ) -> None:
506
+ """Integration test for the complete webhook processing flow"""
507
+
508
+ monkeypatch.setattr(
509
+ "port_ocean.core.integrations.mixins.handler.ocean", mock_context
510
+ )
511
+ monkeypatch.setattr(
512
+ "port_ocean.core.integrations.mixins.live_events.ocean", mock_context
513
+ )
514
+ processed_events: list[WebhookEventRawResults] = []
515
+ mock_upsert.return_value = [entity]
516
+
517
+ class TestProcessor(AbstractWebhookProcessor):
518
+ def __init__(self, event: WebhookEvent) -> None:
519
+ super().__init__(event)
520
+
521
+ async def authenticate(
522
+ self, payload: Dict[str, Any], headers: Dict[str, str]
523
+ ) -> bool:
524
+ return True
525
+
526
+ async def validate_payload(self, payload: Dict[str, Any]) -> bool:
527
+ return True
528
+
529
+ async def handle_event(
530
+ self, payload: EventPayload, resource: ResourceConfig
531
+ ) -> WebhookEventRawResults:
532
+ event_data = WebhookEventRawResults(
533
+ updated_raw_results=[
534
+ {
535
+ "name": "repo-one",
536
+ "links": {"html": {"href": "https://example.com/repo-one"}},
537
+ "main_branch": "main",
538
+ }
539
+ ],
540
+ deleted_raw_results=[],
541
+ )
542
+ processed_events.append(event_data)
543
+ return event_data
544
+
545
+ def should_process_event(self, event: WebhookEvent) -> bool:
546
+ return True
547
+
548
+ def get_matching_kinds(self, event: WebhookEvent) -> list[str]:
549
+ return ["repository"]
550
+
551
+ processing_complete = asyncio.Event()
552
+ original_process_data = LiveEventsMixin.sync_raw_results
553
+
554
+ async def patched_export_single_resource(
555
+ self: LiveEventsMixin, webhookEventRawResults: list[WebhookEventRawResults]
195
556
  ) -> None:
196
- """Test that processors are selected based on their filters."""
197
- type1_event = WebhookEvent.from_dict(
198
- {"payload": {"type": "type1"}, "headers": {}, "trace_id": "test-trace-1"}
557
+ try:
558
+ await original_process_data(self, webhookEventRawResults)
559
+ except Exception as e:
560
+ raise e
561
+ finally:
562
+ processing_complete.set()
563
+
564
+ monkeypatch.setattr(
565
+ LiveEventsMixin,
566
+ "sync_raw_results",
567
+ patched_export_single_resource,
568
+ )
569
+ test_path = "/webhook-test"
570
+ mock_context.app.integration = BaseIntegration(ocean)
571
+ mock_context.app.webhook_manager = LiveEventsProcessorManager(
572
+ mock_context.app.integration_router, SignalHandler()
573
+ )
574
+
575
+ mock_context.app.webhook_manager.register_processor(test_path, TestProcessor)
576
+ await mock_context.app.webhook_manager.start_processing_event_messages()
577
+ mock_context.app.fast_api_app.include_router(
578
+ mock_context.app.webhook_manager._router
579
+ )
580
+ client = TestClient(mock_context.app.fast_api_app)
581
+
582
+ test_payload = {"test": "data"}
583
+
584
+ async with event_context(EventType.HTTP_REQUEST, trigger_type="request") as event:
585
+ mock_context.app.webhook_manager.port_app_config_handler.get_port_app_config = AsyncMock(return_value=mock_port_app_config) # type: ignore
586
+ event.port_app_config = (
587
+ await mock_context.app.webhook_manager.port_app_config_handler.get_port_app_config()
199
588
  )
200
589
 
201
- type2_event = WebhookEvent.from_dict(
202
- {"payload": {"type": "type2"}, "headers": {}, "trace_id": "test-trace-2"}
590
+ response = client.post(
591
+ test_path, json=test_payload, headers={"Content-Type": "application/json"}
203
592
  )
204
593
 
205
- def filter1(e: WebhookEvent) -> bool:
206
- return e.payload.get("type") == "type1"
594
+ assert response.status_code == 200
595
+ assert response.json() == {"status": "ok"}
207
596
 
208
- def filter2(e: WebhookEvent) -> bool:
209
- return e.payload.get("type") == "type2"
597
+ try:
598
+ await asyncio.wait_for(processing_complete.wait(), timeout=10.0)
599
+ except asyncio.TimeoutError:
600
+ pytest.fail("Event processing timed out")
210
601
 
211
- processor_manager.register_processor("/test", MockWebhookProcessor, filter1)
212
- processor_manager.register_processor("/test", MockWebhookProcessor, filter2)
602
+ assert len(processed_events) == 1
213
603
 
214
- await processor_manager.start_processing_event_messages()
604
+ mock_upsert.assert_called_once()
605
+ mock_delete.assert_not_called()
215
606
 
216
- # Process both events
217
- await processor_manager._event_queues["/test"].put(type1_event)
218
- await processor_manager._event_queues["/test"].put(type2_event)
607
+ await mock_context.app.webhook_manager.shutdown()
219
608
 
220
- await asyncio.sleep(0.1)
221
609
 
222
- # Verify both events were processed
223
- self.assert_event_processed_successfully(
224
- processor_manager.running_processors[0] # type: ignore
225
- )
226
- self.assert_event_processed_successfully(
227
- processor_manager.running_processors[1] # type: ignore
610
+ @pytest.mark.asyncio
611
+ @patch(
612
+ "port_ocean.core.handlers.entities_state_applier.port.applier.HttpEntitiesStateApplier.upsert"
613
+ )
614
+ @patch(
615
+ "port_ocean.core.handlers.entities_state_applier.port.applier.HttpEntitiesStateApplier.delete"
616
+ )
617
+ async def test_integrationTest_postRequestSent_reachedTimeout_entityNotUpserted(
618
+ mock_delete: AsyncMock,
619
+ mock_upsert: AsyncMock,
620
+ mock_context: PortOceanContext,
621
+ mock_port_app_config: PortAppConfig,
622
+ monkeypatch: pytest.MonkeyPatch,
623
+ ) -> None:
624
+ """Integration test for the complete webhook processing flow"""
625
+
626
+ monkeypatch.setattr(
627
+ "port_ocean.core.integrations.mixins.handler.ocean", mock_context
628
+ )
629
+ monkeypatch.setattr(
630
+ "port_ocean.core.integrations.mixins.live_events.ocean", mock_context
631
+ )
632
+ mock_upsert.return_value = [entity]
633
+ test_state = {"exception_thrown": None}
634
+
635
+ class TestProcessor(AbstractWebhookProcessor):
636
+ async def authenticate(
637
+ self, payload: Dict[str, Any], headers: Dict[str, str]
638
+ ) -> bool:
639
+ return True
640
+
641
+ async def validate_payload(self, payload: Dict[str, Any]) -> bool:
642
+ return True
643
+
644
+ async def handle_event(
645
+ self, payload: EventPayload, resource: ResourceConfig
646
+ ) -> WebhookEventRawResults:
647
+ await asyncio.sleep(3)
648
+ return WebhookEventRawResults(
649
+ updated_raw_results=[], deleted_raw_results=[]
650
+ )
651
+
652
+ def should_process_event(self, event: WebhookEvent) -> bool:
653
+ return True
654
+
655
+ def get_matching_kinds(self, event: WebhookEvent) -> list[str]:
656
+ return ["repository"]
657
+
658
+ processing_complete = asyncio.Event()
659
+ original_process_data = LiveEventsProcessorManager._process_single_event
660
+
661
+ async def patched_process_single_event(
662
+ self: LiveEventsProcessorManager,
663
+ processor: AbstractWebhookProcessor,
664
+ path: str,
665
+ resource: ResourceConfig,
666
+ ) -> WebhookEventRawResults:
667
+ try:
668
+ return await original_process_data(self, processor, path, resource)
669
+ except Exception as e:
670
+ test_state["exception_thrown"] = e # type: ignore
671
+ raise e
672
+ finally:
673
+ processing_complete.set()
674
+
675
+ monkeypatch.setattr(
676
+ LiveEventsProcessorManager,
677
+ "_process_single_event",
678
+ patched_process_single_event,
679
+ )
680
+ test_path = "/webhook-test"
681
+ mock_context.app.integration = BaseIntegration(ocean)
682
+ mock_context.app.webhook_manager = LiveEventsProcessorManager(
683
+ mock_context.app.integration_router, SignalHandler(), 2
684
+ )
685
+
686
+ mock_context.app.webhook_manager.register_processor(test_path, TestProcessor)
687
+ await mock_context.app.webhook_manager.start_processing_event_messages()
688
+ mock_context.app.fast_api_app.include_router(
689
+ mock_context.app.webhook_manager._router
690
+ )
691
+ client = TestClient(mock_context.app.fast_api_app)
692
+
693
+ test_payload = {"test": "data"}
694
+
695
+ async with event_context(EventType.HTTP_REQUEST, trigger_type="request") as event:
696
+ mock_context.app.webhook_manager.port_app_config_handler.get_port_app_config = AsyncMock(return_value=mock_port_app_config) # type: ignore
697
+ event.port_app_config = (
698
+ await mock_context.app.webhook_manager.port_app_config_handler.get_port_app_config()
228
699
  )
229
700
 
230
- @pytest.mark.skip(reason="Temporarily ignoring this test")
231
- async def test_handler_timeout(
232
- self, router: APIRouter, signal_handler: SignalHandler, mock_event: WebhookEvent
233
- ) -> None:
234
- """Test processor timeout behavior."""
235
-
236
- # Set a short timeout for testing
237
- processor_manager = TestableWebhookProcessorManager(
238
- router, signal_handler, max_event_processing_seconds=0.1
701
+ response = client.post(
702
+ test_path, json=test_payload, headers={"Content-Type": "application/json"}
239
703
  )
240
704
 
241
- class TimeoutHandler(MockWebhookProcessor):
242
- async def handle_event(self, payload: Dict[str, Any]) -> None:
243
- await asyncio.sleep(2) # Longer than max_handler_processing_seconds
705
+ assert response.status_code == 200
706
+ assert response.json() == {"status": "ok"}
244
707
 
245
- processor_manager.register_processor("/test", TimeoutHandler)
246
- await processor_manager.start_processing_event_messages()
247
- await processor_manager._event_queues["/test"].put(mock_event)
708
+ try:
709
+ await asyncio.wait_for(processing_complete.wait(), timeout=100.0)
710
+ except asyncio.TimeoutError:
711
+ pytest.fail("Event processing timed out")
248
712
 
249
- # Wait long enough for the timeout to occur
250
- await asyncio.sleep(0.2)
713
+ assert isinstance(test_state["exception_thrown"], asyncio.TimeoutError) is True
714
+ mock_upsert.assert_not_called()
715
+ mock_delete.assert_not_called()
251
716
 
252
- self.assert_event_processed_with_error(
253
- processor_manager.running_processors[0] # type: ignore
254
- )
717
+ await mock_context.app.webhook_manager.shutdown()
255
718
 
256
- @pytest.mark.skip(reason="Temporarily ignoring this test")
257
- async def test_handler_cancellation(
258
- self,
259
- processor_manager: TestableWebhookProcessorManager,
260
- mock_event: WebhookEvent,
261
- ) -> None:
262
- """Test processor cancellation during shutdown."""
263
- cancelled_events: list[WebhookEvent] = []
264
719
 
265
- class CanceledHandler(MockWebhookProcessor):
266
- async def handle_event(self, payload: Dict[str, Any]) -> None:
267
- await asyncio.sleep(0.2)
720
+ @pytest.mark.asyncio
721
+ @patch(
722
+ "port_ocean.core.handlers.entities_state_applier.port.applier.HttpEntitiesStateApplier.upsert"
723
+ )
724
+ @patch(
725
+ "port_ocean.core.handlers.entities_state_applier.port.applier.HttpEntitiesStateApplier.delete"
726
+ )
727
+ async def test_integrationTest_postRequestSent_noMatchingHandlers_entityNotUpserted(
728
+ mock_delete: AsyncMock,
729
+ mock_upsert: AsyncMock,
730
+ mock_context: PortOceanContext,
731
+ mock_port_app_config: PortAppConfig,
732
+ monkeypatch: pytest.MonkeyPatch,
733
+ ) -> None:
734
+ """Integration test for the complete webhook processing flow"""
735
+
736
+ monkeypatch.setattr(
737
+ "port_ocean.core.integrations.mixins.handler.ocean", mock_context
738
+ )
739
+ monkeypatch.setattr(
740
+ "port_ocean.core.integrations.mixins.live_events.ocean", mock_context
741
+ )
742
+ test_state = {"exception_thrown": None}
743
+ mock_upsert.return_value = [entity]
744
+
745
+ class TestProcessor(AbstractWebhookProcessor):
746
+ async def authenticate(
747
+ self, payload: Dict[str, Any], headers: Dict[str, str]
748
+ ) -> bool:
749
+ return True
750
+
751
+ async def validate_payload(self, payload: Dict[str, Any]) -> bool:
752
+ return True
753
+
754
+ async def handle_event(
755
+ self, payload: EventPayload, resource: ResourceConfig
756
+ ) -> WebhookEventRawResults:
757
+ event_data = WebhookEventRawResults(
758
+ updated_raw_results=[
759
+ {
760
+ "name": "repo-one",
761
+ "links": {"html": {"href": "https://example.com/repo-one"}},
762
+ "main_branch": "main",
763
+ }
764
+ ],
765
+ deleted_raw_results=[],
766
+ )
767
+ return event_data
768
+
769
+ def should_process_event(self, event: WebhookEvent) -> bool:
770
+ return False
771
+
772
+ def get_matching_kinds(self, event: WebhookEvent) -> list[str]:
773
+ return ["repository"]
774
+
775
+ processing_complete = asyncio.Event()
776
+ original_process_data = LiveEventsProcessorManager._extract_matching_processors
777
+
778
+ def patched_extract_matching_processors(
779
+ self: LiveEventsProcessorManager, event: WebhookEvent, path: str
780
+ ) -> list[tuple[ResourceConfig, AbstractWebhookProcessor]]:
781
+ try:
782
+ return original_process_data(self, event, path)
783
+ except Exception as e:
784
+ test_state["exception_thrown"] = e # type: ignore
785
+ return []
786
+ finally:
787
+ processing_complete.set()
788
+
789
+ monkeypatch.setattr(
790
+ LiveEventsProcessorManager,
791
+ "_extract_matching_processors",
792
+ patched_extract_matching_processors,
793
+ )
794
+ test_path = "/webhook-test"
795
+ mock_context.app.integration = BaseIntegration(ocean)
796
+ mock_context.app.webhook_manager = LiveEventsProcessorManager(
797
+ mock_context.app.integration_router, SignalHandler()
798
+ )
799
+
800
+ mock_context.app.webhook_manager.register_processor(test_path, TestProcessor)
801
+ await mock_context.app.webhook_manager.start_processing_event_messages()
802
+ mock_context.app.fast_api_app.include_router(
803
+ mock_context.app.webhook_manager._router
804
+ )
805
+ client = TestClient(mock_context.app.fast_api_app)
806
+
807
+ test_payload = {"test": "data"}
808
+
809
+ async with event_context(EventType.HTTP_REQUEST, trigger_type="request") as event:
810
+ mock_context.app.webhook_manager.port_app_config_handler.get_port_app_config = AsyncMock(return_value=mock_port_app_config) # type: ignore
811
+ event.port_app_config = (
812
+ await mock_context.app.webhook_manager.port_app_config_handler.get_port_app_config()
813
+ )
268
814
 
269
- async def cancel(self) -> None:
270
- cancelled_events.append(self.event)
271
- self.event.payload["canceled"] = True
815
+ response = client.post(
816
+ test_path, json=test_payload, headers={"Content-Type": "application/json"}
817
+ )
272
818
 
273
- processor_manager.register_processor("/test", CanceledHandler)
274
- await processor_manager.start_processing_event_messages()
275
- await processor_manager._event_queues["/test"].put(mock_event)
819
+ assert response.status_code == 200
820
+ assert response.json() == {"status": "ok"}
276
821
 
277
- await asyncio.sleep(0.1)
822
+ try:
823
+ await asyncio.wait_for(processing_complete.wait(), timeout=10.0)
824
+ except asyncio.TimeoutError:
825
+ pytest.fail("Event processing timed out")
278
826
 
279
- # Wait for the event to be processed
280
- await processor_manager._cancel_all_tasks()
827
+ assert isinstance(test_state["exception_thrown"], ValueError) is True
281
828
 
282
- # Verify at least one event was cancelled
283
- assert len(cancelled_events) > 0
284
- assert any(event.payload.get("canceled") for event in cancelled_events)
829
+ mock_upsert.assert_not_called()
830
+ mock_delete.assert_not_called()
285
831
 
286
- @pytest.mark.skip(reason="Temporarily ignoring this test")
287
- async def test_invalid_handler_registration(self) -> None:
288
- """Test registration of invalid processor type."""
289
- handler_manager = WebhookProcessorManager(APIRouter(), SignalHandler())
832
+ await mock_context.app.webhook_manager.shutdown()
290
833
 
291
- with pytest.raises(ValueError):
292
- handler_manager.register_processor("/test", object) # type: ignore
293
834
 
294
- async def test_no_matching_handlers(
295
- self,
296
- processor_manager: TestableWebhookProcessorManager,
297
- mock_event: WebhookEvent,
835
+ @pytest.mark.asyncio
836
+ @patch(
837
+ "port_ocean.core.handlers.entities_state_applier.port.applier.HttpEntitiesStateApplier.upsert"
838
+ )
839
+ @patch(
840
+ "port_ocean.core.handlers.entities_state_applier.port.applier.HttpEntitiesStateApplier.delete"
841
+ )
842
+ async def test_integrationTest_postRequestSent_webhookEventRawResultProcessedForMultipleProcessors_entitiesUpserted(
843
+ mock_delete: AsyncMock,
844
+ mock_upsert: AsyncMock,
845
+ mock_context: PortOceanContext,
846
+ mock_port_app_config: PortAppConfig,
847
+ monkeypatch: pytest.MonkeyPatch,
848
+ ) -> None:
849
+ """Integration test for the complete webhook processing flow"""
850
+
851
+ monkeypatch.setattr(
852
+ "port_ocean.core.integrations.mixins.handler.ocean", mock_context
853
+ )
854
+ monkeypatch.setattr(
855
+ "port_ocean.core.integrations.mixins.live_events.ocean", mock_context
856
+ )
857
+ processed_events: list[WebhookEventRawResults] = []
858
+ mock_upsert.return_value = [entity]
859
+
860
+ class TestProcessorA(AbstractWebhookProcessor):
861
+ async def authenticate(
862
+ self, payload: Dict[str, Any], headers: Dict[str, str]
863
+ ) -> bool:
864
+ return True
865
+
866
+ async def validate_payload(self, payload: Dict[str, Any]) -> bool:
867
+ return True
868
+
869
+ async def handle_event(
870
+ self, payload: EventPayload, resource: ResourceConfig
871
+ ) -> WebhookEventRawResults:
872
+ event_data = WebhookEventRawResults(
873
+ updated_raw_results=[
874
+ {
875
+ "name": "repo-one",
876
+ "links": {"html": {"href": "https://example.com/repo-one"}},
877
+ "main_branch": "main",
878
+ }
879
+ ],
880
+ deleted_raw_results=[],
881
+ )
882
+ processed_events.append(event_data)
883
+ return event_data
884
+
885
+ def should_process_event(self, event: WebhookEvent) -> bool:
886
+ return True
887
+
888
+ def get_matching_kinds(self, event: WebhookEvent) -> list[str]:
889
+ return ["repository"]
890
+
891
+ class TestProcessorB(AbstractWebhookProcessor):
892
+ async def authenticate(
893
+ self, payload: Dict[str, Any], headers: Dict[str, str]
894
+ ) -> bool:
895
+ return True
896
+
897
+ async def validate_payload(self, payload: Dict[str, Any]) -> bool:
898
+ return True
899
+
900
+ async def handle_event(
901
+ self, payload: EventPayload, resource: ResourceConfig
902
+ ) -> WebhookEventRawResults:
903
+ event_data = WebhookEventRawResults(
904
+ updated_raw_results=[
905
+ {
906
+ "name": "repo-two",
907
+ "links": {"html": {"href": "https://example.com/repo-two"}},
908
+ "main_branch": "main",
909
+ }
910
+ ],
911
+ deleted_raw_results=[],
912
+ )
913
+ processed_events.append(event_data)
914
+ return event_data
915
+
916
+ def should_process_event(self, event: WebhookEvent) -> bool:
917
+ return True
918
+
919
+ def get_matching_kinds(self, event: WebhookEvent) -> list[str]:
920
+ return ["repository"]
921
+
922
+ class TestProcessorFiltersOut(AbstractWebhookProcessor):
923
+ async def authenticate(
924
+ self, payload: Dict[str, Any], headers: Dict[str, str]
925
+ ) -> bool:
926
+ return True
927
+
928
+ async def validate_payload(self, payload: Dict[str, Any]) -> bool:
929
+ return True
930
+
931
+ async def handle_event(
932
+ self, payload: EventPayload, resource: ResourceConfig
933
+ ) -> WebhookEventRawResults:
934
+ event_data = WebhookEventRawResults(
935
+ updated_raw_results=[
936
+ {
937
+ "name": "repo-one",
938
+ "links": {"html": {"href": "https://example.com/repo-one"}},
939
+ "main_branch": "main",
940
+ }
941
+ ],
942
+ deleted_raw_results=[],
943
+ )
944
+ processed_events.append(event_data)
945
+ return event_data
946
+
947
+ def should_process_event(self, event: WebhookEvent) -> bool:
948
+ return False
949
+
950
+ def get_matching_kinds(self, event: WebhookEvent) -> list[str]:
951
+ return ["repository"]
952
+
953
+ processing_complete = asyncio.Event()
954
+ original_process_data = LiveEventsMixin.sync_raw_results
955
+
956
+ async def patched_export_single_resource(
957
+ self: LiveEventsMixin, webhookEventRawResults: list[WebhookEventRawResults]
298
958
  ) -> None:
299
- """Test behavior when no processors match the event."""
300
- processor_manager.register_processor(
301
- "/test", MockWebhookProcessor, lambda e: False
959
+ try:
960
+ await original_process_data(self, webhookEventRawResults)
961
+ except Exception as e:
962
+ raise e
963
+ finally:
964
+ processing_complete.set()
965
+
966
+ monkeypatch.setattr(
967
+ LiveEventsMixin,
968
+ "sync_raw_results",
969
+ patched_export_single_resource,
970
+ )
971
+ test_path = "/webhook-test"
972
+ mock_context.app.integration = BaseIntegration(ocean)
973
+ mock_context.app.webhook_manager = LiveEventsProcessorManager(
974
+ mock_context.app.integration_router, SignalHandler()
975
+ )
976
+
977
+ mock_context.app.webhook_manager.register_processor(test_path, TestProcessorA)
978
+ mock_context.app.webhook_manager.register_processor(test_path, TestProcessorB)
979
+ mock_context.app.webhook_manager.register_processor(
980
+ test_path, TestProcessorFiltersOut
981
+ )
982
+ await mock_context.app.webhook_manager.start_processing_event_messages()
983
+ mock_context.app.fast_api_app.include_router(
984
+ mock_context.app.webhook_manager._router
985
+ )
986
+ client = TestClient(mock_context.app.fast_api_app)
987
+
988
+ test_payload = {"test": "data"}
989
+
990
+ async with event_context(EventType.HTTP_REQUEST, trigger_type="request") as event:
991
+ mock_context.app.webhook_manager.port_app_config_handler.get_port_app_config = AsyncMock(return_value=mock_port_app_config) # type: ignore
992
+ event.port_app_config = (
993
+ await mock_context.app.webhook_manager.port_app_config_handler.get_port_app_config()
302
994
  )
303
995
 
304
- await processor_manager.start_processing_event_messages()
305
- await processor_manager._event_queues["/test"].put(mock_event)
996
+ response = client.post(
997
+ test_path, json=test_payload, headers={"Content-Type": "application/json"}
998
+ )
306
999
 
307
- await asyncio.sleep(0.1)
1000
+ assert response.status_code == 200
1001
+ assert response.json() == {"status": "ok"}
308
1002
 
309
- assert processor_manager.no_matching_processors
310
- assert len(processor_manager.running_processors) == 0
1003
+ try:
1004
+ await asyncio.wait_for(processing_complete.wait(), timeout=10.0)
1005
+ except asyncio.TimeoutError:
1006
+ pytest.fail("Event processing timed out")
311
1007
 
312
- @pytest.mark.skip(reason="Temporarily ignoring this test")
313
- async def test_multiple_processors(
314
- self, processor_manager: TestableWebhookProcessorManager
315
- ) -> None:
316
- # Test multiple processors for same path
317
- processor_manager.register_processor("/test", MockWebhookProcessor)
318
- processor_manager.register_processor("/test", MockWebhookProcessor)
319
- assert len(processor_manager._processors["/test"]) == 2
1008
+ assert len(processed_events) == 2
1009
+ assert mock_upsert.call_count == 1
1010
+ mock_delete.assert_not_called()
320
1011
 
321
- @pytest.mark.skip(reason="Temporarily ignoring this test")
322
- async def test_all_matching_processors_execute(
323
- self,
324
- processor_manager: TestableWebhookProcessorManager,
325
- mock_event: WebhookEvent,
326
- ) -> None:
327
- """Test that all matching processors are executed even if some fail."""
328
- processed_count = 0
1012
+ await mock_context.app.webhook_manager.shutdown()
329
1013
 
330
- class SuccessProcessor(MockWebhookProcessor):
331
- async def handle_event(self, payload: Dict[str, Any]) -> None:
332
- nonlocal processed_count
333
- processed_count += 1
334
- self.processed = True
335
1014
 
336
- class FailingProcessor(MockWebhookProcessor):
337
- async def handle_event(self, payload: Dict[str, Any]) -> None:
338
- raise Exception("Simulated failure")
1015
+ @pytest.mark.asyncio
1016
+ @patch(
1017
+ "port_ocean.core.handlers.entities_state_applier.port.applier.HttpEntitiesStateApplier.upsert"
1018
+ )
1019
+ @patch(
1020
+ "port_ocean.core.handlers.entities_state_applier.port.applier.HttpEntitiesStateApplier.delete"
1021
+ )
1022
+ async def test_integrationTest_postRequestSent_webhookEventRawResultProcessedwithRetry_entityUpserted(
1023
+ mock_delete: AsyncMock,
1024
+ mock_upsert: AsyncMock,
1025
+ mock_context: PortOceanContext,
1026
+ mock_port_app_config: PortAppConfig,
1027
+ monkeypatch: pytest.MonkeyPatch,
1028
+ ) -> None:
1029
+ """Integration test for the complete webhook processing flow"""
1030
+
1031
+ monkeypatch.setattr(
1032
+ "port_ocean.core.integrations.mixins.handler.ocean", mock_context
1033
+ )
1034
+ monkeypatch.setattr(
1035
+ "port_ocean.core.integrations.mixins.live_events.ocean", mock_context
1036
+ )
1037
+ processed_events: list[WebhookEventRawResults] = []
1038
+ mock_upsert.return_value = [entity]
1039
+ test_state = {"retry": False}
1040
+
1041
+ class TestProcessor(AbstractWebhookProcessor):
1042
+ def __init__(self, event: WebhookEvent) -> None:
1043
+ super().__init__(event)
1044
+ self.tries = 0
1045
+
1046
+ async def authenticate(
1047
+ self, payload: Dict[str, Any], headers: Dict[str, str]
1048
+ ) -> bool:
1049
+ return True
1050
+
1051
+ async def validate_payload(self, payload: Dict[str, Any]) -> bool:
1052
+ return True
1053
+
1054
+ async def handle_event(
1055
+ self, payload: EventPayload, resource: ResourceConfig
1056
+ ) -> WebhookEventRawResults:
1057
+ self.tries += 1
1058
+ if self.tries < 2:
1059
+ test_state["retry"] = True
1060
+ raise RetryableError("Test error")
1061
+ event_data = WebhookEventRawResults(
1062
+ updated_raw_results=[
1063
+ {
1064
+ "name": "repo-one",
1065
+ "links": {"html": {"href": "https://example.com/repo-one"}},
1066
+ "main_branch": "main",
1067
+ }
1068
+ ],
1069
+ deleted_raw_results=[],
1070
+ )
1071
+ processed_events.append(event_data)
1072
+ return event_data
1073
+
1074
+ def should_process_event(self, event: WebhookEvent) -> bool:
1075
+ return True
1076
+
1077
+ def get_matching_kinds(self, event: WebhookEvent) -> list[str]:
1078
+ return ["repository"]
1079
+
1080
+ processing_complete = asyncio.Event()
1081
+ original_process_data = LiveEventsMixin.sync_raw_results
1082
+
1083
+ async def patched_export_single_resource(
1084
+ self: LiveEventsMixin, webhookEventRawResults: list[WebhookEventRawResults]
1085
+ ) -> None:
1086
+ try:
1087
+ await original_process_data(self, webhookEventRawResults)
1088
+ except Exception as e:
1089
+ raise e
1090
+ finally:
1091
+ processing_complete.set()
1092
+
1093
+ monkeypatch.setattr(
1094
+ LiveEventsMixin,
1095
+ "sync_raw_results",
1096
+ patched_export_single_resource,
1097
+ )
1098
+ test_path = "/webhook-test"
1099
+ mock_context.app.integration = BaseIntegration(ocean)
1100
+ mock_context.app.webhook_manager = LiveEventsProcessorManager(
1101
+ mock_context.app.integration_router, SignalHandler()
1102
+ )
1103
+
1104
+ mock_context.app.webhook_manager.register_processor(test_path, TestProcessor)
1105
+ await mock_context.app.webhook_manager.start_processing_event_messages()
1106
+ mock_context.app.fast_api_app.include_router(
1107
+ mock_context.app.webhook_manager._router
1108
+ )
1109
+ client = TestClient(mock_context.app.fast_api_app)
1110
+
1111
+ test_payload = {"test": "data"}
1112
+
1113
+ async with event_context(EventType.HTTP_REQUEST, trigger_type="request") as event:
1114
+ mock_context.app.webhook_manager.port_app_config_handler.get_port_app_config = AsyncMock(return_value=mock_port_app_config) # type: ignore
1115
+ event.port_app_config = (
1116
+ await mock_context.app.webhook_manager.port_app_config_handler.get_port_app_config()
1117
+ )
339
1118
 
340
- # Register mix of successful and failing processors
341
- processor_manager.register_processor("/test", SuccessProcessor)
342
- processor_manager.register_processor("/test", FailingProcessor)
343
- processor_manager.register_processor("/test", SuccessProcessor)
1119
+ response = client.post(
1120
+ test_path, json=test_payload, headers={"Content-Type": "application/json"}
1121
+ )
344
1122
 
345
- await processor_manager.start_processing_event_messages()
346
- await processor_manager._event_queues["/test"].put(mock_event)
1123
+ assert response.status_code == 200
1124
+ assert response.json() == {"status": "ok"}
347
1125
 
348
- # Wait for processing to complete
349
- await asyncio.sleep(0.1)
1126
+ try:
1127
+ await asyncio.wait_for(processing_complete.wait(), timeout=10.0)
1128
+ except asyncio.TimeoutError:
1129
+ pytest.fail("Event processing timed out")
350
1130
 
351
- # Verify successful processors ran despite failing one
352
- assert processed_count == 2
1131
+ assert len(processed_events) == 1
1132
+ assert test_state["retry"] is True
1133
+ mock_upsert.assert_called_once()
1134
+ mock_delete.assert_not_called()
353
1135
 
354
- @pytest.mark.skip(reason="Temporarily ignoring this test")
355
- async def test_retry_mechanism(
356
- self,
357
- processor_manager: TestableWebhookProcessorManager,
358
- mock_event: WebhookEvent,
359
- ) -> None:
360
- """Test retry mechanism with temporary failures."""
361
- processor = MockWebhookProcessor(mock_event)
362
- processor.error_to_raise = RetryableError("Temporary failure")
1136
+ await mock_context.app.webhook_manager.shutdown()
363
1137
 
364
- # Simulate 2 failures before success
365
- async def handle_event(payload: Dict[str, Any]) -> None:
366
- if processor.retry_count < 2:
367
- raise RetryableError("Temporary failure")
368
- processor.processed = True
369
1138
 
370
- processor.handle_event = handle_event # type: ignore
1139
+ @pytest.mark.asyncio
1140
+ @patch(
1141
+ "port_ocean.core.handlers.entities_state_applier.port.applier.HttpEntitiesStateApplier.upsert"
1142
+ )
1143
+ @patch(
1144
+ "port_ocean.core.handlers.entities_state_applier.port.applier.HttpEntitiesStateApplier.delete"
1145
+ )
1146
+ async def test_integrationTest_postRequestSent_webhookEventRawResultProcessedwithRetry_exceededMaxRetries_entityNotUpserted(
1147
+ mock_delete: AsyncMock,
1148
+ mock_upsert: AsyncMock,
1149
+ mock_context: PortOceanContext,
1150
+ mock_port_app_config: PortAppConfig,
1151
+ monkeypatch: pytest.MonkeyPatch,
1152
+ ) -> None:
1153
+ """Integration test for the complete webhook processing flow"""
1154
+
1155
+ monkeypatch.setattr(
1156
+ "port_ocean.core.integrations.mixins.handler.ocean", mock_context
1157
+ )
1158
+ monkeypatch.setattr(
1159
+ "port_ocean.core.integrations.mixins.live_events.ocean", mock_context
1160
+ )
1161
+ processed_events: list[WebhookEventRawResults] = []
1162
+ mock_upsert.return_value = [entity]
1163
+ test_state = {"retry": False, "exception": False}
1164
+
1165
+ class TestProcessor(AbstractWebhookProcessor):
1166
+ def __init__(self, event: WebhookEvent) -> None:
1167
+ super().__init__(event)
1168
+ self.tries = 0
1169
+
1170
+ async def authenticate(
1171
+ self, payload: Dict[str, Any], headers: Dict[str, str]
1172
+ ) -> bool:
1173
+ return True
1174
+
1175
+ async def validate_payload(self, payload: Dict[str, Any]) -> bool:
1176
+ return True
1177
+
1178
+ async def handle_event(
1179
+ self, payload: EventPayload, resource: ResourceConfig
1180
+ ) -> WebhookEventRawResults:
1181
+ self.tries += 1
1182
+ if self.tries < 5:
1183
+ test_state["retry"] = True
1184
+ raise RetryableError("Test error")
1185
+ event_data = WebhookEventRawResults(
1186
+ updated_raw_results=[
1187
+ {
1188
+ "name": "repo-one",
1189
+ "links": {"html": {"href": "https://example.com/repo-one"}},
1190
+ "main_branch": "main",
1191
+ }
1192
+ ],
1193
+ deleted_raw_results=[],
1194
+ )
1195
+ processed_events.append(event_data)
1196
+ return event_data
1197
+
1198
+ def should_process_event(self, event: WebhookEvent) -> bool:
1199
+ return True
1200
+
1201
+ def get_matching_kinds(self, event: WebhookEvent) -> list[str]:
1202
+ return ["repository"]
1203
+
1204
+ processing_complete = asyncio.Event()
1205
+ original_process_data = LiveEventsProcessorManager._process_webhook_request
1206
+
1207
+ async def patched_process_webhook_request(
1208
+ self: LiveEventsProcessorManager,
1209
+ processor: AbstractWebhookProcessor,
1210
+ resource: ResourceConfig,
1211
+ ) -> WebhookEventRawResults:
1212
+ try:
1213
+ return await original_process_data(self, processor, resource)
1214
+ except Exception as e:
1215
+ test_state["exception"] = True
1216
+ raise e
1217
+ finally:
1218
+ processing_complete.set()
1219
+
1220
+ monkeypatch.setattr(
1221
+ LiveEventsProcessorManager,
1222
+ "_process_webhook_request",
1223
+ patched_process_webhook_request,
1224
+ )
1225
+ test_path = "/webhook-test"
1226
+ mock_context.app.integration = BaseIntegration(ocean)
1227
+ mock_context.app.webhook_manager = LiveEventsProcessorManager(
1228
+ mock_context.app.integration_router, SignalHandler()
1229
+ )
1230
+
1231
+ mock_context.app.webhook_manager.register_processor(test_path, TestProcessor)
1232
+ await mock_context.app.webhook_manager.start_processing_event_messages()
1233
+ mock_context.app.fast_api_app.include_router(
1234
+ mock_context.app.webhook_manager._router
1235
+ )
1236
+ client = TestClient(mock_context.app.fast_api_app)
1237
+
1238
+ test_payload = {"test": "data"}
1239
+
1240
+ async with event_context(EventType.HTTP_REQUEST, trigger_type="request") as event:
1241
+ mock_context.app.webhook_manager.port_app_config_handler.get_port_app_config = AsyncMock(return_value=mock_port_app_config) # type: ignore
1242
+ event.port_app_config = (
1243
+ await mock_context.app.webhook_manager.port_app_config_handler.get_port_app_config()
1244
+ )
371
1245
 
372
- await processor_manager._process_webhook_request(processor)
1246
+ response = client.post(
1247
+ test_path, json=test_payload, headers={"Content-Type": "application/json"}
1248
+ )
373
1249
 
374
- assert processor.processed
375
- assert processor.retry_count == 2
1250
+ assert response.status_code == 200
1251
+ assert response.json() == {"status": "ok"}
376
1252
 
377
- @pytest.mark.skip(reason="Temporarily ignoring this test")
378
- async def test_max_retries_exceeded(
379
- self,
380
- processor_manager: TestableWebhookProcessorManager,
381
- mock_event: WebhookEvent,
382
- ) -> None:
383
- """Test behavior when max retries are exceeded."""
384
- processor = MockWebhookProcessor(mock_event)
385
- processor.max_retries = 1
386
- processor.error_to_raise = RetryableError("Temporary failure")
1253
+ try:
1254
+ await asyncio.wait_for(processing_complete.wait(), timeout=30.0)
1255
+ except asyncio.TimeoutError:
1256
+ pytest.fail("Event processing timed out")
387
1257
 
388
- with pytest.raises(RetryableError):
389
- await processor_manager._process_webhook_request(processor)
1258
+ assert len(processed_events) == 0
1259
+ assert test_state["retry"] is True
1260
+ assert test_state["exception"] is True
1261
+ mock_upsert.assert_not_called()
1262
+ mock_delete.assert_not_called()
390
1263
 
391
- assert processor.retry_count == processor.max_retries
1264
+ await mock_context.app.webhook_manager.shutdown()