Skip to content

Commit

Permalink
Merge branch 'main' into clickup-integrations
Browse files Browse the repository at this point in the history
  • Loading branch information
oiadebayo authored Sep 17, 2024
2 parents 4b88729 + 8fbaeb3 commit 1e25c45
Show file tree
Hide file tree
Showing 13 changed files with 358 additions and 84 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
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -159,3 +159,8 @@ cython_debug/
# VSCode
.vscode/*
!.vscode/launch.json

# Junit

junit/*
**/junit/*
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm

<!-- towncrier release notes start -->

## 0.10.11 (2024-09-17)

### Improvements

- Add smoke test with a live integration to validate core changes

## 0.10.10 (2024-09-12)

### Bug Fixes
Expand Down
3 changes: 2 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ define deactivate_virtualenv
fi
endef

.SILENT: install install/all test/all lint build run new test test/watch clean bump/integrations bump/single-integration execute/all
.SILENT: install install/all test/all lint lint/fix build run new test test/watch clean bump/integrations bump/single-integration execute/all


# Install dependencies
Expand Down Expand Up @@ -102,6 +102,7 @@ lint:
lint/fix:
$(ACTIVATE) && \
black .
ruff check --fix .

# Development commands
build:
Expand Down
2 changes: 2 additions & 0 deletions integrations/_infra/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ ARG BUILD_CONTEXT
ARG INTEGRATION_VERSION

LABEL INTEGRATION_VERSION=${INTEGRATION_VERSION}
# Used to ensure that new integrations will be public, see https://docs.github.com/en/packages/learn-github-packages/configuring-a-packages-access-control-and-visibility
LABEL org.opencontainers.image.source https://github.com/port-labs/ocean

Check warning on line 8 in integrations/_infra/Dockerfile

View workflow job for this annotation

GitHub Actions / build-integration (integrations/clickup/.port/spec.yaml)

Legacy key/value format with whitespace separator should not be used

LegacyKeyValueFormat: "LABEL key=value" should be used instead of legacy "LABEL key value" format More info: https://docs.docker.com/go/dockerfile/rule/legacy-key-value-format/

ENV LIBRDKAFKA_VERSION 1.9.2

Check warning on line 10 in integrations/_infra/Dockerfile

View workflow job for this annotation

GitHub Actions / build-integration (integrations/clickup/.port/spec.yaml)

Legacy key/value format with whitespace separator should not be used

LegacyKeyValueFormat: "ENV key=value" should be used instead of legacy "ENV key value" format More info: https://docs.docker.com/go/dockerfile/rule/legacy-key-value-format/

Expand Down
7 changes: 6 additions & 1 deletion integrations/_infra/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ define deactivate_virtualenv
fi
endef

.SILENT: install install/prod install/local-core lint run test clean
.SILENT: install install/prod install/local-core lint lint/fix run test clean

install:
$(call deactivate_virtualenv) && \
Expand All @@ -56,6 +56,11 @@ lint:
$(ACTIVATE) && \
$(call run_checks,.)

lint/fix:
$(ACTIVATE) && \
black .
ruff check --fix .

run:
$(ACTIVATE) && ocean sail

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
Loading

0 comments on commit 1e25c45

Please sign in to comment.