port-ocean 0.19.2__py3-none-any.whl → 0.20.0__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
@@ -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()