Skip to content

Commit

Permalink
Add smoke test using fake integration
Browse files Browse the repository at this point in the history
Also separate test helpers to fixtures and ocean app helpers
  • Loading branch information
erikzaadi committed Sep 16, 2024
1 parent 5a9a57f commit b89ebba
Show file tree
Hide file tree
Showing 7 changed files with 336 additions and 81 deletions.
38 changes: 37 additions & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,37 @@ jobs:
working-directory: ${{ matrix.folder != '.' && format('integrations/{0}', matrix.folder) || '.' }}
run: |
make install
- name: Test
# Core only actions
- name: Build core for smoke test
if: ${{ matrix.folder == '.' }}
working-directory: ${{ matrix.folder != '.' && format('integrations/{0}', matrix.folder) || '.' }}
run: |
make build
- name: Run fake integration for core test
working-directory: ${{ matrix.folder != '.' && format('integrations/{0}', matrix.folder) || '.' }}
if: ${{ matrix.folder == '.' }}
env:
PORT_CLIENT_ID: ${{ secrets.PORT_CLIENT_ID }}
PORT_CLIENT_SECRET: ${{ secrets.PORT_CLIENT_SECRET }}
PORT_BASE_URL: ${{ secrets.PORT_BASE_URL }}
SMOKE_TEST_SUFFIX: ${{ github.run_id }}
run: |
./scripts/run-smoke-test.sh
- name: Test Core
if: ${{ matrix.folder == '.' }}
working-directory: ${{ matrix.folder != '.' && format('integrations/{0}', matrix.folder) || '.' }}
env:
PYTEST_ADDOPTS: --junitxml=junit/test-results-${{ matrix.folder != '.' && format('integrations/{0}', matrix.folder) || 'ocean/core' }}.xml
PORT_CLIENT_ID: ${{ secrets.PORT_CLIENT_ID }}
PORT_CLIENT_SECRET: ${{ secrets.PORT_CLIENT_SECRET }}
PORT_BASE_URL: ${{ secrets.PORT_BASE_URL }}
SMOKE_TEST_SUFFIX: ${{ github.run_id }}
run: |
make test
- name: Install current core for all integrations
working-directory: ${{ matrix.folder != '.' && format('integrations/{0}', matrix.folder) || '.' }}
if: ${{ matrix.folder == '.' }}
Expand All @@ -46,6 +71,17 @@ jobs:
run: |
echo "Testing all integrations with local core"
SCRIPT_TO_RUN="PYTEST_ADDOPTS=--junitxml=${PWD}/junit/test-results-core-change/\`pwd | xargs basename\`.xml make test" make execute/all
# Integration step
- name: Test Integration ${{ matrix.folder }}
if: ${{ matrix.folder != '.' }}
working-directory: ${{ matrix.folder != '.' && format('integrations/{0}', matrix.folder) || '.' }}
env:
PYTEST_ADDOPTS: --junitxml=junit/test-results-${{ matrix.folder != '.' && format('integrations/{0}', matrix.folder) || 'ocean/core' }}.xml
run: |
make test
# Generic
- name: Publish Test Report
uses: mikepenz/action-junit-report@v4
if: ${{ always() }}
Expand Down
4 changes: 4 additions & 0 deletions port_ocean/tests/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# ruff: noqa
from port_ocean.tests.helpers.fixtures import (
port_client_for_fake_integration,
)
80 changes: 0 additions & 80 deletions port_ocean/tests/helpers/__init__.py
Original file line number Diff line number Diff line change
@@ -1,80 +0,0 @@
import sys
from inspect import getmembers
from pathlib import Path
from typing import Dict, List, Set, Tuple, Union

from yaml import safe_load

from port_ocean.bootstrap import create_default_app
from port_ocean.core.handlers.port_app_config.models import ResourceConfig
from port_ocean.core.ocean_types import RESYNC_RESULT
from port_ocean.ocean import Ocean
from port_ocean.utils.misc import get_spec_file, load_module


def get_integration_ocean_app(integration_path: str) -> Ocean:
spec_file = get_spec_file(Path(integration_path))

config_factory = None if not spec_file else spec_file.get("configurations", [])

default_app = create_default_app(
integration_path,
config_factory,
{
"port": {
"client_id": "bla",
"client_secret": "bla",
},
},
)
main_path = f"{integration_path}/main.py"
sys.path.append(integration_path)
app_module = load_module(main_path)
app: Ocean = {name: item for name, item in getmembers(app_module)}.get(
"app", default_app
)

