port-ocean 0.17.7__py3-none-any.whl → 0.18.0__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.

@@ -29,6 +29,8 @@ from port_ocean.core.models import Entity
29
29
  from port_ocean.context.event import EventContext, event_context, EventType
30
30
  from port_ocean.clients.port.types import UserAgentType
31
31
  from port_ocean.context.ocean import ocean
32
+ from dataclasses import dataclass
33
+ from typing import List, Optional
32
34
 
33
35
 
34
36
  @pytest.fixture
@@ -141,6 +143,26 @@ def mock_entity_processor(mock_context: PortOceanContext) -> JQEntityProcessor:
141
143
  return JQEntityProcessor(mock_context)
142
144
 
143
145
 
146
+ @pytest.fixture
147
+ def mock_resource_config() -> ResourceConfig:
148
+ resource = ResourceConfig(
149
+ kind="service",
150
+ selector=Selector(query="true"),
151
+ port=PortResourceConfig(
152
+ entity=MappingsConfig(
153
+ mappings=EntityMapping(
154
+ identifier=".id",
155
+ title=".name",
156
+ blueprint='"service"',
157
+ properties={"url": ".web_url"},
158
+ relations={},
159
+ )
160
+ )
161
+ ),
162
+ )
163
+ return resource
164
+
165
+
144
166
  @pytest.fixture
