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.

Files changed (112) hide show
  1. integrations/_infra/Dockerfile.Deb +56 -0
  2. integrations/_infra/Dockerfile.alpine +108 -0
  3. integrations/_infra/Dockerfile.base.builder +26 -0
  4. integrations/_infra/Dockerfile.base.runner +13 -0
  5. integrations/_infra/Dockerfile.dockerignore +94 -0
  6. {port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}} → integrations/_infra}/Makefile +21 -8
  7. integrations/_infra/grpcio.sh +18 -0
  8. integrations/_infra/init.sh +5 -0
  9. port_ocean/bootstrap.py +1 -1
  10. port_ocean/cli/commands/defaults/clean.py +3 -1
  11. port_ocean/cli/commands/new.py +42 -7
  12. port_ocean/cli/commands/sail.py +7 -1
  13. port_ocean/cli/cookiecutter/cookiecutter.json +3 -0
  14. port_ocean/cli/cookiecutter/hooks/post_gen_project.py +20 -3
  15. port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/.env.example +6 -0
  16. port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/.port/resources/blueprints.json +41 -0
  17. port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/.port/resources/port-app-config.yml +16 -0
  18. port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/.port/spec.yaml +6 -7
  19. port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/CHANGELOG.md +1 -1
  20. port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/CONTRIBUTING.md +7 -0
  21. port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/changelog/.gitignore +1 -0
  22. port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/main.py +16 -1
  23. port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/pyproject.toml +21 -10
  24. port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/tests/test_sample.py +2 -0
  25. port_ocean/clients/port/authentication.py +16 -4
  26. port_ocean/clients/port/client.py +17 -0
  27. port_ocean/clients/port/mixins/blueprints.py +7 -8
  28. port_ocean/clients/port/mixins/entities.py +108 -53
  29. port_ocean/clients/port/mixins/integrations.py +23 -34
  30. port_ocean/clients/port/retry_transport.py +0 -5
  31. port_ocean/clients/port/utils.py +9 -3
  32. port_ocean/config/base.py +16 -16
  33. port_ocean/config/dynamic.py +2 -0
  34. port_ocean/config/settings.py +79 -11
  35. port_ocean/context/event.py +18 -5
  36. port_ocean/context/ocean.py +14 -3
  37. port_ocean/core/defaults/clean.py +10 -3
  38. port_ocean/core/defaults/common.py +25 -9
  39. port_ocean/core/defaults/initialize.py +111 -100
  40. port_ocean/core/event_listener/__init__.py +8 -0
  41. port_ocean/core/event_listener/base.py +49 -10
  42. port_ocean/core/event_listener/factory.py +9 -1
  43. port_ocean/core/event_listener/http.py +11 -3
  44. port_ocean/core/event_listener/kafka.py +24 -5
  45. port_ocean/core/event_listener/once.py +96 -4
  46. port_ocean/core/event_listener/polling.py +16 -14
  47. port_ocean/core/event_listener/webhooks_only.py +41 -0
  48. port_ocean/core/handlers/__init__.py +1 -2
  49. port_ocean/core/handlers/entities_state_applier/base.py +4 -1
  50. port_ocean/core/handlers/entities_state_applier/port/applier.py +29 -87
  51. port_ocean/core/handlers/entities_state_applier/port/order_by_entities_dependencies.py +5 -2
  52. port_ocean/core/handlers/entity_processor/base.py +26 -22
  53. port_ocean/core/handlers/entity_processor/jq_entity_processor.py +253 -45
  54. port_ocean/core/handlers/port_app_config/base.py +55 -15
  55. port_ocean/core/handlers/port_app_config/models.py +24 -5
  56. port_ocean/core/handlers/resync_state_updater/__init__.py +5 -0
  57. port_ocean/core/handlers/resync_state_updater/updater.py +84 -0
  58. port_ocean/core/integrations/base.py +5 -7
  59. port_ocean/core/integrations/mixins/events.py +3 -1
  60. port_ocean/core/integrations/mixins/sync.py +4 -2
  61. port_ocean/core/integrations/mixins/sync_raw.py +209 -74
  62. port_ocean/core/integrations/mixins/utils.py +1 -1
  63. port_ocean/core/models.py +44 -0
  64. port_ocean/core/ocean_types.py +29 -11
  65. port_ocean/core/utils/entity_topological_sorter.py +90 -0
  66. port_ocean/core/utils/utils.py +109 -0
  67. port_ocean/debug_cli.py +5 -0
  68. port_ocean/exceptions/core.py +4 -0
  69. port_ocean/exceptions/port_defaults.py +0 -2
  70. port_ocean/helpers/retry.py +85 -24
  71. port_ocean/log/handlers.py +23 -2
  72. port_ocean/log/logger_setup.py +8 -1
  73. port_ocean/log/sensetive.py +25 -10
  74. port_ocean/middlewares.py +10 -2
  75. port_ocean/ocean.py +57 -24
  76. port_ocean/run.py +10 -5
  77. port_ocean/tests/__init__.py +0 -0
  78. port_ocean/tests/clients/port/mixins/test_entities.py +53 -0
  79. port_ocean/tests/conftest.py +4 -0
  80. port_ocean/tests/core/defaults/test_common.py +166 -0
  81. port_ocean/tests/core/handlers/entity_processor/test_jq_entity_processor.py +350 -0
  82. port_ocean/tests/core/handlers/mixins/test_sync_raw.py +552 -0
  83. port_ocean/tests/core/test_utils.py +73 -0
  84. port_ocean/tests/core/utils/test_entity_topological_sorter.py +99 -0
  85. port_ocean/tests/helpers/__init__.py +0 -0
  86. port_ocean/tests/helpers/fake_port_api.py +191 -0
  87. port_ocean/tests/helpers/fixtures.py +46 -0
  88. port_ocean/tests/helpers/integration.py +31 -0
  89. port_ocean/tests/helpers/ocean_app.py +66 -0
  90. port_ocean/tests/helpers/port_client.py +21 -0
  91. port_ocean/tests/helpers/smoke_test.py +82 -0
  92. port_ocean/tests/log/test_handlers.py +71 -0
  93. port_ocean/tests/test_smoke.py +74 -0
  94. port_ocean/tests/utils/test_async_iterators.py +45 -0
  95. port_ocean/tests/utils/test_cache.py +189 -0
  96. port_ocean/utils/async_iterators.py +109 -0
  97. port_ocean/utils/cache.py +37 -1
  98. port_ocean/utils/misc.py +22 -4
  99. port_ocean/utils/queue_utils.py +88 -0
  100. port_ocean/utils/signal.py +1 -4
  101. port_ocean/utils/time.py +54 -0
  102. {port_ocean-0.5.5.dist-info → port_ocean-0.17.8.dist-info}/METADATA +27 -19
  103. port_ocean-0.17.8.dist-info/RECORD +164 -0
  104. {port_ocean-0.5.5.dist-info → port_ocean-0.17.8.dist-info}/WHEEL +1 -1
  105. port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/.dockerignore +0 -94
  106. port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/Dockerfile +0 -15
  107. port_ocean/cli/cookiecutter/{{cookiecutter.integration_slug}}/config.yaml +0 -17
  108. port_ocean/core/handlers/entities_state_applier/port/validate_entity_relations.py +0 -40
  109. port_ocean/core/utils.py +0 -65
  110. port_ocean-0.5.5.dist-info/RECORD +0 -129
  111. {port_ocean-0.5.5.dist-info → port_ocean-0.17.8.dist-info}/LICENSE.md +0 -0
  112. {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
+ )