return app


def get_integation_resource_configs(integration_path: str) -> List[ResourceConfig]:
with open(
f"{integration_path}/.port/resources/port-app-config.yml"
) as port_app_config_file:
resource_configs = safe_load(port_app_config_file)

return [ResourceConfig(**item) for item in resource_configs["resources"]]


def get_integation_resource_config_by_name(
integration_path: str, kind: str
) -> Union[ResourceConfig, None]:
resource_configs = get_integation_resource_configs(integration_path)

relevant_configs = [x for x in resource_configs if x.kind == kind]

return relevant_configs[0] if len(relevant_configs) else None


async def get_raw_result_on_integration_sync_kinds(
integration_path: str, override_kinds: Union[Set[str], None] = None
) -> Dict[str, List[Tuple[RESYNC_RESULT, List[Exception]]]]:
app = get_integration_ocean_app(integration_path)

resource_configs = get_integation_resource_configs(integration_path)

if override_kinds:
resource_configs = [x for x in resource_configs if x.kind in override_kinds]

results: Dict[str, List[Tuple[RESYNC_RESULT, List[Exception]]]] = {}

for resource_config in resource_configs:
resource_result = await app.integration._get_resource_raw_results(
resource_config
)

results[resource_config.kind] = results.get(resource_config.kind, []) + [
resource_result
]

return results
110 changes: 110 additions & 0 deletions port_ocean/tests/helpers/fixtures.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
from os import environ, path
from typing import Any, AsyncGenerator, Callable, List, Tuple, Union

import pytest_asyncio
from pydantic import BaseModel

from port_ocean.clients.port.client import PortClient
from port_ocean.core.handlers.port_app_config.models import ResourceConfig
from port_ocean.ocean import Ocean
from port_ocean.tests.helpers.ocean_app import (
get_integation_resource_configs,
get_integration_ocean_app,
)


def get_port_client_for_integration(
client_id: str,
client_secret: str,
integration_identifier: str,
integration_type: str,
integration_version: str,
base_url: Union[str, None],
) -> PortClient:
return PortClient(
base_url=base_url or "https://api.getport/io",
client_id=client_id,
client_secret=client_secret,
integration_identifier=integration_identifier,
integration_type=integration_type,
integration_version=integration_version,
)


async def cleanup_integration(client: PortClient, blueprints: List[str]) -> None:
for blueprint in blueprints:
bp = await client.get_blueprint(blueprint)
if bp is not None:
migration_id = await client.delete_blueprint(
identifier=blueprint, delete_entities=True
)
if migration_id:
await client.wait_for_migration_to_complete(migration_id=migration_id)
headers = await client.auth.headers()
await client.client.delete(f"{client.auth.api_url}/integrations", headers=headers)


class SmokeTestDetails(BaseModel):
integration_identifier: str
blueprint_department: str
blueprint_person: str


@pytest_asyncio.fixture()
async def port_client_for_fake_integration() -> (
AsyncGenerator[Tuple[SmokeTestDetails, PortClient], None]
):
blueprint_department = "fake-department"
blueprint_person = "fake-person"
integration_identifier = "smoke-test-integration"
smoke_test_suffix = environ.get("SMOKE_TEST_SUFFIX")
client_id = environ.get("PORT_CLIENT_ID")
client_secret = environ.get("PORT_CLIENT_SECRET")

if not client_secret or not client_id:
assert False, "Missing port credentials"

base_url = environ.get("PORT_BASE_URL")
integration_version = "0.1.1-dev"
integration_type = "smoke-test"
if smoke_test_suffix is not None:
integration_identifier = f"{integration_identifier}-{smoke_test_suffix}"
blueprint_person = f"{blueprint_person}-{smoke_test_suffix}"
blueprint_department = f"{blueprint_department}-{smoke_test_suffix}"

client = get_port_client_for_integration(
client_id,
client_secret,
integration_identifier,
integration_type,
integration_version,
base_url,
)

smoke_test_details = SmokeTestDetails(
integration_identifier=integration_identifier,
blueprint_person=blueprint_person,
blueprint_department=blueprint_department,
)
yield smoke_test_details, client
await cleanup_integration(client, [blueprint_department, blueprint_person])


@pytest_asyncio.fixture
def get_mocked_ocean_app(request: Any) -> Callable[[], Ocean]:
test_dir = path.join(path.dirname(request.module.__file__), "..")