145
167
  def mock_entities_state_applier(
146
168
  mock_context: PortOceanContext,
@@ -410,7 +432,10 @@ async def test_sync_raw_mixin_dependency(
410
432
 
411
433
  @pytest.mark.asyncio
412
434
  async def test_register_raw(
413
- mock_sync_raw_mixin_with_jq_processor: SyncRawMixin, mock_ocean: Ocean
435
+ mock_sync_raw_mixin_with_jq_processor: SyncRawMixin,
436
+ mock_ocean: Ocean,
437
+ mock_context: PortOceanContext,
438
+ monkeypatch: pytest.MonkeyPatch,
414
439
  ) -> None:
415
440
  kind = "service"
416
441
  user_agent_type = UserAgentType.exporter
@@ -426,6 +451,9 @@ async def test_register_raw(
426
451
  },
427
452
  ]
428
453
 
454
+ # Set is_saas to False
455
+ monkeypatch.setattr(mock_context.app, "is_saas", lambda: False)
456
+
429
457
  async with event_context(EventType.HTTP_REQUEST, trigger_type="machine") as event:
430
458
  # Use patch to mock the method instead of direct assignment
431
459
  with patch.object(
@@ -488,7 +516,10 @@ async def test_register_raw(
488
516
 
489
517
  @pytest.mark.asyncio
490
518
  async def test_unregister_raw(
491
- mock_sync_raw_mixin_with_jq_processor: SyncRawMixin, mock_ocean: Ocean
519
+ mock_sync_raw_mixin_with_jq_processor: SyncRawMixin,
520
+ mock_ocean: Ocean,
521
+ mock_context: PortOceanContext,
522
+ monkeypatch: pytest.MonkeyPatch,
492
523
  ) -> None:
493
524
  kind = "service"
494
525
  user_agent_type = UserAgentType.exporter
@@ -504,6 +535,9 @@ async def test_unregister_raw(
504
535
  },
505
536
  ]
506
537
 
538
+ # Set is_saas to False
539
+ monkeypatch.setattr(mock_context.app, "is_saas", lambda: False)
540
+
507
541
  async with event_context(EventType.HTTP_REQUEST, trigger_type="machine") as event:
508
542
  # Use patch to mock the method instead of direct assignment
509
543
  with patch.object(
@@ -550,3 +584,276 @@ async def test_unregister_raw(
550
584
  assert entity.identifier == result["identifier"]
551
585
  assert entity.blueprint == result["blueprint"]
552
586
  assert entity.properties == result["properties"]
587
+
588
+
589
+ @pytest.mark.asyncio
590
+ async def test_map_entities_compared_with_port_no_port_entities_all_entities_are_mapped(
591
+ mock_sync_raw_mixin: SyncRawMixin,
592
+ mock_ocean: Ocean,
593
+ mock_resource_config: ResourceConfig,
594
+ ) -> None:
595
+ # Setup test data
596
+ entities = [
597
+ create_entity("entity_1", "service", {}, False),
598
+ create_entity("entity_2", "service", {}, False),
599
+ ]
600
+
601
+ # Mock port client to return empty list
602
+ mock_ocean.port_client.search_entities.return_value = [] # type: ignore
603
+
604
+ # Execute test
605
+ changed_entities = await mock_sync_raw_mixin._map_entities_compared_with_port(
606
+ entities, mock_resource_config, UserAgentType.exporter
607
+ )
608
+
609
+ # Verify results
610
+ assert len(changed_entities) == 2
611
+ assert "entity_1" in [e.identifier for e in changed_entities]
612
+ assert "entity_2" in [e.identifier for e in changed_entities]
613
+
614
+
615
+ @pytest.mark.asyncio
616
+ async def test_map_entities_compared_with_port_with_existing_entities_only_changed_third_party_entities_are_mapped(
617
+ mock_sync_raw_mixin: SyncRawMixin,
618
+ mock_ocean: Ocean,
619
+ mock_resource_config: ResourceConfig,
620
+ ) -> None:
621
+ # Setup test data
622
+ third_party_entities = [
623
+ create_entity(
624
+ "entity_1", "service", {"service": "entity_2"}, False
625
+ ), # Should be in changed (modified)
626
+ create_entity("entity_2", "service", {}, False), # Should be in changed (new)
627
+ ]
628
+ port_entities = [
629
+ create_entity("entity_1", "service", {}, False), # Existing but different props
630
+ create_entity("entity_3", "service", {}, False), # Should be in irrelevant
631
+ ]
632
+
633
+ # Mock port client to return our port entities
634
+ mock_ocean.port_client.search_entities.return_value = port_entities # type: ignore
635
+
636
+ changed_entities = await mock_sync_raw_mixin._map_entities_compared_with_port(
637
+ third_party_entities, mock_resource_config, UserAgentType.exporter
638
+ )
639
+
640
+ # Verify results
641
+ assert len(changed_entities) == 2
642
+ assert "entity_1" in [e.identifier for e in changed_entities]
643
+ assert "entity_2" in [e.identifier for e in changed_entities]
644
+
645
+
646
+ @pytest.mark.asyncio
647
+ async def test_map_entities_compared_with_port_with_multiple_batches_all_batches_are_being_proccessed_to_map(
648
+ mock_sync_raw_mixin: SyncRawMixin,
649
+ mock_ocean: Ocean,
650
+ mock_resource_config: ResourceConfig,
651
+ ) -> None:
652
+ # Setup test data with 75 entities (should create 2 batches)
653
+ third_party_entities = [
654
+ create_entity(
655
+ f"entity_{i}", "service", {"service_relation": f"service_{i}"}, False
656
+ )
657
+ for i in range(75)
658
+ ]
659
+ port_entities_batch1 = [
660
+ create_entity(f"entity_{i}", "service", {}, False) for i in range(50)
661
+ ]
662
+ port_entities_batch2 = [
663
+ create_entity(f"entity_{i}", "service", {}, False) for i in range(50, 75)
664
+ ]
665
+
666
+ # Mock port client to return our port entities in batches
667
+ with patch.object(
668
+ mock_ocean.port_client,
669
+ "search_entities",
670
+ new_callable=AsyncMock,
671
+ side_effect=[port_entities_batch1, port_entities_batch2],
672
+ ) as mock_search_entities:
673
+ # Mock resolve_entities_diff to return all entities
674
+ with patch(
675
+ "port_ocean.core.integrations.mixins.sync_raw.resolve_entities_diff",
676
+ return_value=third_party_entities,
677
+ ) as mock_resolve_entities_diff:
678
+ # Execute test
679
+ changed_entities = (
680
+ await mock_sync_raw_mixin._map_entities_compared_with_port(
681
+ third_party_entities, mock_resource_config, UserAgentType.exporter
682
+ )
683
+ )
684
+
685
+ # Verify results
686
+ assert len(changed_entities) == 75
687
+ assert [e.identifier for e in changed_entities] == [
688
+ f"entity_{i}" for i in range(75)
689
+ ]
690
+ assert (
691
+ mock_search_entities.call_count == 2
692
+ ) # Verify two batch calls were made
693
+ assert (
694
+ mock_resolve_entities_diff.call_count == 1
695
+ ) # Verify final diff was calculated once
696
+
697
+
698
+ @dataclass
699
+ class EntitySelectorDiff:
700
+ passed: List[Entity]
701
+ failed: List[Entity]
702
+
703
+ def _replace(self, **kwargs: Any) -> "EntitySelectorDiff":
704
+ return EntitySelectorDiff(
705
+ **{
706
+ "passed": kwargs.get("passed", self.passed),
707
+ "failed": kwargs.get("failed", self.failed),
708
+ }
709
+ )
710
+
711
+
712
+ @dataclass
713
+ class CalculationResult:
714
+ entity_selector_diff: EntitySelectorDiff
715
+ errors: List[Any]
716
+ misconfigurations: List[Any]
717
+ misonfigured_entity_keys: Optional[List[Any]] = None
718
+
719
+
720
+ @pytest.mark.asyncio
721
+ async def test_register_resource_raw_saas_no_changes_upsert_not_called_entitiy_is_returned(
722
+ mock_sync_raw_mixin: SyncRawMixin,
723
+ mock_port_app_config: PortAppConfig,
724
+ mock_context: PortOceanContext,
725
+ monkeypatch: pytest.MonkeyPatch,
726
+ ) -> None:
727
+ # Mock ocean.app.is_saas()
728
+ monkeypatch.setattr(mock_context.app, "is_saas", lambda: True)
729
+
730
+ # Mock dependencies
731
+ entity = Entity(identifier="1", blueprint="service")
732
+ mock_sync_raw_mixin._calculate_raw = AsyncMock(return_value=[CalculationResult(entity_selector_diff=EntitySelectorDiff(passed=[entity], failed=[]), errors=[], misconfigurations=[], misonfigured_entity_keys=[])]) # type: ignore
733
+ mock_sync_raw_mixin._map_entities_compared_with_port = AsyncMock(return_value=([])) # type: ignore
734
+ mock_sync_raw_mixin.entities_state_applier.upsert = AsyncMock() # type: ignore
735
+
736
+ async with event_context(EventType.RESYNC, trigger_type="machine") as event:
737
+ event.port_app_config = mock_port_app_config
738
+
739
+ # Test execution
740
+ result = await mock_sync_raw_mixin._register_resource_raw(
741
+ mock_port_app_config.resources[0], # Use the first resource from the config
742
+ [{"some": "data"}],
743
+ UserAgentType.exporter,
744
+ )
745
+
746
+ # Assertions
747
+ assert len(result.entity_selector_diff.passed) == 1
748
+ mock_sync_raw_mixin._calculate_raw.assert_called_once()
749
+ mock_sync_raw_mixin.entities_state_applier.upsert.assert_not_called()
750
+ mock_sync_raw_mixin._map_entities_compared_with_port.assert_called_once()
751
+
752
+
753
+ @pytest.mark.asyncio
754
+ async def test_register_resource_raw_saas_with_changes_upsert_called_and_entities_are_mapped(
755
+ mock_sync_raw_mixin: SyncRawMixin,
756
+ mock_port_app_config: PortAppConfig,
757
+ mock_context: PortOceanContext,
758
+ monkeypatch: pytest.MonkeyPatch,
759
+ ) -> None:
760
+ # Mock ocean.app.is_saas()
761
+ monkeypatch.setattr(mock_context.app, "is_saas", lambda: True)
762
+
763
+ # Mock dependencies
764
+ entity = Entity(identifier="1", blueprint="service")
765
+ mock_sync_raw_mixin._calculate_raw = AsyncMock(return_value=[CalculationResult(entity_selector_diff=EntitySelectorDiff(passed=[entity], failed=[]), errors=[], misconfigurations=[], misonfigured_entity_keys=[])]) # type: ignore
766
+ mock_sync_raw_mixin._map_entities_compared_with_port = AsyncMock(return_value=([entity])) # type: ignore
767
+ mock_sync_raw_mixin.entities_state_applier.upsert = AsyncMock(return_value=[entity]) # type: ignore
768
+
769
+ async with event_context(EventType.RESYNC, trigger_type="machine") as event:
770
+ event.port_app_config = mock_port_app_config
771
+
772
+ # Test execution
773
+ result = await mock_sync_raw_mixin._register_resource_raw(
774
+ mock_port_app_config.resources[0],
775
+ [{"some": "data"}],
776
+ UserAgentType.exporter,
777
+ )
778
+
779
+ # Assertions
780
+ assert len(result.entity_selector_diff.passed) == 1
781
+ mock_sync_raw_mixin._calculate_raw.assert_called_once()
782
+ mock_sync_raw_mixin.entities_state_applier.upsert.assert_called_once()
783
+ mock_sync_raw_mixin._map_entities_compared_with_port.assert_called_once()
784
+
785
+
786
+ @pytest.mark.asyncio
787
+ async def test_register_resource_raw_non_saas_upsert_called_and_no_entitites_diff_calculation(
788
+ mock_sync_raw_mixin: SyncRawMixin,
789
+ mock_port_app_config: PortAppConfig,
790
+ mock_context: PortOceanContext,
791
+ monkeypatch: pytest.MonkeyPatch,
792
+ ) -> None:
793
+ # Mock ocean.app.is_saas()
794
+ monkeypatch.setattr(mock_context.app, "is_saas", lambda: False)
795
+
796
+ # Mock dependencies
797
+ entity = Entity(identifier="1", blueprint="service")
798
+ calculation_result = CalculationResult(
799
+ entity_selector_diff=EntitySelectorDiff(passed=[entity], failed=[]),
800
+ errors=[],
801
+ misconfigurations=[],
802
+ misonfigured_entity_keys=[],
803
+ )
804
+ mock_sync_raw_mixin._calculate_raw = AsyncMock(return_value=[calculation_result]) # type: ignore
805
+ mock_sync_raw_mixin._map_entities_compared_with_port = AsyncMock() # type: ignore
806
+ mock_sync_raw_mixin.entities_state_applier.upsert = AsyncMock(return_value=[entity]) # type: ignore
807
+
808
+ async with event_context(EventType.RESYNC, trigger_type="machine") as event:
809
+ event.port_app_config = mock_port_app_config
810
+
811
+ # Test execution
812
+ result = await mock_sync_raw_mixin._register_resource_raw(
813
+ mock_port_app_config.resources[0],
814
+ [{"some": "data"}],
815
+ UserAgentType.exporter,
816
+ )
817
+
818
+ # Assertions
819
+ assert len(result.entity_selector_diff.passed) == 1
820
+ mock_sync_raw_mixin._calculate_raw.assert_called_once()
821
+ mock_sync_raw_mixin._map_entities_compared_with_port.assert_not_called()
822
+ mock_sync_raw_mixin.entities_state_applier.upsert.assert_called_once()
823
+
824
+
825
+ @pytest.mark.asyncio
826
+ async def test_register_resource_raw_saas_with_errors(
827
+ mock_sync_raw_mixin: SyncRawMixin,
828
+ mock_port_app_config: PortAppConfig,
829
+ mock_context: PortOceanContext,
830
+ monkeypatch: pytest.MonkeyPatch,
831
+ ) -> None:
832
+ # Mock ocean.app.is_saas()
833
+ monkeypatch.setattr(mock_context.app, "is_saas", lambda: True)
834
+
835
+ # Mock dependencies
836
+ failed_entity = Entity(identifier="1", blueprint="service")
837
+ error = Exception("Test error")
838
+ 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
839
+ mock_sync_raw_mixin._map_entities_compared_with_port = AsyncMock(return_value=([])) # type: ignore
840
+ mock_sync_raw_mixin.entities_state_applier.upsert = AsyncMock() # type: ignore
841
+
842
+ async with event_context(EventType.RESYNC, trigger_type="machine") as event:
843
+ event.port_app_config = mock_port_app_config
844
+
845
+ # Test execution
846
+ result = await mock_sync_raw_mixin._register_resource_raw(
847
+ mock_port_app_config.resources[0],
848
+ [{"some": "data"}],
849
+ UserAgentType.exporter,
850
+ )
851
+
852
+ # Assertions
853
+ assert len(result.entity_selector_diff.passed) == 0
854
+ assert len(result.entity_selector_diff.failed) == 1
855
+ assert len(result.errors) == 1
856
+ assert result.errors[0] == error
857
+ mock_sync_raw_mixin._calculate_raw.assert_called_once()
858
+ mock_sync_raw_mixin._map_entities_compared_with_port.assert_called_once()
859
+ mock_sync_raw_mixin.entities_state_applier.upsert.assert_not_called()