-
Notifications
You must be signed in to change notification settings - Fork 63
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[Core] Raise error if integration gets empty config from port (#1353)
# Description What - raise error if integration gets empty config from port Why - to avoid integration to delete entities if resync gets 0 entities How - raise error ## Type of change Please leave one option from the following and delete the rest: - [ ] Bug fix (non-breaking change which fixes an issue) ### Core testing checklist - [ ] Integration able to create all default resources from scratch - [ ] Resync finishes successfully - [ ] Resync able to create entities - [ ] Resync able to update entities - [ ] Resync able to detect and delete entities - [ ] Scheduled resync able to abort existing resync and start a new one - [ ] Tested with at least 2 integrations from scratch - [ ] Tested with Kafka and Polling event listeners - [ ] Tested deletion of entities that don't pass the selector
- Loading branch information
Showing
7 changed files
with
290 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
67 changes: 67 additions & 0 deletions
67
port_ocean/tests/core/handlers/port_app_config/test_api.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
import pytest | ||
from unittest.mock import AsyncMock | ||
|
||
from port_ocean.core.handlers.port_app_config.api import APIPortAppConfig | ||
from port_ocean.exceptions.api import EmptyPortAppConfigError | ||
|
||
|
||
@pytest.fixture | ||
def mock_context() -> AsyncMock: | ||
context = AsyncMock() | ||
context.port_client.get_current_integration = AsyncMock() | ||
return context | ||
|
||
|
||
@pytest.fixture | ||
def api_config(mock_context: AsyncMock) -> APIPortAppConfig: | ||
return APIPortAppConfig(mock_context) | ||
|
||
|
||
async def test_get_port_app_config_valid_config_returns_config( | ||
api_config: APIPortAppConfig, mock_context: AsyncMock | ||
) -> None: | ||
# Arrange | ||
expected_config = {"key": "value"} | ||
mock_context.port_client.get_current_integration.return_value = { | ||
"config": expected_config | ||
} | ||
|
||
# Act | ||
result = await api_config._get_port_app_config() | ||
|
||
# Assert | ||
assert result == expected_config | ||
mock_context.port_client.get_current_integration.assert_called_once() | ||
|
||
|
||
async def test_get_port_app_config_empty_config_raises_value_error( | ||
api_config: APIPortAppConfig, mock_context: AsyncMock | ||
) -> None: | ||
# Arrange | ||
mock_context.port_client.get_current_integration.return_value = {"config": {}} | ||
|
||
# Act & Assert | ||
with pytest.raises(EmptyPortAppConfigError, match="Port app config is empty"): | ||
await api_config._get_port_app_config() | ||
|
||
|
||
async def test_get_port_app_config_missing_config_key_raises_key_error( | ||
api_config: APIPortAppConfig, mock_context: AsyncMock | ||
) -> None: | ||
# Arrange | ||
mock_context.port_client.get_current_integration.return_value = {} | ||
|
||
# Act & Assert | ||
with pytest.raises(KeyError): | ||
await api_config._get_port_app_config() | ||
|
||
|
||
async def test_get_port_app_config_empty_integration_raises_key_error( | ||
api_config: APIPortAppConfig, mock_context: AsyncMock | ||
) -> None: | ||
# Arrange | ||
mock_context.port_client.get_current_integration.return_value = {} | ||
|
||
# Act & Assert | ||
with pytest.raises(KeyError): | ||
await api_config._get_port_app_config() |
197 changes: 197 additions & 0 deletions
197
port_ocean/tests/core/handlers/port_app_config/test_base.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,197 @@ | ||
import pytest | ||
from unittest.mock import MagicMock | ||
from pydantic import ValidationError | ||
from typing import Any, Dict | ||
|
||
from port_ocean.context.ocean import PortOceanContext | ||
from port_ocean.core.handlers.port_app_config.base import BasePortAppConfig | ||
from port_ocean.core.handlers.port_app_config.models import PortAppConfig | ||
from port_ocean.context.event import EventType, event_context | ||
from port_ocean.exceptions.api import EmptyPortAppConfigError | ||
|
||
|
||
class TestPortAppConfig(BasePortAppConfig): | ||
mock_get_port_app_config: Any | ||
|
||
async def _get_port_app_config(self) -> Dict[str, Any]: | ||
return self.mock_get_port_app_config() | ||
|
||
|
||
@pytest.fixture | ||
def mock_context() -> PortOceanContext: | ||
context = MagicMock(spec=PortOceanContext) | ||
context.config.port.port_app_config_cache_ttl = 300 # 5 minutes | ||
return context | ||
|
||
|
||
@pytest.fixture | ||
def port_app_config_handler(mock_context: PortOceanContext) -> TestPortAppConfig: | ||
handler = TestPortAppConfig(mock_context) | ||
handler.mock_get_port_app_config = MagicMock() | ||
return handler | ||
|
||
|
||
@pytest.mark.asyncio | ||
async def test_get_port_app_config_success( | ||
port_app_config_handler: TestPortAppConfig, | ||
) -> None: | ||
# Arrange | ||
valid_config = { | ||
"resources": [ | ||
{ | ||
"kind": "repository", | ||
"selector": {"query": "true"}, | ||
"port": { | ||
"entity": { | ||
"mappings": { | ||
"identifier": ".name", | ||
"title": ".name", | ||
"blueprint": '"service"', | ||
"properties": { | ||
"description": ".description", | ||
"url": ".html_url", | ||
"defaultBranch": ".default_branch", | ||
}, | ||
} | ||
} | ||
}, | ||
} | ||
] | ||
} | ||
port_app_config_handler.mock_get_port_app_config.return_value = valid_config | ||
|
||
# Act | ||
async with event_context(EventType.RESYNC, trigger_type="machine"): | ||
result = await port_app_config_handler.get_port_app_config() | ||
|
||
# Assert | ||
assert isinstance(result, PortAppConfig) | ||
assert result.resources[0].port.entity.mappings.title == ".name" | ||
assert result.resources[0].port.entity.mappings.identifier == ".name" | ||
assert result.resources[0].port.entity.mappings.blueprint == '"service"' | ||
assert ( | ||
result.resources[0].port.entity.mappings.properties["description"] | ||
== ".description" | ||
) | ||
assert result.resources[0].port.entity.mappings.properties["url"] == ".html_url" | ||
assert ( | ||
result.resources[0].port.entity.mappings.properties["defaultBranch"] | ||
== ".default_branch" | ||
) | ||
port_app_config_handler.mock_get_port_app_config.assert_called_once() | ||
|
||
|
||
@pytest.mark.asyncio | ||
async def test_get_port_app_config_uses_cache( | ||
port_app_config_handler: TestPortAppConfig, | ||
) -> None: | ||
# Arrange | ||
valid_config = { | ||
"resources": [ | ||
{ | ||
"kind": "repository", | ||
"selector": {"query": "true"}, | ||
"port": { | ||
"entity": { | ||
"mappings": { | ||
"identifier": ".name", | ||
"title": ".name", | ||
"blueprint": '"service"', | ||
"properties": { | ||
"description": ".description", | ||
"url": ".html_url", | ||
"defaultBranch": ".default_branch", | ||
}, | ||
} | ||
} | ||
}, | ||
} | ||
] | ||
} | ||
port_app_config_handler.mock_get_port_app_config.return_value = valid_config | ||
|
||
# Act | ||
async with event_context(EventType.RESYNC, trigger_type="machine"): | ||
result1 = await port_app_config_handler.get_port_app_config() | ||
result2 = await port_app_config_handler.get_port_app_config() | ||
|
||
# Assert | ||
assert result1 == result2 | ||
port_app_config_handler.mock_get_port_app_config.assert_called_once() # Called only once due to caching | ||
|
||
|
||
@pytest.mark.asyncio | ||
async def test_get_port_app_config_bypass_cache( | ||
port_app_config_handler: TestPortAppConfig, | ||
) -> None: | ||
# Arrange | ||
valid_config = { | ||
"resources": [ | ||
{ | ||
"kind": "repository", | ||
"selector": {"query": "true"}, | ||
"port": { | ||
"entity": { | ||
"mappings": { | ||
"identifier": ".name", | ||
"title": ".name", | ||
"blueprint": '"service"', | ||
"properties": { | ||
"description": ".description", | ||
"url": ".html_url", | ||
"defaultBranch": ".default_branch", | ||
}, | ||
} | ||
} | ||
}, | ||
} | ||
] | ||
} | ||
port_app_config_handler.mock_get_port_app_config.return_value = valid_config | ||
|
||
# Act | ||
async with event_context(EventType.RESYNC, trigger_type="machine"): | ||
result1 = await port_app_config_handler.get_port_app_config() | ||
result2 = await port_app_config_handler.get_port_app_config(use_cache=False) | ||
|
||
# Assert | ||
assert result1 == result2 | ||
assert ( | ||
port_app_config_handler.mock_get_port_app_config.call_count == 2 | ||
) # Called twice due to cache bypass | ||
|
||
|
||
@pytest.mark.asyncio | ||
async def test_get_port_app_config_validation_error( | ||
port_app_config_handler: TestPortAppConfig, monkeypatch: pytest.MonkeyPatch | ||
) -> None: | ||
# Arrange | ||
invalid_config = {"invalid_field": "invalid_value"} | ||
port_app_config_handler.mock_get_port_app_config.return_value = invalid_config | ||
|
||
def mock_parse_obj(*args: Any, **kwargs: Any) -> None: | ||
raise ValidationError(errors=[], model=PortAppConfig) | ||
|
||
monkeypatch.setattr( | ||
port_app_config_handler.CONFIG_CLASS, "parse_obj", mock_parse_obj | ||
) | ||
|
||
# Act & Assert | ||
with pytest.raises(ValidationError): | ||
async with event_context(EventType.RESYNC, trigger_type="machine"): | ||
await port_app_config_handler.get_port_app_config() | ||
|
||
|
||
@pytest.mark.asyncio | ||
async def test_get_port_app_config_fetch_error( | ||
port_app_config_handler: TestPortAppConfig, | ||
) -> None: | ||
# Arrange | ||
port_app_config_handler.mock_get_port_app_config.side_effect = ( | ||
EmptyPortAppConfigError("Port app config is empty") | ||
) | ||
|
||
# Act & Assert | ||
async with event_context(EventType.RESYNC, trigger_type="machine"): | ||
with pytest.raises(EmptyPortAppConfigError, match="Port app config is empty"): | ||
await port_app_config_handler.get_port_app_config() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters