port-ocean 0.18.4__py3-none-any.whl → 0.18.6__py3-none-any.whl
Sign up to get free protection for your applications and to get access to all the features.
- port_ocean/context/event.py +8 -1
- port_ocean/core/handlers/port_app_config/api.py +4 -1
- port_ocean/core/integrations/mixins/sync_raw.py +25 -19
- port_ocean/exceptions/api.py +7 -0
- port_ocean/tests/core/handlers/mixins/test_sync_raw.py +149 -124
- port_ocean/tests/core/handlers/port_app_config/test_api.py +67 -0
- port_ocean/tests/core/handlers/port_app_config/test_base.py +197 -0
- {port_ocean-0.18.4.dist-info → port_ocean-0.18.6.dist-info}/METADATA +1 -1
- {port_ocean-0.18.4.dist-info → port_ocean-0.18.6.dist-info}/RECORD +12 -10
- {port_ocean-0.18.4.dist-info → port_ocean-0.18.6.dist-info}/LICENSE.md +0 -0
- {port_ocean-0.18.4.dist-info → port_ocean-0.18.6.dist-info}/WHEEL +0 -0
- {port_ocean-0.18.4.dist-info → port_ocean-0.18.6.dist-info}/entry_points.txt +0 -0
port_ocean/context/event.py
CHANGED
@@ -19,6 +19,7 @@ from pydispatch import dispatcher # type: ignore
|
|
19
19
|
from werkzeug.local import LocalStack, LocalProxy
|
20
20
|
|
21
21
|
from port_ocean.context.resource import resource
|
22
|
+
from port_ocean.exceptions.api import EmptyPortAppConfigError
|
22
23
|
from port_ocean.exceptions.context import (
|
23
24
|
EventContextNotFoundError,
|
24
25
|
ResourceContextNotFoundError,
|
@@ -176,8 +177,14 @@ async def event_context(
|
|
176
177
|
logger.info("Event started")
|
177
178
|
try:
|
178
179
|
yield event
|
179
|
-
except:
|
180
|
+
except EmptyPortAppConfigError as e:
|
181
|
+
logger.error(
|
182
|
+
f"Skipping resync due to empty mapping: {str(e)}", exc_info=True
|
183
|
+
)
|
184
|
+
raise
|
185
|
+
except Exception as e:
|
180
186
|
success = False
|
187
|
+
logger.error(f"Event failed with error: {str(e)}", exc_info=True)
|
181
188
|
raise
|
182
189
|
else:
|
183
190
|
success = True
|
@@ -3,6 +3,7 @@ from typing import Any
|
|
3
3
|
from loguru import logger
|
4
4
|
|
5
5
|
from port_ocean.core.handlers.port_app_config.base import BasePortAppConfig
|
6
|
+
from port_ocean.exceptions.api import EmptyPortAppConfigError
|
6
7
|
|
7
8
|
|
8
9
|
class APIPortAppConfig(BasePortAppConfig):
|
@@ -20,7 +21,9 @@ class APIPortAppConfig(BasePortAppConfig):
|
|
20
21
|
if not config:
|
21
22
|
logger.error(
|
22
23
|
"The integration port app config is empty. "
|
24
|
+
f"Integration: {integration}, "
|
25
|
+
f"Config: {config}. "
|
23
26
|
"Please make sure to configure your port app config using Port's API."
|
24
27
|
)
|
25
|
-
|
28
|
+
raise EmptyPortAppConfigError()
|
26
29
|
return config
|
@@ -220,26 +220,32 @@ class SyncRawMixin(HandlerMixin, EventsMixin):
|
|
220
220
|
)
|
221
221
|
modified_objects = []
|
222
222
|
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
if changed_entities:
|
230
|
-
logger.info("Upserting changed entities", changed_entities=len(changed_entities),
|
231
|
-
total_entities=len(objects_diff[0].entity_selector_diff.passed))
|
232
|
-
await self.entities_state_applier.upsert(
|
233
|
-
changed_entities, user_agent_type
|
234
|
-
)
|
235
|
-
else:
|
236
|
-
logger.info("Entities in batch didn't changed since last sync, skipping", total_entities=len(objects_diff[0].entity_selector_diff.passed))
|
237
|
-
modified_objects = [ocean.port_client._reduce_entity(entity) for entity in objects_diff[0].entity_selector_diff.passed]
|
238
|
-
except Exception as e:
|
239
|
-
logger.warning(f"Failed to resolve batch entities with Port, falling back to upserting all entities: {str(e)}")
|
240
|
-
modified_objects = await self.entities_state_applier.upsert(
|
241
|
-
objects_diff[0].entity_selector_diff.passed, user_agent_type
|
223
|
+
if event.event_type == EventType.RESYNC:
|
224
|
+
try:
|
225
|
+
changed_entities = await self._map_entities_compared_with_port(
|
226
|
+
objects_diff[0].entity_selector_diff.passed,
|
227
|
+
resource,
|
228
|
+
user_agent_type
|
242
229
|
)
|
230
|
+
if changed_entities:
|
231
|
+
logger.info("Upserting changed entities", changed_entities=len(changed_entities),
|
232
|
+
total_entities=len(objects_diff[0].entity_selector_diff.passed))
|
233
|
+
await self.entities_state_applier.upsert(
|
234
|
+
changed_entities, user_agent_type
|
235
|
+
)
|
236
|
+
else:
|
237
|
+
logger.info("Entities in batch didn't changed since last sync, skipping", total_entities=len(objects_diff[0].entity_selector_diff.passed))
|
238
|
+
modified_objects = [ocean.port_client._reduce_entity(entity) for entity in objects_diff[0].entity_selector_diff.passed]
|
239
|
+
except Exception as e:
|
240
|
+
logger.warning(f"Failed to resolve batch entities with Port, falling back to upserting all entities: {str(e)}")
|
241
|
+
modified_objects = await self.entities_state_applier.upsert(
|
242
|
+
objects_diff[0].entity_selector_diff.passed, user_agent_type
|
243
|
+
)
|
244
|
+
else:
|
245
|
+
modified_objects = await self.entities_state_applier.upsert(
|
246
|
+
objects_diff[0].entity_selector_diff.passed, user_agent_type
|
247
|
+
)
|
248
|
+
|
243
249
|
|
244
250
|
return CalculationResult(
|
245
251
|
objects_diff[0].entity_selector_diff._replace(passed=modified_objects),
|
port_ocean/exceptions/api.py
CHANGED
@@ -13,3 +13,10 @@ class BaseAPIException(BaseOceanException, abc.ABC):
|
|
13
13
|
class InternalServerException(BaseAPIException):
|
14
14
|
def response(self) -> Response:
|
15
15
|
return PlainTextResponse(content="Internal server error", status_code=500)
|
16
|
+
|
17
|
+
|
18
|
+
class EmptyPortAppConfigError(Exception):
|
19
|
+
"""Exception raised when the Port app configuration is empty."""
|
20
|
+
|
21
|
+
def __init__(self, message: str = "Port app config is empty") -> None:
|
22
|
+
super().__init__(message)
|
@@ -435,66 +435,63 @@ async def test_register_raw(
|
|
435
435
|
mock_sync_raw_mixin_with_jq_processor: SyncRawMixin,
|
436
436
|
mock_resource_config: ResourceConfig,
|
437
437
|
) -> None:
|
438
|
-
# Mock the integration settings with skip_check_diff
|
439
|
-
with patch.object(ocean.config.integration, "skip_check_diff", False):
|
440
|
-
kind = "service"
|
441
|
-
user_agent_type = UserAgentType.exporter
|
442
|
-
raw_entity = [
|
443
|
-
{"id": "entity_1", "name": "entity_1", "web_url": "https://example.com"},
|
444
|
-
]
|
445
|
-
expected_result = [
|
446
|
-
{
|
447
|
-
"identifier": "entity_1",
|
448
|
-
"blueprint": "service",
|
449
|
-
"name": "entity_1",
|
450
|
-
"properties": {},
|
451
|
-
},
|
452
|
-
]
|
453
438
|
|
454
|
-
|
455
|
-
|
456
|
-
|
457
|
-
|
439
|
+
kind = "service"
|
440
|
+
user_agent_type = UserAgentType.exporter
|
441
|
+
raw_entity = [
|
442
|
+
{"id": "entity_1", "name": "entity_1", "web_url": "https://example.com"},
|
443
|
+
]
|
444
|
+
expected_result = [
|
445
|
+
{
|
446
|
+
"identifier": "entity_1",
|
447
|
+
"blueprint": "service",
|
448
|
+
"name": "entity_1",
|
449
|
+
"properties": {"url": "https://example.com"},
|
450
|
+
},
|
451
|
+
]
|
452
|
+
|
453
|
+
async with event_context(EventType.HTTP_REQUEST, trigger_type="machine") as event:
|
454
|
+
# Use patch to mock the method instead of direct assignment
|
455
|
+
with patch.object(
|
456
|
+
mock_sync_raw_mixin_with_jq_processor.port_app_config_handler,
|
457
|
+
"get_port_app_config",
|
458
|
+
return_value=PortAppConfig(
|
459
|
+
enable_merge_entity=True,
|
460
|
+
delete_dependent_entities=True,
|
461
|
+
create_missing_related_entities=False,
|
462
|
+
resources=[mock_resource_config],
|
463
|
+
),
|
464
|
+
):
|
465
|
+
# Ensure the event.port_app_config is set correctly
|
466
|
+
event.port_app_config = await mock_sync_raw_mixin_with_jq_processor.port_app_config_handler.get_port_app_config(
|
467
|
+
use_cache=False
|
468
|
+
)
|
469
|
+
|
470
|
+
def upsert_side_effect(
|
471
|
+
entities: list[Entity], user_agent_type: UserAgentType
|
472
|
+
) -> list[Entity]:
|
473
|
+
# Simulate returning the passed entities
|
474
|
+
return entities
|
475
|
+
|
476
|
+
# Patch the upsert method with the side effect
|
458
477
|
with patch.object(
|
459
|
-
mock_sync_raw_mixin_with_jq_processor.
|
460
|
-
"
|
461
|
-
|
462
|
-
enable_merge_entity=True,
|
463
|
-
delete_dependent_entities=True,
|
464
|
-
create_missing_related_entities=False,
|
465
|
-
resources=[mock_resource_config],
|
466
|
-
),
|
478
|
+
mock_sync_raw_mixin_with_jq_processor.entities_state_applier,
|
479
|
+
"upsert",
|
480
|
+
side_effect=upsert_side_effect,
|
467
481
|
):
|
468
|
-
#
|
469
|
-
|
470
|
-
|
471
|
-
|
472
|
-
|
473
|
-
def upsert_side_effect(
|
474
|
-
entities: list[Entity], user_agent_type: UserAgentType
|
475
|
-
) -> list[Entity]:
|
476
|
-
# Simulate returning the passed entities
|
477
|
-
return entities
|
478
|
-
|
479
|
-
# Patch the upsert method with the side effect
|
480
|
-
with patch.object(
|
481
|
-
mock_sync_raw_mixin_with_jq_processor.entities_state_applier,
|
482
|
-
"upsert",
|
483
|
-
side_effect=upsert_side_effect,
|
484
|
-
):
|
485
|
-
# Call the register_raw method
|
486
|
-
registered_entities = (
|
487
|
-
await mock_sync_raw_mixin_with_jq_processor.register_raw(
|
488
|
-
kind, raw_entity, user_agent_type
|
489
|
-
)
|
482
|
+
# Call the register_raw method
|
483
|
+
registered_entities = (
|
484
|
+
await mock_sync_raw_mixin_with_jq_processor.register_raw(
|
485
|
+
kind, raw_entity, user_agent_type
|
490
486
|
)
|
487
|
+
)
|
491
488
|
|
492
|
-
|
493
|
-
|
494
|
-
|
495
|
-
|
496
|
-
|
497
|
-
|
489
|
+
# Assert that the registered entities match the expected results
|
490
|
+
assert len(registered_entities) == len(expected_result)
|
491
|
+
for entity, result in zip(registered_entities, expected_result):
|
492
|
+
assert entity.identifier == result["identifier"]
|
493
|
+
assert entity.blueprint == result["blueprint"]
|
494
|
+
assert entity.properties == result["properties"]
|
498
495
|
|
499
496
|
|
500
497
|
@pytest.mark.asyncio
|
@@ -689,30 +686,26 @@ async def test_register_resource_raw_no_changes_upsert_not_called_entitiy_is_ret
|
|
689
686
|
mock_sync_raw_mixin: SyncRawMixin,
|
690
687
|
mock_port_app_config: PortAppConfig,
|
691
688
|
) -> None:
|
692
|
-
|
693
|
-
|
694
|
-
|
695
|
-
|
696
|
-
|
697
|
-
|
698
|
-
|
699
|
-
|
700
|
-
|
701
|
-
|
702
|
-
#
|
703
|
-
|
704
|
-
|
705
|
-
|
706
|
-
], # Use the first resource from the config
|
707
|
-
[{"some": "data"}],
|
708
|
-
UserAgentType.exporter,
|
709
|
-
)
|
689
|
+
entity = Entity(identifier="1", blueprint="service")
|
690
|
+
mock_sync_raw_mixin._calculate_raw = AsyncMock(return_value=[CalculationResult(entity_selector_diff=EntitySelectorDiff(passed=[entity], failed=[]), errors=[], misconfigurations=[], misonfigured_entity_keys=[])]) # type: ignore
|
691
|
+
mock_sync_raw_mixin._map_entities_compared_with_port = AsyncMock(return_value=([])) # type: ignore
|
692
|
+
mock_sync_raw_mixin.entities_state_applier.upsert = AsyncMock() # type: ignore
|
693
|
+
|
694
|
+
async with event_context(EventType.RESYNC, trigger_type="machine") as event:
|
695
|
+
event.port_app_config = mock_port_app_config
|
696
|
+
|
697
|
+
# Test execution
|
698
|
+
result = await mock_sync_raw_mixin._register_resource_raw(
|
699
|
+
mock_port_app_config.resources[0], # Use the first resource from the config
|
700
|
+
[{"some": "data"}],
|
701
|
+
UserAgentType.exporter,
|
702
|
+
)
|
710
703
|
|
711
|
-
|
712
|
-
|
713
|
-
|
714
|
-
|
715
|
-
|
704
|
+
# Assertions
|
705
|
+
assert len(result.entity_selector_diff.passed) == 1
|
706
|
+
mock_sync_raw_mixin._calculate_raw.assert_called_once()
|
707
|
+
mock_sync_raw_mixin.entities_state_applier.upsert.assert_not_called()
|
708
|
+
mock_sync_raw_mixin._map_entities_compared_with_port.assert_called_once()
|
716
709
|
|
717
710
|
|
718
711
|
@pytest.mark.asyncio
|
@@ -720,57 +713,89 @@ async def test_register_resource_raw_with_changes_upsert_called_and_entities_are
|
|
720
713
|
mock_sync_raw_mixin: SyncRawMixin,
|
721
714
|
mock_port_app_config: PortAppConfig,
|
722
715
|
) -> None:
|
723
|
-
|
724
|
-
|
725
|
-
|
726
|
-
|
727
|
-
|
728
|
-
|
729
|
-
|
730
|
-
|
731
|
-
|
732
|
-
|
733
|
-
|
734
|
-
|
735
|
-
|
736
|
-
|
737
|
-
UserAgentType.exporter,
|
738
|
-
)
|
716
|
+
entity = Entity(identifier="1", blueprint="service")
|
717
|
+
mock_sync_raw_mixin._calculate_raw = AsyncMock(return_value=[CalculationResult(entity_selector_diff=EntitySelectorDiff(passed=[entity], failed=[]), errors=[], misconfigurations=[], misonfigured_entity_keys=[])]) # type: ignore
|
718
|
+
mock_sync_raw_mixin._map_entities_compared_with_port = AsyncMock(return_value=([entity])) # type: ignore
|
719
|
+
mock_sync_raw_mixin.entities_state_applier.upsert = AsyncMock(return_value=[entity]) # type: ignore
|
720
|
+
|
721
|
+
async with event_context(EventType.RESYNC, trigger_type="machine") as event:
|
722
|
+
event.port_app_config = mock_port_app_config
|
723
|
+
|
724
|
+
# Test execution
|
725
|
+
result = await mock_sync_raw_mixin._register_resource_raw(
|
726
|
+
mock_port_app_config.resources[0],
|
727
|
+
[{"some": "data"}],
|
728
|
+
UserAgentType.exporter,
|
729
|
+
)
|
739
730
|
|
740
|
-
|
741
|
-
|
742
|
-
|
743
|
-
|
744
|
-
|
731
|
+
# Assertions
|
732
|
+
assert len(result.entity_selector_diff.passed) == 1
|
733
|
+
mock_sync_raw_mixin._calculate_raw.assert_called_once()
|
734
|
+
mock_sync_raw_mixin.entities_state_applier.upsert.assert_called_once()
|
735
|
+
mock_sync_raw_mixin._map_entities_compared_with_port.assert_called_once()
|
745
736
|
|
746
737
|
|
747
738
|
@pytest.mark.asyncio
|
748
739
|
async def test_register_resource_raw_with_errors(
|
749
740
|
mock_sync_raw_mixin: SyncRawMixin, mock_port_app_config: PortAppConfig
|
750
741
|
) -> None:
|
751
|
-
|
752
|
-
|
753
|
-
|
754
|
-
|
755
|
-
|
756
|
-
|
757
|
-
|
758
|
-
|
759
|
-
|
760
|
-
|
761
|
-
|
762
|
-
|
763
|
-
|
764
|
-
|
765
|
-
|
766
|
-
|
767
|
-
|
742
|
+
failed_entity = Entity(identifier="1", blueprint="service")
|
743
|
+
error = Exception("Test error")
|
744
|
+
mock_sync_raw_mixin._calculate_raw = AsyncMock(return_value=[CalculationResult(entity_selector_diff=EntitySelectorDiff(passed=[], failed=[failed_entity]), errors=[error], misconfigurations=[], misonfigured_entity_keys=[])]) # type: ignore
|
745
|
+
mock_sync_raw_mixin._map_entities_compared_with_port = AsyncMock(return_value=([])) # type: ignore
|
746
|
+
mock_sync_raw_mixin.entities_state_applier.upsert = AsyncMock() # type: ignore
|
747
|
+
|
748
|
+
async with event_context(EventType.RESYNC, trigger_type="machine") as event:
|
749
|
+
event.port_app_config = mock_port_app_config
|
750
|
+
|
751
|
+
# Test execution
|
752
|
+
result = await mock_sync_raw_mixin._register_resource_raw(
|
753
|
+
mock_port_app_config.resources[0],
|
754
|
+
[{"some": "data"}],
|
755
|
+
UserAgentType.exporter,
|
756
|
+
)
|
757
|
+
|
758
|
+
# Assertions
|
759
|
+
assert len(result.entity_selector_diff.passed) == 0
|
760
|
+
assert len(result.entity_selector_diff.failed) == 1
|
761
|
+
assert len(result.errors) == 1
|
762
|
+
assert result.errors[0] == error
|
763
|
+
mock_sync_raw_mixin._calculate_raw.assert_called_once()
|
764
|
+
mock_sync_raw_mixin._map_entities_compared_with_port.assert_called_once()
|
765
|
+
mock_sync_raw_mixin.entities_state_applier.upsert.assert_not_called()
|
766
|
+
|
767
|
+
|
768
|
+
@pytest.mark.asyncio
|
769
|
+
async def test_register_resource_raw_skip_event_type_http_request_upsert_called_and_no_entitites_diff_calculation(
|
770
|
+
mock_sync_raw_mixin: SyncRawMixin,
|
771
|
+
mock_port_app_config: PortAppConfig,
|
772
|
+
mock_context: PortOceanContext,
|
773
|
+
monkeypatch: pytest.MonkeyPatch,
|
774
|
+
) -> None:
|
775
|
+
# Mock dependencies
|
776
|
+
entity = Entity(identifier="1", blueprint="service")
|
777
|
+
calculation_result = CalculationResult(
|
778
|
+
entity_selector_diff=EntitySelectorDiff(passed=[entity], failed=[]),
|
779
|
+
errors=[],
|
780
|
+
misconfigurations=[],
|
781
|
+
misonfigured_entity_keys=[],
|
782
|
+
)
|
783
|
+
mock_sync_raw_mixin._calculate_raw = AsyncMock(return_value=[calculation_result]) # type: ignore
|
784
|
+
mock_sync_raw_mixin._map_entities_compared_with_port = AsyncMock() # type: ignore
|
785
|
+
mock_sync_raw_mixin.entities_state_applier.upsert = AsyncMock(return_value=[entity]) # type: ignore
|
786
|
+
|
787
|
+
async with event_context(EventType.HTTP_REQUEST, trigger_type="machine") as event:
|
788
|
+
event.port_app_config = mock_port_app_config
|
789
|
+
|
790
|
+
# Test execution
|
791
|
+
result = await mock_sync_raw_mixin._register_resource_raw(
|
792
|
+
mock_port_app_config.resources[0],
|
793
|
+
[{"some": "data"}],
|
794
|
+
UserAgentType.exporter,
|
795
|
+
)
|
768
796
|
|
769
|
-
|
770
|
-
|
771
|
-
|
772
|
-
|
773
|
-
|
774
|
-
mock_sync_raw_mixin._calculate_raw.assert_called_once()
|
775
|
-
mock_sync_raw_mixin._map_entities_compared_with_port.assert_called_once()
|
776
|
-
mock_sync_raw_mixin.entities_state_applier.upsert.assert_not_called()
|
797
|
+
# Assertions
|
798
|
+
assert len(result.entity_selector_diff.passed) == 1
|
799
|
+
mock_sync_raw_mixin._calculate_raw.assert_called_once()
|
800
|
+
mock_sync_raw_mixin._map_entities_compared_with_port.assert_not_called()
|
801
|
+
mock_sync_raw_mixin.entities_state_applier.upsert.assert_called_once()
|
@@ -0,0 +1,67 @@
|
|
1
|
+
import pytest
|
2
|
+
from unittest.mock import AsyncMock
|
3
|
+
|
4
|
+
from port_ocean.core.handlers.port_app_config.api import APIPortAppConfig
|
5
|
+
from port_ocean.exceptions.api import EmptyPortAppConfigError
|
6
|
+
|
7
|
+
|
8
|
+
@pytest.fixture
|
9
|
+
def mock_context() -> AsyncMock:
|
10
|
+
context = AsyncMock()
|
11
|
+
context.port_client.get_current_integration = AsyncMock()
|
12
|
+
return context
|
13
|
+
|
14
|
+
|
15
|
+
@pytest.fixture
|
16
|
+
def api_config(mock_context: AsyncMock) -> APIPortAppConfig:
|
17
|
+
return APIPortAppConfig(mock_context)
|
18
|
+
|
19
|
+
|
20
|
+
async def test_get_port_app_config_valid_config_returns_config(
|
21
|
+
api_config: APIPortAppConfig, mock_context: AsyncMock
|
22
|
+
) -> None:
|
23
|
+
# Arrange
|
24
|
+
expected_config = {"key": "value"}
|
25
|
+
mock_context.port_client.get_current_integration.return_value = {
|
26
|
+
"config": expected_config
|
27
|
+
}
|
28
|
+
|
29
|
+
# Act
|
30
|
+
result = await api_config._get_port_app_config()
|
31
|
+
|
32
|
+
# Assert
|
33
|
+
assert result == expected_config
|
34
|
+
mock_context.port_client.get_current_integration.assert_called_once()
|
35
|
+
|
36
|
+
|
37
|
+
async def test_get_port_app_config_empty_config_raises_value_error(
|
38
|
+
api_config: APIPortAppConfig, mock_context: AsyncMock
|
39
|
+
) -> None:
|
40
|
+
# Arrange
|
41
|
+
mock_context.port_client.get_current_integration.return_value = {"config": {}}
|
42
|
+
|
43
|
+
# Act & Assert
|
44
|
+
with pytest.raises(EmptyPortAppConfigError, match="Port app config is empty"):
|
45
|
+
await api_config._get_port_app_config()
|
46
|
+
|
47
|
+
|
48
|
+
async def test_get_port_app_config_missing_config_key_raises_key_error(
|
49
|
+
api_config: APIPortAppConfig, mock_context: AsyncMock
|
50
|
+
) -> None:
|
51
|
+
# Arrange
|
52
|
+
mock_context.port_client.get_current_integration.return_value = {}
|
53
|
+
|
54
|
+
# Act & Assert
|
55
|
+
with pytest.raises(KeyError):
|
56
|
+
await api_config._get_port_app_config()
|
57
|
+
|
58
|
+
|
59
|
+
async def test_get_port_app_config_empty_integration_raises_key_error(
|
60
|
+
api_config: APIPortAppConfig, mock_context: AsyncMock
|
61
|
+
) -> None:
|
62
|
+
# Arrange
|
63
|
+
mock_context.port_client.get_current_integration.return_value = {}
|
64
|
+
|
65
|
+
# Act & Assert
|
66
|
+
with pytest.raises(KeyError):
|
67
|
+
await api_config._get_port_app_config()
|
@@ -0,0 +1,197 @@
|
|
1
|
+
import pytest
|
2
|
+
from unittest.mock import MagicMock
|
3
|
+
from pydantic import ValidationError
|
4
|
+
from typing import Any, Dict
|
5
|
+
|
6
|
+
from port_ocean.context.ocean import PortOceanContext
|
7
|
+
from port_ocean.core.handlers.port_app_config.base import BasePortAppConfig
|
8
|
+
from port_ocean.core.handlers.port_app_config.models import PortAppConfig
|
9
|
+
from port_ocean.context.event import EventType, event_context
|
10
|
+
from port_ocean.exceptions.api import EmptyPortAppConfigError
|
11
|
+
|
12
|
+
|
13
|
+
class TestPortAppConfig(BasePortAppConfig):
|
14
|
+
mock_get_port_app_config: Any
|
15
|
+
|
16
|
+
async def _get_port_app_config(self) -> Dict[str, Any]:
|
17
|
+
return self.mock_get_port_app_config()
|
18
|
+
|
19
|
+
|
20
|
+
@pytest.fixture
|
21
|
+
def mock_context() -> PortOceanContext:
|
22
|
+
context = MagicMock(spec=PortOceanContext)
|
23
|
+
context.config.port.port_app_config_cache_ttl = 300 # 5 minutes
|
24
|
+
return context
|
25
|
+
|
26
|
+
|
27
|
+
@pytest.fixture
|
28
|
+
def port_app_config_handler(mock_context: PortOceanContext) -> TestPortAppConfig:
|
29
|
+
handler = TestPortAppConfig(mock_context)
|
30
|
+
handler.mock_get_port_app_config = MagicMock()
|
31
|
+
return handler
|
32
|
+
|
33
|
+
|
34
|
+
@pytest.mark.asyncio
|
35
|
+
async def test_get_port_app_config_success(
|
36
|
+
port_app_config_handler: TestPortAppConfig,
|
37
|
+
) -> None:
|
38
|
+
# Arrange
|
39
|
+
valid_config = {
|
40
|
+
"resources": [
|
41
|
+
{
|
42
|
+
"kind": "repository",
|
43
|
+
"selector": {"query": "true"},
|
44
|
+
"port": {
|
45
|
+
"entity": {
|
46
|
+
"mappings": {
|
47
|
+
"identifier": ".name",
|
48
|
+
"title": ".name",
|
49
|
+
"blueprint": '"service"',
|
50
|
+
"properties": {
|
51
|
+
"description": ".description",
|
52
|
+
"url": ".html_url",
|
53
|
+
"defaultBranch": ".default_branch",
|
54
|
+
},
|
55
|
+
}
|
56
|
+
}
|
57
|
+
},
|
58
|
+
}
|
59
|
+
]
|
60
|
+
}
|
61
|
+
port_app_config_handler.mock_get_port_app_config.return_value = valid_config
|
62
|
+
|
63
|
+
# Act
|
64
|
+
async with event_context(EventType.RESYNC, trigger_type="machine"):
|
65
|
+
result = await port_app_config_handler.get_port_app_config()
|
66
|
+
|
67
|
+
# Assert
|
68
|
+
assert isinstance(result, PortAppConfig)
|
69
|
+
assert result.resources[0].port.entity.mappings.title == ".name"
|
70
|
+
assert result.resources[0].port.entity.mappings.identifier == ".name"
|
71
|
+
assert result.resources[0].port.entity.mappings.blueprint == '"service"'
|
72
|
+
assert (
|
73
|
+
result.resources[0].port.entity.mappings.properties["description"]
|
74
|
+
== ".description"
|
75
|
+
)
|
76
|
+
assert result.resources[0].port.entity.mappings.properties["url"] == ".html_url"
|
77
|
+
assert (
|
78
|
+
result.resources[0].port.entity.mappings.properties["defaultBranch"]
|
79
|
+
== ".default_branch"
|
80
|
+
)
|
81
|
+
port_app_config_handler.mock_get_port_app_config.assert_called_once()
|
82
|
+
|
83
|
+
|
84
|
+
@pytest.mark.asyncio
|
85
|
+
async def test_get_port_app_config_uses_cache(
|
86
|
+
port_app_config_handler: TestPortAppConfig,
|
87
|
+
) -> None:
|
88
|
+
# Arrange
|
89
|
+
valid_config = {
|
90
|
+
"resources": [
|
91
|
+
{
|
92
|
+
"kind": "repository",
|
93
|
+
"selector": {"query": "true"},
|
94
|
+
"port": {
|
95
|
+
"entity": {
|
96
|
+
"mappings": {
|
97
|
+
"identifier": ".name",
|
98
|
+
"title": ".name",
|
99
|
+
"blueprint": '"service"',
|
100
|
+
"properties": {
|
101
|
+
"description": ".description",
|
102
|
+
"url": ".html_url",
|
103
|
+
"defaultBranch": ".default_branch",
|
104
|
+
},
|
105
|
+
}
|
106
|
+
}
|
107
|
+
},
|
108
|
+
}
|
109
|
+
]
|
110
|
+
}
|
111
|
+
port_app_config_handler.mock_get_port_app_config.return_value = valid_config
|
112
|
+
|
113
|
+
# Act
|
114
|
+
async with event_context(EventType.RESYNC, trigger_type="machine"):
|
115
|
+
result1 = await port_app_config_handler.get_port_app_config()
|
116
|
+
result2 = await port_app_config_handler.get_port_app_config()
|
117
|
+
|
118
|
+
# Assert
|
119
|
+
assert result1 == result2
|
120
|
+
port_app_config_handler.mock_get_port_app_config.assert_called_once() # Called only once due to caching
|
121
|
+
|
122
|
+
|
123
|
+
@pytest.mark.asyncio
|
124
|
+
async def test_get_port_app_config_bypass_cache(
|
125
|
+
port_app_config_handler: TestPortAppConfig,
|
126
|
+
) -> None:
|
127
|
+
# Arrange
|
128
|
+
valid_config = {
|
129
|
+
"resources": [
|
130
|
+
{
|
131
|
+
"kind": "repository",
|
132
|
+
"selector": {"query": "true"},
|
133
|
+
"port": {
|
134
|
+
"entity": {
|
135
|
+
"mappings": {
|
136
|
+
"identifier": ".name",
|
137
|
+
"title": ".name",
|
138
|
+
"blueprint": '"service"',
|
139
|
+
"properties": {
|
140
|
+
"description": ".description",
|
141
|
+
"url": ".html_url",
|
142
|
+
"defaultBranch": ".default_branch",
|
143
|
+
},
|
144
|
+
}
|
145
|
+
}
|
146
|
+
},
|
147
|
+
}
|
148
|
+
]
|
149
|
+
}
|
150
|
+
port_app_config_handler.mock_get_port_app_config.return_value = valid_config
|
151
|
+
|
152
|
+
# Act
|
153
|
+
async with event_context(EventType.RESYNC, trigger_type="machine"):
|
154
|
+
result1 = await port_app_config_handler.get_port_app_config()
|
155
|
+
result2 = await port_app_config_handler.get_port_app_config(use_cache=False)
|
156
|
+
|
157
|
+
# Assert
|
158
|
+
assert result1 == result2
|
159
|
+
assert (
|
160
|
+
port_app_config_handler.mock_get_port_app_config.call_count == 2
|
161
|
+
) # Called twice due to cache bypass
|
162
|
+
|
163
|
+
|
164
|
+
@pytest.mark.asyncio
|
165
|
+
async def test_get_port_app_config_validation_error(
|
166
|
+
port_app_config_handler: TestPortAppConfig, monkeypatch: pytest.MonkeyPatch
|
167
|
+
) -> None:
|
168
|
+
# Arrange
|
169
|
+
invalid_config = {"invalid_field": "invalid_value"}
|
170
|
+
port_app_config_handler.mock_get_port_app_config.return_value = invalid_config
|
171
|
+
|
172
|
+
def mock_parse_obj(*args: Any, **kwargs: Any) -> None:
|
173
|
+
raise ValidationError(errors=[], model=PortAppConfig)
|
174
|
+
|
175
|
+
monkeypatch.setattr(
|
176
|
+
port_app_config_handler.CONFIG_CLASS, "parse_obj", mock_parse_obj
|
177
|
+
)
|
178
|
+
|
179
|
+
# Act & Assert
|
180
|
+
with pytest.raises(ValidationError):
|
181
|
+
async with event_context(EventType.RESYNC, trigger_type="machine"):
|
182
|
+
await port_app_config_handler.get_port_app_config()
|
183
|
+
|
184
|
+
|
185
|
+
@pytest.mark.asyncio
|
186
|
+
async def test_get_port_app_config_fetch_error(
|
187
|
+
port_app_config_handler: TestPortAppConfig,
|
188
|
+
) -> None:
|
189
|
+
# Arrange
|
190
|
+
port_app_config_handler.mock_get_port_app_config.side_effect = (
|
191
|
+
EmptyPortAppConfigError("Port app config is empty")
|
192
|
+
)
|
193
|
+
|
194
|
+
# Act & Assert
|
195
|
+
async with event_context(EventType.RESYNC, trigger_type="machine"):
|
196
|
+
with pytest.raises(EmptyPortAppConfigError, match="Port app config is empty"):
|
197
|
+
await port_app_config_handler.get_port_app_config()
|
@@ -63,7 +63,7 @@ port_ocean/config/settings.py,sha256=q5KgDIr8snIxejoKyWSyf21R_AYEEXiEd3-ry9qf3ss
|
|
63
63
|
port_ocean/consumers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
64
64
|
port_ocean/consumers/kafka_consumer.py,sha256=N8KocjBi9aR0BOPG8hgKovg-ns_ggpEjrSxqSqF_BSo,4710
|
65
65
|
port_ocean/context/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
66
|
-
port_ocean/context/event.py,sha256=
|
66
|
+
port_ocean/context/event.py,sha256=QK2ben4fJtxdorq_yRroATttP0DRc4wLtlUJ1as5D58,6208
|
67
67
|
port_ocean/context/ocean.py,sha256=0kgIi0zAsGarF52Qehu4bOxnAFEb0yayzG7xZioMHJc,4993
|
68
68
|
port_ocean/context/resource.py,sha256=yDj63URzQelj8zJPh4BAzTtPhpKr9Gw9DRn7I_0mJ1s,1692
|
69
69
|
port_ocean/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
@@ -91,7 +91,7 @@ port_ocean/core/handlers/entity_processor/__init__.py,sha256=FvFCunFg44wNQoqlybe
|
|
91
91
|
port_ocean/core/handlers/entity_processor/base.py,sha256=udR0w5TstTOS5xOfTjAZIEdldn4xr6Oyb3DylatYX3Q,1869
|
92
92
|
port_ocean/core/handlers/entity_processor/jq_entity_processor.py,sha256=C6zJbS3miKyDeXiEV-0t5vJvkEznOeXRZFFOnwJcNdA,11714
|
93
93
|
port_ocean/core/handlers/port_app_config/__init__.py,sha256=8AAT5OthiVM7KCcM34iEgEeXtn2pRMrT4Dze5r1Ixbk,134
|
94
|
-
port_ocean/core/handlers/port_app_config/api.py,sha256=
|
94
|
+
port_ocean/core/handlers/port_app_config/api.py,sha256=r_Th66NEw38IpRdnXZcRvI8ACfvxW_A6V62WLwjWXlQ,1044
|
95
95
|
port_ocean/core/handlers/port_app_config/base.py,sha256=4Nxt2g8voEIHJ4Y1Km5NJcaG2iSbCklw5P8-Kus7Y9k,3007
|
96
96
|
port_ocean/core/handlers/port_app_config/models.py,sha256=YvYtf_44KD_rN4xK-3xHtdpRZ1M8Qo-m9K4LDtH7FYU,2344
|
97
97
|
port_ocean/core/handlers/resync_state_updater/__init__.py,sha256=kG6y-JQGpPfuTHh912L_bctIDCzAK4DN-d00S7rguWU,81
|
@@ -102,7 +102,7 @@ port_ocean/core/integrations/mixins/__init__.py,sha256=FA1FEKMM6P-L2_m7Q4L20mFa4
|
|
102
102
|
port_ocean/core/integrations/mixins/events.py,sha256=0jKRsBw6lU8Mqs7MaQK4n-t_H6Z4NEkXZ5VWzqTrKEc,2396
|
103
103
|
port_ocean/core/integrations/mixins/handler.py,sha256=mZ7-0UlG3LcrwJttFbMe-R4xcOU2H_g33tZar7PwTv8,3771
|
104
104
|
port_ocean/core/integrations/mixins/sync.py,sha256=B9fEs8faaYLLikH9GBjE_E61vo0bQDjIGQsQ1SRXOlA,3931
|
105
|
-
port_ocean/core/integrations/mixins/sync_raw.py,sha256=
|
105
|
+
port_ocean/core/integrations/mixins/sync_raw.py,sha256=xDGz4pOfpd6G_pMQveEHQCDdOTk-Uun_aP0G_0IxsDA,24784
|
106
106
|
port_ocean/core/integrations/mixins/utils.py,sha256=oN4Okz6xlaefpid1_Pud8HPSw9BwwjRohyNsknq-Myg,2309
|
107
107
|
port_ocean/core/models.py,sha256=FvTp-BlpbvLbMbngE0wsiimsCfmIhUR1PvsE__Z--1I,2206
|
108
108
|
port_ocean/core/ocean_types.py,sha256=j_-or1VxDy22whLLxwxgzIsE4wAhFLH19Xff9l4oJA8,1124
|
@@ -110,7 +110,7 @@ port_ocean/core/utils/entity_topological_sorter.py,sha256=MDUjM6OuDy4Xj68o-7InNN
|
|
110
110
|
port_ocean/core/utils/utils.py,sha256=HmumOeH27N0NX1_OP3t4oGKt074ht9XyXhvfZ5I05s4,6474
|
111
111
|
port_ocean/debug_cli.py,sha256=gHrv-Ey3cImKOcGZpjoHlo4pa_zfmyOl6TUM4o9VtcA,96
|
112
112
|
port_ocean/exceptions/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
113
|
-
port_ocean/exceptions/api.py,sha256=
|
113
|
+
port_ocean/exceptions/api.py,sha256=1JcA-H12lhSgolMEA6dM4JMbDrh9sYDcE7oydPSTuK8,649
|
114
114
|
port_ocean/exceptions/base.py,sha256=uY4DX7fIITDFfemCJDWpaZi3bD51lcANc5swpoNvMJA,46
|
115
115
|
port_ocean/exceptions/clients.py,sha256=LKLLs-Zy3caNG85rwxfOw2rMr8qqVV6SHUq4fRCZ99U,180
|
116
116
|
port_ocean/exceptions/context.py,sha256=mA8HII6Rl4QxKUz98ppy1zX3kaziaen21h1ZWuU3ADc,372
|
@@ -135,7 +135,9 @@ port_ocean/tests/clients/port/mixins/test_organization_mixin.py,sha256=-8iHM33Oe
|
|
135
135
|
port_ocean/tests/conftest.py,sha256=JXASSS0IY0nnR6bxBflhzxS25kf4iNaABmThyZ0mZt8,101
|
136
136
|
port_ocean/tests/core/defaults/test_common.py,sha256=sR7RqB3ZYV6Xn6NIg-c8k5K6JcGsYZ2SCe_PYX5vLYM,5560
|
137
137
|
port_ocean/tests/core/handlers/entity_processor/test_jq_entity_processor.py,sha256=FnEnaDjuoAbKvKyv6xJ46n3j0ZcaT70Sg2zc7oy7HAA,13596
|
138
|
-
port_ocean/tests/core/handlers/mixins/test_sync_raw.py,sha256=
|
138
|
+
port_ocean/tests/core/handlers/mixins/test_sync_raw.py,sha256=gxQ4e9hQuMS8-o5UbiUSt1I1uaK0DCO3yCFDVigpZvo,31740
|
139
|
+
port_ocean/tests/core/handlers/port_app_config/test_api.py,sha256=eJZ6SuFBLz71y4ca3DNqKag6d6HUjNJS0aqQPwiLMTI,1999
|
140
|
+
port_ocean/tests/core/handlers/port_app_config/test_base.py,sha256=s3D98JP3YV9V6T5PCDPE2852gkqGiDmo03UyexESX_I,6872
|
139
141
|
port_ocean/tests/core/test_utils.py,sha256=Z3kdhb5V7Svhcyy3EansdTpgHL36TL6erNtU-OPwAcI,2647
|
140
142
|
port_ocean/tests/core/utils/test_entity_topological_sorter.py,sha256=zuq5WSPy_88PemG3mOUIHTxWMR_js1R7tOzUYlgBd68,3447
|
141
143
|
port_ocean/tests/core/utils/test_resolve_entities_diff.py,sha256=4kTey1c0dWKbLXjJ-9m2GXrHyAWZeLQ2asdtYRRUdRs,16573
|
@@ -160,8 +162,8 @@ port_ocean/utils/repeat.py,sha256=0EFWM9d8lLXAhZmAyczY20LAnijw6UbIECf5lpGbOas,32
|
|
160
162
|
port_ocean/utils/signal.py,sha256=K-6kKFQTltcmKDhtyZAcn0IMa3sUpOHGOAUdWKgx0_E,1369
|
161
163
|
port_ocean/utils/time.py,sha256=pufAOH5ZQI7gXvOvJoQXZXZJV-Dqktoj9Qp9eiRwmJ4,1939
|
162
164
|
port_ocean/version.py,sha256=UsuJdvdQlazzKGD3Hd5-U7N69STh8Dq9ggJzQFnu9fU,177
|
163
|
-
port_ocean-0.18.
|
164
|
-
port_ocean-0.18.
|
165
|
-
port_ocean-0.18.
|
166
|
-
port_ocean-0.18.
|
167
|
-
port_ocean-0.18.
|
165
|
+
port_ocean-0.18.6.dist-info/LICENSE.md,sha256=WNHhf_5RCaeuKWyq_K39vmp9F28LxKsB4SpomwSZ2L0,11357
|
166
|
+
port_ocean-0.18.6.dist-info/METADATA,sha256=71enLFwwGK7wlTo-ETXIVzGAtGQ5VHEFfs0y75kAtP0,6669
|
167
|
+
port_ocean-0.18.6.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
|
168
|
+
port_ocean-0.18.6.dist-info/entry_points.txt,sha256=F_DNUmGZU2Kme-8NsWM5LLE8piGMafYZygRYhOVtcjA,54
|
169
|
+
port_ocean-0.18.6.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|