diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index b763a2b..9041220 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -9,6 +9,7 @@ jobs: runs-on: ubuntu-latest steps: + - name: SCM Checkout uses: actions/checkout@v3 with: @@ -103,9 +104,12 @@ jobs: with: python-version: ${{ matrix.python-version }} - - name: Calculate Test Coverage - if: ${{ hashFiles('.coverage') != '' }} - run: poetry run nox -s coverage -- -- --db-version ${{ matrix.exasol-version }} + - name: Run Tests and Calculate Coverage + env: + SAAS_HOST: ${{ secrets.INTEGRATION_TEAM_SAAS_STAGING_HOST }} + SAAS_ACCOUNT_ID: ${{ secrets.INTEGRATION_TEAM_SAAS_STAGING_ACCOUNT_ID }} + SAAS_PAT: ${{ secrets.INTEGRATION_TEAM_SAAS_STAGING_PAT }} + run: poetry run nox -s coverage -- -- - name: Upload Artifacts uses: actions/upload-artifact@v3 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f0dc9d1..8e5dc15 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,6 +18,7 @@ jobs: ci-job: name: Checks uses: ./.github/workflows/checks.yml + secrets: inherit metrics: needs: [ ci-job ] diff --git a/doc/changes/changelog.md b/doc/changes/changelog.md index 3f5762b..555c95f 100644 --- a/doc/changes/changelog.md +++ b/doc/changes/changelog.md @@ -1,5 +1,6 @@ # Changes +* [0.3.0](changes_0.3.0.md) * [0.2.0](changes_0.2.0.md) * [0.1.0](changes_0.1.0.md) @@ -8,6 +9,7 @@ --- hidden: --- +changes_0.3.0 changes_0.2.0 changes_0.1.0 diff --git a/doc/changes/changes_0.3.0.md b/doc/changes/changes_0.3.0.md new file mode 100644 index 0000000..d8a2dc6 --- /dev/null +++ b/doc/changes/changes_0.3.0.md @@ -0,0 +1,9 @@ +# Saas API Python 0.3.0, released tbd + +## Summary + +This release adds integration tests for the most important calls to SaaS API. + +## Refactorings + +* #21: Added integration test for operation "create database" diff --git a/doc/developer_guide/developer_guide.md b/doc/developer_guide/developer_guide.md index 0ca9a25..4c5a7a8 100644 --- a/doc/developer_guide/developer_guide.md +++ b/doc/developer_guide/developer_guide.md @@ -35,3 +35,14 @@ Use CLI option `--path` to read the JSON definition from a local file instead of ```python "--path", "/path/to/openapi.json", ``` + +## Run Tests + +Executing the integration tests requires the following environment variables to be set: + +| Variable | Description | +|-------------------|-------------------------------------------------------| +| `SAAS_HOST` | Host to use for requests to REST API | +| `SAAS_ACCOUNT_ID` | ID of the Exasol SAAS account to be used by the tests | +| `SAAS_PAT` | Personal access token to access the SAAS API | + diff --git a/pyproject.toml b/pyproject.toml index 7ad9bd6..3fe57fa 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "saas-api" -version = "0.2.0" +version = "0.3.0" description = "API enabling Python applications connecting to Exasol database SaaS instances and using their SaaS services" packages = [ {include = "exasol"}, ] authors = [ "Christoph Kuhnke " ] @@ -77,6 +77,7 @@ ignore = [ ] ignore-paths = [ ".*/exasol/saas/client/openapi/.*", + ".*/test/.*", ] diff --git a/test/integration/api_access.py b/test/integration/api_access.py new file mode 100644 index 0000000..e8b9a7c --- /dev/null +++ b/test/integration/api_access.py @@ -0,0 +1,89 @@ +from typing import Iterable +from contextlib import contextmanager +from datetime import datetime + +from exasol.saas.client import openapi +from exasol.saas.client.openapi.api.databases import ( + create_database, + delete_database, + list_databases, +) +from exasol.saas.client.openapi.api.security import ( + list_allowed_i_ps, + add_allowed_ip, + delete_allowed_ip, +) + + +def timestamp() -> str: + return f'{datetime.now().timestamp():.0f}' + + +def create_saas_client( + host: str, + pat: str, + raise_on_unexpected_status: bool = True, +) -> openapi.AuthenticatedClient: + return openapi.AuthenticatedClient( + base_url=host, + token=pat, + raise_on_unexpected_status = raise_on_unexpected_status, + ) + + +class _OpenApiAccess: + """ + This class is meant to be used only in the context of the API + generator repository while integration tests in other repositories are + planned to only use fixture ``saas_database_id()``. + """ + + def __init__(self, client: openapi.Client, account_id: str): + self._client = client + self._account_id = account_id + + def create_database(self, cluster_size: str = "XS") -> openapi.models.database.Database: + cluster_spec = openapi.models.CreateCluster( + name="my-cluster", + size=cluster_size, + ) + return create_database.sync( + self._account_id, + client=self._client, + body=openapi.models.CreateDatabase( + name=f"pytest-{timestamp()}", + initial_cluster=cluster_spec, + provider="aws", + region='us-east-1', + ) + ) + + @contextmanager + def _ignore_failures(self, ignore: bool = False): + before = self._client.raise_on_unexpected_status + self._client.raise_on_unexpected_status = not ignore + yield self._client + self._client.raise_on_unexpected_status = before + + def delete_database(self, database_id: str, ignore_failures=False): + with self._ignore_failures(ignore_failures) as client: + return delete_database.sync_detailed( + self._account_id, database_id, client=client) + + def list_database_ids(self) -> Iterable[str]: + dbs = list_databases.sync(self._account_id, client=self._client) + return (db.id for db in dbs) + + @contextmanager + def database( + self, + keep: bool = False, + ignore_delete_failure: bool = False, + ): + db = None + try: + db = self.create_database() + yield db + finally: + if not keep and db: + self.delete_database(db.id, ignore_delete_failure) diff --git a/test/integration/conftest.py b/test/integration/conftest.py new file mode 100644 index 0000000..b407910 --- /dev/null +++ b/test/integration/conftest.py @@ -0,0 +1,35 @@ +import pytest +import os + +from exasol.saas.client import openapi +from api_access import create_saas_client, _OpenApiAccess + +@pytest.fixture(scope="session") +def saas_host() -> str: + return os.environ["SAAS_HOST"] + + +@pytest.fixture(scope="session") +def saas_pat() -> str: + return os.environ["SAAS_PAT"] + + +@pytest.fixture(scope="session") +def saas_account_id() -> str: + return os.environ["SAAS_ACCOUNT_ID"] + + +@pytest.fixture(scope="session") +def api_access(saas_host, saas_pat, saas_account_id) -> _OpenApiAccess: + with create_saas_client(saas_host, saas_pat) as client: + yield _OpenApiAccess(client, saas_account_id) + + +@pytest.fixture(scope="session") +def saas_database(api_access) -> openapi.models.database.Database: + """ + Note: The SaaS instance database returned by this fixture initially + will not be operational. The startup takes about 20 minutes. + """ + with api_access.database() as db: + yield db diff --git a/test/integration/databases_test.py b/test/integration/databases_test.py new file mode 100644 index 0000000..80c76d5 --- /dev/null +++ b/test/integration/databases_test.py @@ -0,0 +1,26 @@ +from exasol.saas.client import openapi + + +def test_lifecycle(api_access): + """ + This integration test uses the database created and provided by pytest + context ``_OpenApiAccess.database()`` to verify + + - initial status and number of clusters of the created database + - list_databases includes the new database + - delete_database deletes the database + - list_databases does not include the deleted database anymore + """ + + testee = api_access + with testee.database(ignore_delete_failure=True) as db: + # verify state and clusters of created database + assert db.status == openapi.models.Status.TOCREATE and \ + db.clusters.total == 1 + + # verify database is listed + assert db.id in testee.list_database_ids() + + # delete database and verify database is not listed anymore + testee.delete_database(db.id) + assert db.id not in testee.list_database_ids() diff --git a/test/integration/test_placeholder.py b/test/integration/test_placeholder.py deleted file mode 100644 index 4033aab..0000000 --- a/test/integration/test_placeholder.py +++ /dev/null @@ -1,4 +0,0 @@ -""" doc Integration tests """ - -def test_placeholder(): - """ doc """ diff --git a/version.py b/version.py index 55cfb40..ff792e9 100644 --- a/version.py +++ b/version.py @@ -5,6 +5,6 @@ # Do not edit this file manually! # If you need to change the version, do so in the project.toml, e.g. by using `poetry version X.Y.Z`. MAJOR = 0 -MINOR = 2 +MINOR = 3 PATCH = 0 VERSION = f"{MAJOR}.{MINOR}.{PATCH}"