port-ocean 0.5.5__py3-none-any.whl → 0.17.8__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.
Potentially problematic release.
This version of port-ocean might be problematic. Click here for more details.
- integrations/_infra/Dockerfile.Deb +56 -0
- integrations/_infra/Dockerfile.alpine +108 -0
- integrations/_infra/Dockerfile.base.builder +26 -0
- integrations/_infra/Dockerfile.base.runner +13 -0
- integrations/_infra/Dockerfile.dockerignore +94 -0
- {port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}} → integrations/_infra}/Makefile +21 -8
- integrations/_infra/grpcio.sh +18 -0
- integrations/_infra/init.sh +5 -0
- port_ocean/bootstrap.py +1 -1
- port_ocean/cli/commands/defaults/clean.py +3 -1
- port_ocean/cli/commands/new.py +42 -7
- port_ocean/cli/commands/sail.py +7 -1
- port_ocean/cli/cookiecutter/cookiecutter.json +3 -0
- port_ocean/cli/cookiecutter/hooks/post_gen_project.py +20 -3
- port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/.env.example +6 -0
- port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/.port/resources/blueprints.json +41 -0
- port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/.port/resources/port-app-config.yml +16 -0
- port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/.port/spec.yaml +6 -7
- port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/CHANGELOG.md +1 -1
- port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/CONTRIBUTING.md +7 -0
- port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/changelog/.gitignore +1 -0
- port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/main.py +16 -1
- port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/pyproject.toml +21 -10
- port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/tests/test_sample.py +2 -0
- port_ocean/clients/port/authentication.py +16 -4
- port_ocean/clients/port/client.py +17 -0
- port_ocean/clients/port/mixins/blueprints.py +7 -8
- port_ocean/clients/port/mixins/entities.py +108 -53
- port_ocean/clients/port/mixins/integrations.py +23 -34
- port_ocean/clients/port/retry_transport.py +0 -5
- port_ocean/clients/port/utils.py +9 -3
- port_ocean/config/base.py +16 -16
- port_ocean/config/dynamic.py +2 -0
- port_ocean/config/settings.py +79 -11
- port_ocean/context/event.py +18 -5
- port_ocean/context/ocean.py +14 -3
- port_ocean/core/defaults/clean.py +10 -3
- port_ocean/core/defaults/common.py +25 -9
- port_ocean/core/defaults/initialize.py +111 -100
- port_ocean/core/event_listener/__init__.py +8 -0
- port_ocean/core/event_listener/base.py +49 -10
- port_ocean/core/event_listener/factory.py +9 -1
- port_ocean/core/event_listener/http.py +11 -3
- port_ocean/core/event_listener/kafka.py +24 -5
- port_ocean/core/event_listener/once.py +96 -4
- port_ocean/core/event_listener/polling.py +16 -14
- port_ocean/core/event_listener/webhooks_only.py +41 -0
- port_ocean/core/handlers/__init__.py +1 -2
- port_ocean/core/handlers/entities_state_applier/base.py +4 -1
- port_ocean/core/handlers/entities_state_applier/port/applier.py +29 -87
- port_ocean/core/handlers/entities_state_applier/port/order_by_entities_dependencies.py +5 -2
- port_ocean/core/handlers/entity_processor/base.py +26 -22
- port_ocean/core/handlers/entity_processor/jq_entity_processor.py +253 -45
- port_ocean/core/handlers/port_app_config/base.py +55 -15
- port_ocean/core/handlers/port_app_config/models.py +24 -5
- port_ocean/core/handlers/resync_state_updater/__init__.py +5 -0
- port_ocean/core/handlers/resync_state_updater/updater.py +84 -0
- port_ocean/core/integrations/base.py +5 -7
- port_ocean/core/integrations/mixins/events.py +3 -1
- port_ocean/core/integrations/mixins/sync.py +4 -2
- port_ocean/core/integrations/mixins/sync_raw.py +209 -74
- port_ocean/core/integrations/mixins/utils.py +1 -1
- port_ocean/core/models.py +44 -0
- port_ocean/core/ocean_types.py +29 -11
- port_ocean/core/utils/entity_topological_sorter.py +90 -0
- port_ocean/core/utils/utils.py +109 -0
- port_ocean/debug_cli.py +5 -0
- port_ocean/exceptions/core.py +4 -0
- port_ocean/exceptions/port_defaults.py +0 -2
- port_ocean/helpers/retry.py +85 -24
- port_ocean/log/handlers.py +23 -2
- port_ocean/log/logger_setup.py +8 -1
- port_ocean/log/sensetive.py +25 -10
- port_ocean/middlewares.py +10 -2
- port_ocean/ocean.py +57 -24
- port_ocean/run.py +10 -5
- port_ocean/tests/__init__.py +0 -0
- port_ocean/tests/clients/port/mixins/test_entities.py +53 -0
- port_ocean/tests/conftest.py +4 -0
- port_ocean/tests/core/defaults/test_common.py +166 -0
- port_ocean/tests/core/handlers/entity_processor/test_jq_entity_processor.py +350 -0
- port_ocean/tests/core/handlers/mixins/test_sync_raw.py +552 -0
- port_ocean/tests/core/test_utils.py +73 -0
- port_ocean/tests/core/utils/test_entity_topological_sorter.py +99 -0
- port_ocean/tests/helpers/__init__.py +0 -0
- port_ocean/tests/helpers/fake_port_api.py +191 -0
- port_ocean/tests/helpers/fixtures.py +46 -0
- port_ocean/tests/helpers/integration.py +31 -0
- port_ocean/tests/helpers/ocean_app.py +66 -0
- port_ocean/tests/helpers/port_client.py +21 -0
- port_ocean/tests/helpers/smoke_test.py +82 -0
- port_ocean/tests/log/test_handlers.py +71 -0
- port_ocean/tests/test_smoke.py +74 -0
- port_ocean/tests/utils/test_async_iterators.py +45 -0
- port_ocean/tests/utils/test_cache.py +189 -0
- port_ocean/utils/async_iterators.py +109 -0
- port_ocean/utils/cache.py +37 -1
- port_ocean/utils/misc.py +22 -4
- port_ocean/utils/queue_utils.py +88 -0
- port_ocean/utils/signal.py +1 -4
- port_ocean/utils/time.py +54 -0
- {port_ocean-0.5.5.dist-info → port_ocean-0.17.8.dist-info}/METADATA +27 -19
- port_ocean-0.17.8.dist-info/RECORD +164 -0
- {port_ocean-0.5.5.dist-info → port_ocean-0.17.8.dist-info}/WHEEL +1 -1
- port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/.dockerignore +0 -94
- port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/Dockerfile +0 -15
- port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/config.yaml +0 -17
- port_ocean/core/handlers/entities_state_applier/port/validate_entity_relations.py +0 -40
- port_ocean/core/utils.py +0 -65
- port_ocean-0.5.5.dist-info/RECORD +0 -129
- {port_ocean-0.5.5.dist-info → port_ocean-0.17.8.dist-info}/LICENSE.md +0 -0
- {port_ocean-0.5.5.dist-info → port_ocean-0.17.8.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,552 @@
|
|
|
1
|
+
from contextlib import asynccontextmanager
|
|
2
|
+
from graphlib import CycleError
|
|
3
|
+
from typing import Any, AsyncGenerator
|
|
4
|
+
|
|
5
|
+
from httpx import Response
|
|
6
|
+
from port_ocean.clients.port.client import PortClient
|
|
7
|
+
from port_ocean.core.utils.entity_topological_sorter import EntityTopologicalSorter
|
|
8
|
+
from port_ocean.exceptions.core import OceanAbortException
|
|
9
|
+
import pytest
|
|
10
|
+
from unittest.mock import MagicMock, AsyncMock, patch
|
|
11
|
+
from port_ocean.ocean import Ocean
|
|
12
|
+
from port_ocean.context.ocean import PortOceanContext
|
|
13
|
+
from port_ocean.core.handlers.port_app_config.models import (
|
|
14
|
+
EntityMapping,
|
|
15
|
+
MappingsConfig,
|
|
16
|
+
PortAppConfig,
|
|
17
|
+
PortResourceConfig,
|
|
18
|
+
ResourceConfig,
|
|
19
|
+
Selector,
|
|
20
|
+
)
|
|
21
|
+
from port_ocean.core.integrations.mixins import SyncRawMixin
|
|
22
|
+
from port_ocean.core.handlers.entities_state_applier.port.applier import (
|
|
23
|
+
HttpEntitiesStateApplier,
|
|
24
|
+
)
|
|
25
|
+
from port_ocean.core.handlers.entity_processor.jq_entity_processor import (
|
|
26
|
+
JQEntityProcessor,
|
|
27
|
+
)
|
|
28
|
+
from port_ocean.core.models import Entity
|
|
29
|
+
from port_ocean.context.event import EventContext, event_context, EventType
|
|
30
|
+
from port_ocean.clients.port.types import UserAgentType
|
|
31
|
+
from port_ocean.context.ocean import ocean
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@pytest.fixture
|
|
35
|
+
def mock_port_client(mock_http_client: MagicMock) -> PortClient:
|
|
36
|
+
mock_port_client = PortClient(
|
|
37
|
+
MagicMock(), MagicMock(), MagicMock(), MagicMock(), MagicMock(), MagicMock()
|
|
38
|
+
)
|
|
39
|
+
mock_port_client.auth = AsyncMock()
|
|
40
|
+
mock_port_client.auth.headers = AsyncMock(
|
|
41
|
+
return_value={
|
|
42
|
+
"Authorization": "test",
|
|
43
|
+
"User-Agent": "test",
|
|
44
|
+
}
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
mock_port_client.search_entities = AsyncMock(return_value=[]) # type: ignore
|
|
48
|
+
mock_port_client.client = mock_http_client
|
|
49
|
+
return mock_port_client
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@pytest.fixture
|
|
53
|
+
def mock_http_client() -> MagicMock:
|
|
54
|
+
mock_http_client = MagicMock()
|
|
55
|
+
mock_upserted_entities = []
|
|
56
|
+
|
|
57
|
+
async def post(url: str, *args: Any, **kwargs: Any) -> Response:
|
|
58
|
+
entity = kwargs.get("json", {})
|
|
59
|
+
if entity.get("properties", {}).get("mock_is_to_fail", {}):
|
|
60
|
+
return Response(
|
|
61
|
+
404, headers=MagicMock(), json={"ok": False, "error": "not_found"}
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
mock_upserted_entities.append(
|
|
65
|
+
f"{entity.get('identifier')}-{entity.get('blueprint')}"
|
|
66
|
+
)
|
|
67
|
+
return Response(
|
|
68
|
+
200,
|
|
69
|
+
json={
|
|
70
|
+
"entity": {
|
|
71
|
+
"identifier": entity.get("identifier"),
|
|
72
|
+
"blueprint": entity.get("blueprint"),
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
mock_http_client.post = AsyncMock(side_effect=post)
|
|
78
|
+
return mock_http_client
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@pytest.fixture
|
|
82
|
+
def mock_ocean(mock_port_client: PortClient) -> Ocean:
|
|
83
|
+
with patch("port_ocean.ocean.Ocean.__init__", return_value=None):
|
|
84
|
+
ocean_mock = Ocean(
|
|
85
|
+
MagicMock(), MagicMock(), MagicMock(), MagicMock(), MagicMock()
|
|
86
|
+
)
|
|
87
|
+
ocean_mock.config = MagicMock()
|
|
88
|
+
ocean_mock.config.port = MagicMock()
|
|
89
|
+
ocean_mock.config.port.port_app_config_cache_ttl = 60
|
|
90
|
+
ocean_mock.port_client = mock_port_client
|
|
91
|
+
|
|
92
|
+
return ocean_mock
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
@pytest.fixture
|
|
96
|
+
def mock_context(mock_ocean: Ocean) -> PortOceanContext:
|
|
97
|
+
context = PortOceanContext(mock_ocean)
|
|
98
|
+
ocean._app = context.app
|
|
99
|
+
return context
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
@pytest.fixture
|
|
103
|
+
def mock_port_app_config() -> PortAppConfig:
|
|
104
|
+
return PortAppConfig(
|
|
105
|
+
enable_merge_entity=True,
|
|
106
|
+
delete_dependent_entities=True,
|
|
107
|
+
create_missing_related_entities=False,
|
|
108
|
+
resources=[
|
|
109
|
+
ResourceConfig(
|
|
110
|
+
kind="project",
|
|
111
|
+
selector=Selector(query="true"),
|
|
112
|
+
port=PortResourceConfig(
|
|
113
|
+
entity=MappingsConfig(
|
|
114
|
+
mappings=EntityMapping(
|
|
115
|
+
identifier=".id | tostring",
|
|
116
|
+
title=".name",
|
|
117
|
+
blueprint='"service"',
|
|
118
|
+
properties={"url": ".web_url"},
|
|
119
|
+
relations={},
|
|
120
|
+
)
|
|
121
|
+
)
|
|
122
|
+
),
|
|
123
|
+
)
|
|
124
|
+
],
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
@pytest.fixture
|
|
129
|
+
def mock_port_app_config_handler(mock_port_app_config: PortAppConfig) -> MagicMock:
|
|
130
|
+
handler = MagicMock()
|
|
131
|
+
|
|
132
|
+
async def get_config(use_cache: bool = True) -> Any:
|
|
133
|
+
return mock_port_app_config
|
|
134
|
+
|
|
135
|
+
handler.get_port_app_config = get_config
|
|
136
|
+
return handler
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
@pytest.fixture
|
|
140
|
+
def mock_entity_processor(mock_context: PortOceanContext) -> JQEntityProcessor:
|
|
141
|
+
return JQEntityProcessor(mock_context)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
@pytest.fixture
|
|
145
|
+
def mock_entities_state_applier(
|
|
146
|
+
mock_context: PortOceanContext,
|
|
147
|
+
) -> HttpEntitiesStateApplier:
|
|
148
|
+
return HttpEntitiesStateApplier(mock_context)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
@pytest.fixture
|
|
152
|
+
def mock_sync_raw_mixin(
|
|
153
|
+
mock_entity_processor: JQEntityProcessor,
|
|
154
|
+
mock_entities_state_applier: HttpEntitiesStateApplier,
|
|
155
|
+
mock_port_app_config_handler: MagicMock,
|
|
156
|
+
) -> SyncRawMixin:
|
|
157
|
+
sync_raw_mixin = SyncRawMixin()
|
|
158
|
+
sync_raw_mixin._entity_processor = mock_entity_processor
|
|
159
|
+
sync_raw_mixin._entities_state_applier = mock_entities_state_applier
|
|
160
|
+
sync_raw_mixin._port_app_config_handler = mock_port_app_config_handler
|
|
161
|
+
sync_raw_mixin._get_resource_raw_results = AsyncMock(return_value=([{}], [])) # type: ignore
|
|
162
|
+
sync_raw_mixin._entity_processor.parse_items = AsyncMock(return_value=MagicMock()) # type: ignore
|
|
163
|
+
|
|
164
|
+
return sync_raw_mixin
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
@pytest.fixture
|
|
168
|
+
def mock_sync_raw_mixin_with_jq_processor(
|
|
169
|
+
mock_sync_raw_mixin: SyncRawMixin,
|
|
170
|
+
) -> SyncRawMixin:
|
|
171
|
+
mock_sync_raw_mixin._entity_processor = JQEntityProcessor(mock_context) # type: ignore
|
|
172
|
+
return mock_sync_raw_mixin
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
@asynccontextmanager
|
|
176
|
+
async def no_op_event_context(
|
|
177
|
+
existing_event: EventContext,
|
|
178
|
+
) -> AsyncGenerator[EventContext, None]:
|
|
179
|
+
yield existing_event
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def create_entity(
|
|
183
|
+
id: str, blueprint: str, relation: dict[str, str], is_to_fail: bool
|
|
184
|
+
) -> Entity:
|
|
185
|
+
entity = Entity(identifier=id, blueprint=blueprint)
|
|
186
|
+
entity.relations = relation
|
|
187
|
+
entity.properties = {"mock_is_to_fail": is_to_fail}
|
|
188
|
+
return entity
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
@pytest.mark.asyncio
|
|
192
|
+
async def test_sync_raw_mixin_self_dependency(
|
|
193
|
+
mock_sync_raw_mixin: SyncRawMixin,
|
|
194
|
+
) -> None:
|
|
195
|
+
entities_params = [
|
|
196
|
+
("entity_1", "service", {"service": "entity_1"}, True),
|
|
197
|
+
("entity_2", "service", {"service": "entity_2"}, False),
|
|
198
|
+
]
|
|
199
|
+
entities = [create_entity(*entity_param) for entity_param in entities_params]
|
|
200
|
+
|
|
201
|
+
calc_result_mock = MagicMock()
|
|
202
|
+
calc_result_mock.entity_selector_diff.passed = entities
|
|
203
|
+
calc_result_mock.errors = []
|
|
204
|
+
|
|
205
|
+
mock_sync_raw_mixin.entity_processor.parse_items = AsyncMock(return_value=calc_result_mock) # type: ignore
|
|
206
|
+
|
|
207
|
+
mock_order_by_entities_dependencies = MagicMock(
|
|
208
|
+
side_effect=EntityTopologicalSorter.order_by_entities_dependencies
|
|
209
|
+
)
|
|
210
|
+
async with event_context(EventType.RESYNC, trigger_type="machine") as event:
|
|
211
|
+
app_config = (
|
|
212
|
+
await mock_sync_raw_mixin.port_app_config_handler.get_port_app_config(
|
|
213
|
+
use_cache=False
|
|
214
|
+
)
|
|
215
|
+
)
|
|
216
|
+
event.port_app_config = app_config
|
|
217
|
+
event.entity_topological_sorter.register_entity = MagicMock(side_effect=event.entity_topological_sorter.register_entity) # type: ignore
|
|
218
|
+
event.entity_topological_sorter.get_entities = MagicMock(side_effect=event.entity_topological_sorter.get_entities) # type: ignore
|
|
219
|
+
|
|
220
|
+
with patch(
|
|
221
|
+
"port_ocean.core.integrations.mixins.sync_raw.event_context",
|
|
222
|
+
lambda *args, **kwargs: no_op_event_context(event),
|
|
223
|
+
):
|
|
224
|
+
with patch(
|
|
225
|
+
"port_ocean.core.utils.entity_topological_sorter.EntityTopologicalSorter.order_by_entities_dependencies",
|
|
226
|
+
mock_order_by_entities_dependencies,
|
|
227
|
+
):
|
|
228
|
+
|
|
229
|
+
await mock_sync_raw_mixin.sync_raw_all(
|
|
230
|
+
trigger_type="machine", user_agent_type=UserAgentType.exporter
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
assert (
|
|
234
|
+
len(event.entity_topological_sorter.entities) == 1
|
|
235
|
+
), "Expected one failed entity callback due to retry logic"
|
|
236
|
+
assert event.entity_topological_sorter.register_entity.call_count == 1
|
|
237
|
+
assert event.entity_topological_sorter.get_entities.call_count == 1
|
|
238
|
+
|
|
239
|
+
assert mock_order_by_entities_dependencies.call_count == 1
|
|
240
|
+
assert [
|
|
241
|
+
call[0][0][0]
|
|
242
|
+
for call in mock_order_by_entities_dependencies.call_args_list
|
|
243
|
+
] == [entity for entity in entities if entity.identifier == "entity_1"]
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
@pytest.mark.asyncio
|
|
247
|
+
async def test_sync_raw_mixin_circular_dependency(
|
|
248
|
+
mock_sync_raw_mixin: SyncRawMixin, mock_ocean: Ocean
|
|
249
|
+
) -> None:
|
|
250
|
+
entities_params = [
|
|
251
|
+
("entity_1", "service", {"service": "entity_2"}, True),
|
|
252
|
+
("entity_2", "service", {"service": "entity_1"}, True),
|
|
253
|
+
]
|
|
254
|
+
entities = [create_entity(*entity_param) for entity_param in entities_params]
|
|
255
|
+
|
|
256
|
+
calc_result_mock = MagicMock()
|
|
257
|
+
calc_result_mock.entity_selector_diff.passed = entities
|
|
258
|
+
calc_result_mock.errors = []
|
|
259
|
+
|
|
260
|
+
mock_sync_raw_mixin.entity_processor.parse_items = AsyncMock(return_value=calc_result_mock) # type: ignore
|
|
261
|
+
|
|
262
|
+
mock_order_by_entities_dependencies = MagicMock(
|
|
263
|
+
side_effect=EntityTopologicalSorter.order_by_entities_dependencies
|
|
264
|
+
)
|
|
265
|
+
async with event_context(EventType.RESYNC, trigger_type="machine") as event:
|
|
266
|
+
app_config = (
|
|
267
|
+
await mock_sync_raw_mixin.port_app_config_handler.get_port_app_config(
|
|
268
|
+
use_cache=False
|
|
269
|
+
)
|
|
270
|
+
)
|
|
271
|
+
event.port_app_config = app_config
|
|
272
|
+
org = event.entity_topological_sorter.register_entity
|
|
273
|
+
|
|
274
|
+
def mock_register_entity(*args: Any, **kwargs: Any) -> Any:
|
|
275
|
+
entity = args[0]
|
|
276
|
+
entity.properties["mock_is_to_fail"] = False
|
|
277
|
+
return org(*args, **kwargs)
|
|
278
|
+
|
|
279
|
+
event.entity_topological_sorter.register_entity = MagicMock(side_effect=mock_register_entity) # type: ignore
|
|
280
|
+
raiesed_error_handle_failed = []
|
|
281
|
+
org_get_entities = event.entity_topological_sorter.get_entities
|
|
282
|
+
|
|
283
|
+
def handle_failed_wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
284
|
+
try:
|
|
285
|
+
return list(org_get_entities(*args, **kwargs))
|
|
286
|
+
except Exception as e:
|
|
287
|
+
raiesed_error_handle_failed.append(e)
|
|
288
|
+
raise e
|
|
289
|
+
|
|
290
|
+
event.entity_topological_sorter.get_entities = MagicMock(side_effect=lambda *args, **kwargs: handle_failed_wrapper(*args, **kwargs)) # type: ignore
|
|
291
|
+
|
|
292
|
+
with patch(
|
|
293
|
+
"port_ocean.core.integrations.mixins.sync_raw.event_context",
|
|
294
|
+
lambda *args, **kwargs: no_op_event_context(event),
|
|
295
|
+
):
|
|
296
|
+
with patch(
|
|
297
|
+
"port_ocean.core.utils.entity_topological_sorter.EntityTopologicalSorter.order_by_entities_dependencies",
|
|
298
|
+
mock_order_by_entities_dependencies,
|
|
299
|
+
):
|
|
300
|
+
|
|
301
|
+
await mock_sync_raw_mixin.sync_raw_all(
|
|
302
|
+
trigger_type="machine", user_agent_type=UserAgentType.exporter
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
assert (
|
|
306
|
+
len(event.entity_topological_sorter.entities) == 2
|
|
307
|
+
), "Expected one failed entity callback due to retry logic"
|
|
308
|
+
assert event.entity_topological_sorter.register_entity.call_count == 2
|
|
309
|
+
assert event.entity_topological_sorter.get_entities.call_count == 2
|
|
310
|
+
assert [
|
|
311
|
+
call[0]
|
|
312
|
+
for call in event.entity_topological_sorter.get_entities.call_args_list
|
|
313
|
+
] == [(), (False,)]
|
|
314
|
+
assert len(raiesed_error_handle_failed) == 1
|
|
315
|
+
assert isinstance(raiesed_error_handle_failed[0], OceanAbortException)
|
|
316
|
+
assert isinstance(raiesed_error_handle_failed[0].__cause__, CycleError)
|
|
317
|
+
assert (
|
|
318
|
+
len(mock_ocean.port_client.client.post.call_args_list) # type: ignore
|
|
319
|
+
/ len(entities)
|
|
320
|
+
== 2
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
@pytest.mark.asyncio
|
|
325
|
+
async def test_sync_raw_mixin_dependency(
|
|
326
|
+
mock_sync_raw_mixin: SyncRawMixin, mock_ocean: Ocean
|
|
327
|
+
) -> None:
|
|
328
|
+
entities_params = [
|
|
329
|
+
("entity_1", "service", {"service": "entity_3"}, True),
|
|
330
|
+
("entity_2", "service", {"service": "entity_4"}, True),
|
|
331
|
+
("entity_3", "service", {"service": ""}, True),
|
|
332
|
+
("entity_4", "service", {"service": "entity_3"}, True),
|
|
333
|
+
("entity_5", "service", {"service": "entity_1"}, True),
|
|
334
|
+
]
|
|
335
|
+
entities = [create_entity(*entity_param) for entity_param in entities_params]
|
|
336
|
+
|
|
337
|
+
calc_result_mock = MagicMock()
|
|
338
|
+
calc_result_mock.entity_selector_diff.passed = entities
|
|
339
|
+
calc_result_mock.errors = []
|
|
340
|
+
|
|
341
|
+
mock_sync_raw_mixin.entity_processor.parse_items = AsyncMock(return_value=calc_result_mock) # type: ignore
|
|
342
|
+
|
|
343
|
+
mock_order_by_entities_dependencies = MagicMock(
|
|
344
|
+
side_effect=EntityTopologicalSorter.order_by_entities_dependencies
|
|
345
|
+
)
|
|
346
|
+
async with event_context(EventType.RESYNC, trigger_type="machine") as event:
|
|
347
|
+
app_config = (
|
|
348
|
+
await mock_sync_raw_mixin.port_app_config_handler.get_port_app_config(
|
|
349
|
+
use_cache=False
|
|
350
|
+
)
|
|
351
|
+
)
|
|
352
|
+
event.port_app_config = app_config
|
|
353
|
+
org = event.entity_topological_sorter.register_entity
|
|
354
|
+
|
|
355
|
+
def mock_register_entity(*args: Any, **kwargs: Any) -> None:
|
|
356
|
+
entity = args[0]
|
|
357
|
+
entity.properties["mock_is_to_fail"] = False
|
|
358
|
+
return org(*args, **kwargs)
|
|
359
|
+
|
|
360
|
+
event.entity_topological_sorter.register_entity = MagicMock(side_effect=mock_register_entity) # type: ignore
|
|
361
|
+
raiesed_error_handle_failed = []
|
|
362
|
+
org_event_get_entities = event.entity_topological_sorter.get_entities
|
|
363
|
+
|
|
364
|
+
def get_entities_wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
365
|
+
try:
|
|
366
|
+
return org_event_get_entities(*args, **kwargs)
|
|
367
|
+
except Exception as e:
|
|
368
|
+
raiesed_error_handle_failed.append(e)
|
|
369
|
+
raise e
|
|
370
|
+
|
|
371
|
+
event.entity_topological_sorter.get_entities = MagicMock(side_effect=lambda *args, **kwargs: get_entities_wrapper(*args, **kwargs)) # type: ignore
|
|
372
|
+
|
|
373
|
+
with patch(
|
|
374
|
+
"port_ocean.core.integrations.mixins.sync_raw.event_context",
|
|
375
|
+
lambda *args, **kwargs: no_op_event_context(event),
|
|
376
|
+
):
|
|
377
|
+
with patch(
|
|
378
|
+
"port_ocean.core.utils.entity_topological_sorter.EntityTopologicalSorter.order_by_entities_dependencies",
|
|
379
|
+
mock_order_by_entities_dependencies,
|
|
380
|
+
):
|
|
381
|
+
|
|
382
|
+
await mock_sync_raw_mixin.sync_raw_all(
|
|
383
|
+
trigger_type="machine", user_agent_type=UserAgentType.exporter
|
|
384
|
+
)
|
|
385
|
+
|
|
386
|
+
assert event.entity_topological_sorter.register_entity.call_count == 5
|
|
387
|
+
assert (
|
|
388
|
+
len(event.entity_topological_sorter.entities) == 5
|
|
389
|
+
), "Expected one failed entity callback due to retry logic"
|
|
390
|
+
assert event.entity_topological_sorter.get_entities.call_count == 1
|
|
391
|
+
assert len(raiesed_error_handle_failed) == 0
|
|
392
|
+
assert mock_ocean.port_client.client.post.call_count == 10 # type: ignore
|
|
393
|
+
assert mock_order_by_entities_dependencies.call_count == 1
|
|
394
|
+
|
|
395
|
+
first = mock_ocean.port_client.client.post.call_args_list[0:5] # type: ignore
|
|
396
|
+
second = mock_ocean.port_client.client.post.call_args_list[5:10] # type: ignore
|
|
397
|
+
|
|
398
|
+
assert "-".join(
|
|
399
|
+
[call[1].get("json").get("identifier") for call in first]
|
|
400
|
+
) == "-".join([entity.identifier for entity in entities])
|
|
401
|
+
assert "-".join(
|
|
402
|
+
[call[1].get("json").get("identifier") for call in second]
|
|
403
|
+
) in (
|
|
404
|
+
"entity_3-entity_4-entity_1-entity_2-entity_5",
|
|
405
|
+
"entity_3-entity_4-entity_1-entity_5-entity_2",
|
|
406
|
+
"entity_3-entity_1-entity_4-entity_2-entity_5",
|
|
407
|
+
"entity_3-entity_1-entity_4-entity_5-entity_2",
|
|
408
|
+
)
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
@pytest.mark.asyncio
|
|
412
|
+
async def test_register_raw(
|
|
413
|
+
mock_sync_raw_mixin_with_jq_processor: SyncRawMixin, mock_ocean: Ocean
|
|
414
|
+
) -> None:
|
|
415
|
+
kind = "service"
|
|
416
|
+
user_agent_type = UserAgentType.exporter
|
|
417
|
+
raw_entity = [
|
|
418
|
+
{"id": "entity_1", "name": "entity_1", "web_url": "https://example.com"},
|
|
419
|
+
]
|
|
420
|
+
expected_result = [
|
|
421
|
+
{
|
|
422
|
+
"identifier": "entity_1",
|
|
423
|
+
"blueprint": "service",
|
|
424
|
+
"name": "entity_1",
|
|
425
|
+
"properties": {"url": "https://example.com"},
|
|
426
|
+
},
|
|
427
|
+
]
|
|
428
|
+
|
|
429
|
+
async with event_context(EventType.HTTP_REQUEST, trigger_type="machine") as event:
|
|
430
|
+
# Use patch to mock the method instead of direct assignment
|
|
431
|
+
with patch.object(
|
|
432
|
+
mock_sync_raw_mixin_with_jq_processor.port_app_config_handler,
|
|
433
|
+
"get_port_app_config",
|
|
434
|
+
return_value=PortAppConfig(
|
|
435
|
+
enable_merge_entity=True,
|
|
436
|
+
delete_dependent_entities=True,
|
|
437
|
+
create_missing_related_entities=False,
|
|
438
|
+
resources=[
|
|
439
|
+
ResourceConfig(
|
|
440
|
+
kind=kind,
|
|
441
|
+
selector=Selector(query="true"),
|
|
442
|
+
port=PortResourceConfig(
|
|
443
|
+
entity=MappingsConfig(
|
|
444
|
+
mappings=EntityMapping(
|
|
445
|
+
identifier=".id | tostring",
|
|
446
|
+
title=".name",
|
|
447
|
+
blueprint='"service"',
|
|
448
|
+
properties={"url": ".web_url"},
|
|
449
|
+
relations={},
|
|
450
|
+
)
|
|
451
|
+
)
|
|
452
|
+
),
|
|
453
|
+
)
|
|
454
|
+
],
|
|
455
|
+
),
|
|
456
|
+
):
|
|
457
|
+
# Ensure the event.port_app_config is set correctly
|
|
458
|
+
event.port_app_config = await mock_sync_raw_mixin_with_jq_processor.port_app_config_handler.get_port_app_config(
|
|
459
|
+
use_cache=False
|
|
460
|
+
)
|
|
461
|
+
|
|
462
|
+
def upsert_side_effect(
|
|
463
|
+
entities: list[Entity], user_agent_type: UserAgentType
|
|
464
|
+
) -> list[Entity]:
|
|
465
|
+
# Simulate returning the passed entities
|
|
466
|
+
return entities
|
|
467
|
+
|
|
468
|
+
# Patch the upsert method with the side effect
|
|
469
|
+
with patch.object(
|
|
470
|
+
mock_sync_raw_mixin_with_jq_processor.entities_state_applier,
|
|
471
|
+
"upsert",
|
|
472
|
+
side_effect=upsert_side_effect,
|
|
473
|
+
):
|
|
474
|
+
# Call the register_raw method
|
|
475
|
+
registered_entities = (
|
|
476
|
+
await mock_sync_raw_mixin_with_jq_processor.register_raw(
|
|
477
|
+
kind, raw_entity, user_agent_type
|
|
478
|
+
)
|
|
479
|
+
)
|
|
480
|
+
|
|
481
|
+
# Assert that the registered entities match the expected results
|
|
482
|
+
assert len(registered_entities) == len(expected_result)
|
|
483
|
+
for entity, result in zip(registered_entities, expected_result):
|
|
484
|
+
assert entity.identifier == result["identifier"]
|
|
485
|
+
assert entity.blueprint == result["blueprint"]
|
|
486
|
+
assert entity.properties == result["properties"]
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
@pytest.mark.asyncio
|
|
490
|
+
async def test_unregister_raw(
|
|
491
|
+
mock_sync_raw_mixin_with_jq_processor: SyncRawMixin, mock_ocean: Ocean
|
|
492
|
+
) -> None:
|
|
493
|
+
kind = "service"
|
|
494
|
+
user_agent_type = UserAgentType.exporter
|
|
495
|
+
raw_entity = [
|
|
496
|
+
{"id": "entity_1", "name": "entity_1", "web_url": "https://example.com"},
|
|
497
|
+
]
|
|
498
|
+
expected_result = [
|
|
499
|
+
{
|
|
500
|
+
"identifier": "entity_1",
|
|
501
|
+
"blueprint": "service",
|
|
502
|
+
"name": "entity_1",
|
|
503
|
+
"properties": {"url": "https://example.com"},
|
|
504
|
+
},
|
|
505
|
+
]
|
|
506
|
+
|
|
507
|
+
async with event_context(EventType.HTTP_REQUEST, trigger_type="machine") as event:
|
|
508
|
+
# Use patch to mock the method instead of direct assignment
|
|
509
|
+
with patch.object(
|
|
510
|
+
mock_sync_raw_mixin_with_jq_processor.port_app_config_handler,
|
|
511
|
+
"get_port_app_config",
|
|
512
|
+
return_value=PortAppConfig(
|
|
513
|
+
enable_merge_entity=True,
|
|
514
|
+
delete_dependent_entities=True,
|
|
515
|
+
create_missing_related_entities=False,
|
|
516
|
+
resources=[
|
|
517
|
+
ResourceConfig(
|
|
518
|
+
kind=kind,
|
|
519
|
+
selector=Selector(query="true"),
|
|
520
|
+
port=PortResourceConfig(
|
|
521
|
+
entity=MappingsConfig(
|
|
522
|
+
mappings=EntityMapping(
|
|
523
|
+
identifier=".id | tostring",
|
|
524
|
+
title=".name",
|
|
525
|
+
blueprint='"service"',
|
|
526
|
+
properties={"url": ".web_url"},
|
|
527
|
+
relations={},
|
|
528
|
+
)
|
|
529
|
+
)
|
|
530
|
+
),
|
|
531
|
+
)
|
|
532
|
+
],
|
|
533
|
+
),
|
|
534
|
+
):
|
|
535
|
+
# Ensure the event.port_app_config is set correctly
|
|
536
|
+
event.port_app_config = await mock_sync_raw_mixin_with_jq_processor.port_app_config_handler.get_port_app_config(
|
|
537
|
+
use_cache=False
|
|
538
|
+
)
|
|
539
|
+
|
|
540
|
+
# Call the unregister_raw method
|
|
541
|
+
unregistered_entities = (
|
|
542
|
+
await mock_sync_raw_mixin_with_jq_processor.unregister_raw(
|
|
543
|
+
kind, raw_entity, user_agent_type
|
|
544
|
+
)
|
|
545
|
+
)
|
|
546
|
+
|
|
547
|
+
# Assert that the unregistered entities match the expected results
|
|
548
|
+
assert len(unregistered_entities) == len(expected_result)
|
|
549
|
+
for entity, result in zip(unregistered_entities, expected_result):
|
|
550
|
+
assert entity.identifier == result["identifier"]
|
|
551
|
+
assert entity.blueprint == result["blueprint"]
|
|
552
|
+
assert entity.properties == result["properties"]
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
from unittest.mock import AsyncMock, patch
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from port_ocean.core.utils.utils import validate_integration_runtime
|
|
6
|
+
from port_ocean.clients.port.client import PortClient
|
|
7
|
+
from port_ocean.core.models import Runtime
|
|
8
|
+
from port_ocean.tests.helpers.port_client import get_port_client_for_integration
|
|
9
|
+
from port_ocean.exceptions.core import IntegrationRuntimeException
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class TestValidateIntegrationRuntime:
|
|
13
|
+
|
|
14
|
+
@pytest.mark.asyncio
|
|
15
|
+
@pytest.mark.parametrize(
|
|
16
|
+
"requested_runtime, installation_type, should_raise",
|
|
17
|
+
[
|
|
18
|
+
(Runtime.Saas, "Saas", False),
|
|
19
|
+
(Runtime.Saas, "SaasOauth2", False),
|
|
20
|
+
(Runtime.Saas, "OnPrem", True),
|
|
21
|
+
(Runtime.OnPrem, "OnPrem", False),
|
|
22
|
+
(Runtime.OnPrem, "SaasOauth2", True),
|
|
23
|
+
],
|
|
24
|
+
)
|
|
25
|
+
@patch.object(PortClient, "get_current_integration", new_callable=AsyncMock)
|
|
26
|
+
async def test_validate_integration_runtime(
|
|
27
|
+
self,
|
|
28
|
+
mock_get_current_integration: AsyncMock,
|
|
29
|
+
requested_runtime: Runtime,
|
|
30
|
+
installation_type: str,
|
|
31
|
+
should_raise: bool,
|
|
32
|
+
) -> None:
|
|
33
|
+
# Arrange
|
|
34
|
+
port_client = get_port_client_for_integration(
|
|
35
|
+
client_id="mock-client-id",
|
|
36
|
+
client_secret="mock-client-secret",
|
|
37
|
+
integration_identifier="mock-integration-identifier",
|
|
38
|
+
integration_type="mock-integration-type",
|
|
39
|
+
integration_version="mock-integration-version",
|
|
40
|
+
base_url="mock-base-url",
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
# Mock the return value of get_current_integration
|
|
44
|
+
mock_get_current_integration.return_value = {
|
|
45
|
+
"installationType": installation_type
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
# Act & Assert
|
|
49
|
+
if should_raise:
|
|
50
|
+
with pytest.raises(IntegrationRuntimeException):
|
|
51
|
+
await validate_integration_runtime(port_client, requested_runtime)
|
|
52
|
+
else:
|
|
53
|
+
await validate_integration_runtime(port_client, requested_runtime)
|
|
54
|
+
|
|
55
|
+
# Verify that get_current_integration was called once
|
|
56
|
+
mock_get_current_integration.assert_called_once()
|
|
57
|
+
|
|
58
|
+
@pytest.mark.parametrize(
|
|
59
|
+
"requested_runtime, installation_type, expected",
|
|
60
|
+
[
|
|
61
|
+
(Runtime.Saas, "SaasOauth2", True),
|
|
62
|
+
(Runtime.Saas, "OnPrem", False),
|
|
63
|
+
(Runtime.OnPrem, "OnPrem", True),
|
|
64
|
+
(Runtime.OnPrem, "SaasCloud", False),
|
|
65
|
+
],
|
|
66
|
+
)
|
|
67
|
+
def test_runtime_installation_compatibility(
|
|
68
|
+
self, requested_runtime: Runtime, installation_type: str, expected: bool
|
|
69
|
+
) -> None:
|
|
70
|
+
assert (
|
|
71
|
+
requested_runtime.is_installation_type_compatible(installation_type)
|
|
72
|
+
== expected
|
|
73
|
+
)
|