def get_ocean_app() -> Ocean:
return get_integration_ocean_app(test_dir)

return get_ocean_app


@pytest_asyncio.fixture
def get_mock_ocean_resource_configs(request: Any) -> Callable[[], List[ResourceConfig]]:
module_dir = path.join(path.dirname(request.module.__file__), "..")

def get_ocean_resource_configs() -> List[ResourceConfig]:
return get_integation_resource_configs(module_dir)

return get_ocean_resource_configs
54 changes: 54 additions & 0 deletions port_ocean/tests/helpers/ocean_app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import sys
from inspect import getmembers
from pathlib import Path
from typing import List, Tuple

from yaml import safe_load

from port_ocean.bootstrap import create_default_app
from port_ocean.core.handlers.port_app_config.models import ResourceConfig
from port_ocean.core.ocean_types import RESYNC_RESULT
from port_ocean.ocean import Ocean
from port_ocean.utils.misc import get_spec_file, load_module


def get_integration_ocean_app(integration_path: str) -> Ocean:
spec_file = get_spec_file(Path(integration_path))

config_factory = None if not spec_file else spec_file.get("configurations", [])

default_app = create_default_app(
integration_path,
config_factory,
{
"port": {
"client_id": "bla",
"client_secret": "bla",
},
},
)
main_path = f"{integration_path}/main.py"
sys.path.append(integration_path)
app_module = load_module(main_path)
app: Ocean = {name: item for name, item in getmembers(app_module)}.get(
"app", default_app
)

return app


def get_integation_resource_configs(integration_path: str) -> List[ResourceConfig]:
with open(
f"{integration_path}/.port/resources/port-app-config.yml"
) as port_app_config_file:
resource_configs = safe_load(port_app_config_file)

return [ResourceConfig(**item) for item in resource_configs["resources"]]


async def get_raw_result_on_integration_sync_resource_config(
app: Ocean, resource_config: ResourceConfig
) -> Tuple[RESYNC_RESULT, List[Exception]]:
resource_result = await app.integration._get_resource_raw_results(resource_config)

return resource_result
71 changes: 71 additions & 0 deletions port_ocean/tests/test_smoke.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
from os import environ
from typing import Tuple
import pytest

from port_ocean.clients.port.client import PortClient
from port_ocean.clients.port.types import UserAgentType
from port_ocean.tests.helpers.fixtures import SmokeTestDetails


@pytest.mark.skipif(
environ.get("SMOKE_TEST_SUFFIX", None) is None,
reason="You need to run the fake integration once",
)
async def test_valid_fake_integration(
port_client_for_fake_integration: Tuple[SmokeTestDetails, PortClient],
) -> None:
_, port_client = port_client_for_fake_integration
current_integration = await port_client.get_current_integration()
assert current_integration is not None
assert current_integration.get("resyncState") is not None
assert current_integration.get("resyncState", {}).get("status") == "completed"


@pytest.mark.skipif(
environ.get("SMOKE_TEST_SUFFIX", None) is None,
reason="You need to run the fake integration once",
)
async def test_valid_fake_departments(
port_client_for_fake_integration: Tuple[SmokeTestDetails, PortClient],
) -> None:
details, port_client = port_client_for_fake_integration
entities = await port_client.search_entities(user_agent_type=UserAgentType.exporter)
assert len(entities)
departments = [
x for x in entities if f"{x.blueprint}" == details.blueprint_department
]
assert len(departments) == 5


@pytest.mark.skipif(
environ.get("SMOKE_TEST_SUFFIX", None) is None,
reason="You need to run the fake integration once",
)
async def test_valid_fake_persons(
port_client_for_fake_integration: Tuple[SmokeTestDetails, PortClient],
) -> None:
details, port_client = port_client_for_fake_integration
headers = await port_client.auth.headers()
fake_person_entities_result = await port_client.client.get(
f"{port_client.auth.api_url}/blueprints/{details.blueprint_person}/entities",
headers=headers,
)

fake_person_entities = fake_person_entities_result.json()["entities"]
assert len(fake_person_entities)

fake_departments_result = await port_client.client.get(
f"{port_client.auth.api_url}/blueprints/{details.blueprint_department}/entities",
headers=headers,
)

departments = [x["identifier"] for x in fake_departments_result.json()["entities"]]

for department in departments:
assert len(
[
x
for x in fake_person_entities
if x["relations"]["department"] == department
]
)
Loading

0 comments on commit b89ebba

Please sign in to comment.