port-ocean 0.18.4__py3-none-any.whl → 0.18.6__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- port_ocean/context/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
|