From 15abef62b0b270469147dd844feedd55f7ff8a39 Mon Sep 17 00:00:00 2001 From: abalias Date: Tue, 6 Dec 2022 12:47:14 +0000 Subject: [PATCH] test: enhancements for e2e tests (#200) --- .github/workflows/e2e-tests.yml | 33 ++- infrastructure/local/.env | 2 +- infrastructure/local/run.sh | 30 +- infrastructure/local/stop.sh | 22 +- infrastructure/local/update_env.sh | 14 + infrastructure/shared/docker-compose.yml | 63 ++--- .../utils/python/github-helpers/.gitignore | 3 + .../github-helpers/github_helpers/__init__.py | 0 .../github-helpers/github_helpers/api.py | 160 +++++++++++ .../github-helpers/github_helpers/cli.py | 218 +++++++++++++++ .../github-helpers/github_helpers/test_cli.py | 38 +++ .../utils/python/github-helpers/setup.py | 22 ++ tests/e2e-tests/build.gradle.kts | 18 +- .../main/resources => }/serenity.properties | 4 +- .../src/main/kotlin/api_models/Connection.kt | 1 + .../src/test/kotlin/extentions/WithAgents.kt | 52 ++++ .../kotlin/features/did/ResolveDidSteps.kt | 23 +- .../IssueCredentialsSteps.kt | 257 +++++++----------- .../src/test/kotlin/runners/E2eTestsRunner.kt | 11 +- .../issue_credentials.feature | 15 +- 20 files changed, 715 insertions(+), 271 deletions(-) create mode 100755 infrastructure/local/update_env.sh create mode 100644 infrastructure/utils/python/github-helpers/.gitignore create mode 100644 infrastructure/utils/python/github-helpers/github_helpers/__init__.py create mode 100644 infrastructure/utils/python/github-helpers/github_helpers/api.py create mode 100644 infrastructure/utils/python/github-helpers/github_helpers/cli.py create mode 100644 infrastructure/utils/python/github-helpers/github_helpers/test_cli.py create mode 100644 infrastructure/utils/python/github-helpers/setup.py rename tests/e2e-tests/{src/main/resources => }/serenity.properties (72%) create mode 100644 tests/e2e-tests/src/test/kotlin/extentions/WithAgents.kt diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index f9d799e217..4285f1293e 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -30,6 +30,11 @@ jobs: with: java-version: openjdk@1.11 + - name: Install Python + uses: actions/setup-python@v2 + with: + python-version: '3.8' + - name: Login to GitHub Container Registry uses: docker/login-action@v2 with: @@ -43,27 +48,45 @@ jobs: version: v2.12.2 # defaults to 'latest' legacy: true # will also install in PATH as `docker-compose` - - name: Start services + - name: Start services for issuer + env: + PORT: 8080 + uses: isbang/compose-action@v1.4.1 + with: + compose-file: "./infrastructure/shared/docker-compose.yml" + compose-flags: "--env-file ./infrastructure/local/.env -p issuer" + up-flags: "--wait" + down-flags: "--volumes" + + - name: Start services for holder + env: + PORT: 8090 uses: isbang/compose-action@v1.4.1 with: - compose-file: "./infrastructure/ci/docker-compose-multiple-actors.yml" + compose-file: "./infrastructure/shared/docker-compose.yml" + compose-flags: "--env-file ./infrastructure/local/.env -p holder" up-flags: "--wait" + down-flags: "--volumes" - name: Run e2e tests + env: + ATALA_GITHUB_TOKEN: ${{ secrets.ATALA_GITHUB_TOKEN }} run: | - ./gradlew test --info + ../../infrastructure/local/update_env.sh + cat ../../infrastructure/local/.env + ./gradlew test --tests "E2eTestsRunner" - uses: actions/upload-artifact@v2 if: always() with: name: e2e-tests-result - path: tests/e2e-tests/target/site/reports + path: tests/e2e-tests/target/site/serenity - name: Publish e2e test Results if: always() id: publish-unit-tests uses: EnricoMi/publish-unit-test-result-action@v2 with: - junit_files: "tests/e2e-tests/target/site/reports/SERENITY-JUNIT-*.xml" + junit_files: "tests/e2e-tests/target/site/serenity/SERENITY-JUNIT-*.xml" comment_title: "E2E Test Results" check_name: "E2E Test Results" diff --git a/infrastructure/local/.env b/infrastructure/local/.env index 35c68adda2..3f6976d591 100644 --- a/infrastructure/local/.env +++ b/infrastructure/local/.env @@ -1,4 +1,4 @@ MERCURY_MEDIATOR_VERSION=0.2.0 IRIS_SERVICE_VERSION=0.1.0 -PRISM_AGENT_VERSION=0.6.0 +PRISM_AGENT_VERSION=0.10.0 PORT=80 diff --git a/infrastructure/local/run.sh b/infrastructure/local/run.sh index 60cdf3f6ba..e317213182 100755 --- a/infrastructure/local/run.sh +++ b/infrastructure/local/run.sh @@ -4,9 +4,6 @@ set -e SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) -# Set working directory -cd ${SCRIPT_DIR} - Help() { # Display Help @@ -17,6 +14,7 @@ Help() echo "-n/--name Name of this instance - defaults to dev." echo "-p/--port Port to run this instance on - defaults to 80." echo "-b/--background Run in docker-compose daemon mode in the background." + echo "-w/--wait Wait until all containers are healthy (only in the background)." echo "-h/--help Print this help text." echo } @@ -39,6 +37,10 @@ while [[ $# -gt 0 ]]; do BACKGROUND="-d" shift # past argument ;; + -w|--wait) + WAIT="--wait" + shift # past argument + ;; -h|--help) Help exit @@ -57,26 +59,13 @@ done set -- "${POSITIONAL_ARGS[@]}" # restore positional parameters - if [[ -n $1 ]]; then echo "Last line of file specified as non-opt/last argument:" tail -1 "$1" fi -if [ -z ${NAME+x} ]; -then - NAME="local" -fi - -if [ -z ${PORT+x} ]; -then - PORT="80" -fi - -if [ -z ${BACKGROUND+x} ]; -then - BACKGROUND="" -fi +NAME="${NAME:=local}" +PORT="${PORT:=80}" echo "NAME = ${NAME}" echo "PORT = ${PORT}" @@ -85,4 +74,7 @@ echo "--------------------------------------" echo "Bringing up stack using docker-compose" echo "--------------------------------------" -PORT=${PORT} docker-compose -p ${NAME} -f ../shared/docker-compose.yml --env-file ${SCRIPT_DIR}/.env up ${BACKGROUND} +PORT=${PORT} docker-compose \ + -p ${NAME} \ + -f ${SCRIPT_DIR}/../shared/docker-compose.yml \ + --env-file ${SCRIPT_DIR}/.env up ${BACKGROUND} ${WAIT} diff --git a/infrastructure/local/stop.sh b/infrastructure/local/stop.sh index f485ea75cc..b77882a5eb 100755 --- a/infrastructure/local/stop.sh +++ b/infrastructure/local/stop.sh @@ -4,9 +4,6 @@ set -e SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) -# Set working directory -cd ${SCRIPT_DIR} - Help() { # Display Help @@ -57,18 +54,8 @@ if [[ -n $1 ]]; then tail -1 "$1" fi -if [ -z ${NAME+x} ]; -then - NAME="local" -fi - -if [ -z ${VOLUMES+x} ]; -then - VOLUMES="" -fi - -# set a default port as required to ensure docker-compose is valid if not set in env -PORT="80" +NAME="${NAME:=local}" +PORT="${PORT:=80}" echo "NAME = ${NAME}" @@ -76,4 +63,7 @@ echo "--------------------------------------" echo "Stopping stack using docker-compose" echo "--------------------------------------" -PORT=${PORT} docker-compose -p ${NAME} -f ../shared/docker-compose.yml --env-file ${SCRIPT_DIR}/.env down ${VOLUMES} +PORT=${PORT} docker-compose \ + -p ${NAME} \ + -f ${SCRIPT_DIR}/../shared/docker-compose.yml \ + --env-file ${SCRIPT_DIR}/.env down ${VOLUMES} diff --git a/infrastructure/local/update_env.sh b/infrastructure/local/update_env.sh new file mode 100755 index 0000000000..e8e695ed80 --- /dev/null +++ b/infrastructure/local/update_env.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash + +SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) +ENV_FILE="${SCRIPT_DIR}/.env" + +pip install ${SCRIPT_DIR}/../utils/python/github-helpers > /dev/null 2>&1 + +MERCURY_MEDIATOR_VERSION=$(github get-latest-package-version --package mercury-mediator --package-type container) +IRIS_SERVICE_VERSION=$(github get-latest-package-version --package iris-service --package-type container) +PRISM_AGENT_VERSION=$(github get-latest-package-version --package prism-agent --package-type container) + +sed -i.bak "s/MERCURY_MEDIATOR_VERSION=.*/MERCURY_MEDIATOR_VERSION=${MERCURY_MEDIATOR_VERSION}/" ${ENV_FILE} && rm -f ${ENV_FILE}.bak +sed -i.bak "s/IRIS_SERVICE_VERSION=.*/IRIS_SERVICE_VERSION=${IRIS_SERVICE_VERSION}/" ${ENV_FILE} && rm -f ${ENV_FILE}.bak +sed -i.bak "s/PRISM_AGENT_VERSION=.*/PRISM_AGENT_VERSION=${PRISM_AGENT_VERSION}/" ${ENV_FILE} && rm -f ${ENV_FILE}.bak diff --git a/infrastructure/shared/docker-compose.yml b/infrastructure/shared/docker-compose.yml index 4338de090e..1e00481fe5 100644 --- a/infrastructure/shared/docker-compose.yml +++ b/infrastructure/shared/docker-compose.yml @@ -14,14 +14,11 @@ services: POSTGRES_PASSWORD: postgres volumes: - pg_data_castor_db:/var/lib/postgresql/data - - # delay to ensure DB is up before applying migrations - db_castor_init_delay: - image: alpine:3 - command: sleep 5 - depends_on: - db_castor: - condition: service_started + healthcheck: + test: [ "CMD", "pg_isready", "-U", "postgres", "-d", "castor" ] + interval: 10s + timeout: 5s + retries: 5 ########################## # Pollux Database @@ -36,14 +33,11 @@ services: POSTGRES_PASSWORD: postgres volumes: - pg_data_pollux_db:/var/lib/postgresql/data - - # delay to ensure DB is up before applying migrations - db_pollux_init_delay: - image: alpine:3 - command: sleep 5 - depends_on: - db_pollux: - condition: service_started + healthcheck: + test: [ "CMD", "pg_isready", "-U", "postgres", "-d", "pollux" ] + interval: 10s + timeout: 5s + retries: 5 ########################## # Connect Database @@ -58,14 +52,11 @@ services: POSTGRES_PASSWORD: postgres volumes: - pg_data_connect_db:/var/lib/postgresql/data - - # delay to ensure DB is up before applying migrations - db_connect_init_delay: - image: alpine:3 - command: sleep 5 - depends_on: - db_connect: - condition: service_started + healthcheck: + test: [ "CMD", "pg_isready", "-U", "postgres", "-d", "connect" ] + interval: 10s + timeout: 5s + retries: 5 ########################## # Iris Database @@ -80,15 +71,11 @@ services: POSTGRES_PASSWORD: postgres volumes: - pg_data_iris_db:/var/lib/postgresql/data - - # delay to ensure DB is up before applying migrations - db_iris_init_delay: - image: alpine:3 - command: sleep 5 - depends_on: - db_iris: - condition: service_started - + healthcheck: + test: [ "CMD", "pg_isready", "-U", "postgres", "-d", "iris" ] + interval: 10s + timeout: 5s + retries: 5 ########################## # Services @@ -125,6 +112,16 @@ services: CONNECT_DB_USER: postgres CONNECT_DB_PASSWORD: postgres DIDCOMM_SERVICE_URL: http://host.docker.internal:${PORT}/didcomm/ + depends_on: + - db_castor + - db_pollux + healthcheck: + test: [ "CMD", "curl", "-f", "http://prism-agent:8080/connections" ] + interval: 30s + timeout: 10s + retries: 5 + extra_hosts: + - "host.docker.internal:host-gateway" swagger-ui: image: swaggerapi/swagger-ui:v4.14.0 diff --git a/infrastructure/utils/python/github-helpers/.gitignore b/infrastructure/utils/python/github-helpers/.gitignore new file mode 100644 index 0000000000..1a51912d75 --- /dev/null +++ b/infrastructure/utils/python/github-helpers/.gitignore @@ -0,0 +1,3 @@ +build/ +*.egg-info/ +.env-e diff --git a/infrastructure/utils/python/github-helpers/github_helpers/__init__.py b/infrastructure/utils/python/github-helpers/github_helpers/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/infrastructure/utils/python/github-helpers/github_helpers/api.py b/infrastructure/utils/python/github-helpers/github_helpers/api.py new file mode 100644 index 0000000000..f1d8500253 --- /dev/null +++ b/infrastructure/utils/python/github-helpers/github_helpers/api.py @@ -0,0 +1,160 @@ +""" +GitHub Helpers API module +""" +import os +import zipfile + +import requests +from requests.adapters import HTTPAdapter +from requests.auth import HTTPBasicAuth +from urllib3 import Retry + + +class GithubError(Exception): + """Error happened in GitHub API call""" + + +class GithubApi: + """Client for GitHub API""" + + def __init__(self, token=None, owner=None, repo=None, url="https://api.github.com"): + """Initialize a client to interact with GitHub API. + + :param token: GitHub API access token. Defaults to GITHUB_TOKEN env var + :param url: The URL of the GitHub API instance + """ + if not token: + raise GithubError("Missing or empty GitHub API access token") + self.token = token + if not owner or not repo: + raise GithubError("`owner` and `repo` must be defined for this request.") + self.owner = owner + self.repo = repo + self.url = url + self._request_session() + + def __repr__(self): + opts = { + "token": self.token, + "url": self.url, + } + kwargs = [f"{k}={v!r}" for k, v in opts.items()] + return f'GithubApi({", ".join(kwargs)})' + + def get_workflow_artifacts(self, run_id): + """Get workflow artifacts. + + Endpoint: + GET: ``/repos/{owner}/{repo}/actions/runs/{run_id}/artifacts`` + """ + endpoint = f"repos/{self.owner}/{self.repo}/actions/runs/{run_id}/artifacts" + resp = self._get(endpoint) + return resp + + def download_artifact(self, artifact_id, destdir=None, filename=None, unzip=True): + """Downloads artifact by its ID""" + endpoint = f"repos/{self.owner}/{self.repo}/actions/artifacts/{artifact_id}/zip" + path = self._download(endpoint, destdir=destdir, filename=filename) + if unzip: + with zipfile.ZipFile(path, "r") as zip_ref: + zip_ref.extractall(os.path.dirname(path)) + os.remove(path) + return path + + def get_package_versions(self, package_name, package_type="maven"): + """Get versions of Maven package at GitHib packages + + Endpoint: + GET: ``/orgs/{org}/packages/{package_type}/{package_name}/versions`` + """ + endpoint = f"orgs/{self.owner}/packages/{package_type}/{package_name}/versions" + resp = self._get(endpoint) + return resp + + def get_release_versions(self): + """Get versions of Maven package at GitHib packages + + Endpoint: + GET: ``/orgs/{owner}/{repo}/releases`` + """ + endpoint = f"repos/{self.owner}/{self.repo}/releases" + resp = self._get(endpoint) + return resp + + def get_workflow_runs(self, branch=None): + """Get workflow runs + + Endpoint: + GET ``/repos/{owner}/{repo}/actions/runs`` + """ + endpoint = f"repos/{self.owner}/{self.repo}/actions/runs" + if branch: + endpoint = f"{endpoint}?branch={branch}" + resp = self._get(endpoint) + return resp + + def _request_session( + self, + retries=3, + backoff_factor=0.3, + status_forcelist=(408, 429, 500, 502, 503, 504, 520, 521, 522, 523, 524), + ): + """Get a session with Retry enabled. + + :param retries: Number of retries to allow. + :param backoff_factor: Backoff factor to apply between attempts. + :param status_forcelist: HTTP status codes to force a retry on. + """ + self._session = requests.Session() + retry = Retry( + total=retries, + backoff_factor=backoff_factor, + status_forcelist=status_forcelist, + allowed_methods=False, + raise_on_redirect=False, + raise_on_status=False, + respect_retry_after_header=False, + ) + adapter = HTTPAdapter(max_retries=retry) + self._session.mount("http://", adapter) + self._session.mount("https://", adapter) + return self._session + + def _get(self, endpoint): + """Send a GET HTTP request. + + :param endpoint: API endpoint to call. + :type endpoint: str + + :raises requests.exceptions.HTTPError: When response code is not successful. + :returns: A JSON object with the response from the API. + """ + headers = {"Accept": "application/vnd.github.v3+json"} + auth = HTTPBasicAuth(self.token, "") + resp = None + request_url = "{0}/{1}".format(self.url, endpoint) + resp = self._session.get(request_url, auth=auth, headers=headers) + resp.raise_for_status() + return resp.json() + + def _download(self, endpoint, destdir=None, filename=None): + """Downloads a file. + + :param endpoint: Endpoint to download from. + :param destdir: Optional destination directory. + :param filename: Optional file name. Defaults to download.zip. + """ + + if not filename: + filename = "download.zip" + if not destdir: + destdir = os.getcwd() + auth = HTTPBasicAuth(self.token, "") + request_url = "{0}/{1}".format(self.url, endpoint) + resp = self._session.get(request_url, stream=True, auth=auth) + path = "{0}/{1}".format(destdir, filename) + with open(path, "wb") as download_file: + for chunk in resp.iter_content(chunk_size=1024): + if chunk: + download_file.write(chunk) + return path diff --git a/infrastructure/utils/python/github-helpers/github_helpers/cli.py b/infrastructure/utils/python/github-helpers/github_helpers/cli.py new file mode 100644 index 0000000000..f10b6be74a --- /dev/null +++ b/infrastructure/utils/python/github-helpers/github_helpers/cli.py @@ -0,0 +1,218 @@ +""" +GitHub helpers for CI/CD scripts + +Following actions are available: +* Getting latest version of a package from GitHub packages +* Downloading arts from GitHub Actions pipelines +""" + +import dataclasses +import logging +import sys + +import click + +# pylint: disable=E0402 +from .api import GithubApi + + +@dataclasses.dataclass +class Errors: + """Error codes for GitHub helpers""" + + UNABLE_TO_DOWNLOAD = 1 + EMPTY_VERSIONS_LIST = 2 + + +def init_logger(): + """Initializes logger + + :return: Initialized logger + :rtype: logging.Logger + """ + logger = logging.getLogger("GitHub Helpers") + logger.setLevel(logging.DEBUG) + chandler = logging.StreamHandler() + chandler.setLevel(logging.DEBUG) + formatter = logging.Formatter( + "%(asctime)s - %(name)s - %(levelname)s - %(message)s" + ) + chandler.setFormatter(formatter) + logger.addHandler(chandler) + return logger + + +# pylint: disable=R0903 +class Globals: + """Global variables to share between entry points""" + + def __init__(self, token, owner, repo): + """Default constructor + + :param token: GitHub API token + :type token: str + :param owner: GitHub owner(user) + :type owner: str + :param repo: GitHub repository name + :type repo: str + """ + self.api = GithubApi(token=token, owner=owner, repo=repo) + self.logger = init_logger() + + +pass_globals = click.make_pass_decorator(Globals) + + +@click.group() +@click.option( + "--token", + envvar="ATALA_GITHUB_TOKEN", + metavar="TOKEN", + required=True, + help="GitHub authentication token.", +) +@click.option( + "--owner", + default="input-output-hk", + metavar="OWNER", + help="GitHub owner(user).", +) +@click.option( + "--repo", + default="atala-prism-android-app", + metavar="REPOSITORY", + help="GitHub repo(project).", +) +@click.pass_context +def cli(ctx, token, owner, repo): + """Command line interface entry point + \f + + :param ctx: Shared context + :type ctx: click.core.Context + :param token: GitHub API token + :type token: str + :param owner: GitHub owner(user) + :type owner: str + :param repo: GitHub repository name + :type repo: str + """ + ctx.obj = Globals(token, owner, repo) + + +@cli.command() +@click.option( + "--package", + default="io.iohk.atala.prism-identity", + metavar="PACKAGE", + help="Package name.", +) +@click.option( + "--package-type", + default="maven", + metavar="TYPE", + help="Package type.", +) +@pass_globals +def get_latest_package_version(ctx, package, package_type): + """Gets latest version of package + \f + + :param ctx: Shared context + :type ctx: github_helpers.click_main.Globals + :param package: Name of GitHub package + :type package: str + :param package_type: Package type + :type package_type: str + """ + versions = ctx.api.get_package_versions(package, package_type=package_type) + if not versions: + ctx.logger.error( + f"Specified package {package} doesn't exist." "Versions list is empty." + ) + sys.exit(Errors.EMPTY_VERSIONS_LIST) + else: + try: + if package_type == 'container': + print(versions[0].get("metadata").get("container").get("tags")[0]) + else: + print(versions[0].get("name", "NOT EXIST")) + except: + print("NOT EXIST") + + +@cli.command() +@pass_globals +def get_latest_release_version(ctx): + """Gets latest version of the release + \f + + :param ctx: Shared context + :type ctx: github_helpers.click_main.Globals + """ + versions = ctx.api.get_release_versions() + if not versions: + ctx.logger.error("Releases list is empty.") + sys.exit(Errors.EMPTY_VERSIONS_LIST) + else: + print(versions[0].get("tag_name", "NOT EXIST")) + + +@cli.command() +@click.option( + "--run-id", + metavar="RUN_ID", + default="latest", + help="ID of GitHub Actions run.", +) +@click.option( + "--dest-dir", + metavar="DEST", + default=".", + help="Download destination directory.", +) +@click.option( + "--branch", + metavar="BRANCH", + default="master", + help="GitHub branch to search for latest artifacts.", +) +@pass_globals +def download_arts(ctx, run_id, dest_dir, branch): + """Downloads artifacts from GitHub + \f + + :param ctx: Shared context + :type ctx: github_helpers.click_main.Globals + :param run_id: GitHub Actions run ID + :type run_id: str + :param dest_dir: Download destination directory + :type dest_dir: str + :param branch: Branch for latest arts download + :type branch: str + """ + ctx.logger.info( + "Downloading artifacts for " f"run_id='{run_id}' and branch='{branch}'" + ) + wf_artifacts = {} + if run_id in ("latest", ""): + wf_runs = ctx.api.get_workflow_runs(branch) + for run in wf_runs["workflow_runs"]: + wf_artifacts = ctx.api.get_workflow_artifacts(run["id"]) + if wf_artifacts.get("total_count", 0) > 0: + break + else: + wf_artifacts = ctx.api.get_workflow_artifacts(run_id) + if wf_artifacts.get("total_count", 0) > 0: + for art in wf_artifacts["artifacts"]: + artifact_id = art.get("id") + artifact_name = art.get("name") + ctx.api.download_artifact( + artifact_id, filename=f"{artifact_name}.zip", destdir=dest_dir + ) + else: + ctx.logger.error( + "No runs with artifacts found for " + f"run_id='{run_id}' and branch='{branch}'" + ) + sys.exit(Errors.UNABLE_TO_DOWNLOAD) diff --git a/infrastructure/utils/python/github-helpers/github_helpers/test_cli.py b/infrastructure/utils/python/github-helpers/github_helpers/test_cli.py new file mode 100644 index 0000000000..4f99adbec0 --- /dev/null +++ b/infrastructure/utils/python/github-helpers/github_helpers/test_cli.py @@ -0,0 +1,38 @@ +"""Testing module for GitHub helpers""" + +import os + +from click.testing import CliRunner + +# pylint: disable=E0402 +from .cli import cli + + +def test_download_arts_positive(): + """Download arts test""" + runner = CliRunner() + result = runner.invoke( + cli, ["download-arts", "--branch", "master", "--run-id", "latest"] + ) + assert result.exit_code == 0, "Error during download-arts command" + if not os.path.exists("app-debug.apk"): + assert False, "Application was not downloaded!" + else: + os.remove("app-debug.apk") + + +def test_download_arts_negative(): + """Negative test for arts downloading""" + runner = CliRunner() + result = runner.invoke( + cli, ["download-arts", "--branch", "master", "--run-id", "never-exist"] + ) + assert result.exit_code == 1, "Arts downloaded but not exist!" + + +def test_get_latest_version(): + """Latest version test""" + runner = CliRunner() + result = runner.invoke(cli, ["get-latest-package-version"]) + assert result.exit_code == 0, "Unable to get latest package version!" + assert result.output != "", "Latest version cannot be empty string!" diff --git a/infrastructure/utils/python/github-helpers/setup.py b/infrastructure/utils/python/github-helpers/setup.py new file mode 100644 index 0000000000..67e0667ff2 --- /dev/null +++ b/infrastructure/utils/python/github-helpers/setup.py @@ -0,0 +1,22 @@ +"""Setup script for GitHub Helpers""" + +from setuptools import find_packages, setup + +setup( + name="github-helpers", + version="0.1", + description="GitHub API Python helpers for QA needs.", + author="Baliasnikov Anton", + author_email="anton.baliasnikov@iohk.io", + packages=find_packages(), + entry_points={ + "console_scripts": ["github=github_helpers.cli:cli"], + }, + install_requires=[ + "Click==8.0.1", + "requests==2.26.0", + "pylint==2.10.2", + "pytest==6.2.4", + "pytest-cov==2.12.1", + ], +) diff --git a/tests/e2e-tests/build.gradle.kts b/tests/e2e-tests/build.gradle.kts index dad738ac1a..b0f7fe299b 100644 --- a/tests/e2e-tests/build.gradle.kts +++ b/tests/e2e-tests/build.gradle.kts @@ -1,5 +1,5 @@ plugins { - kotlin("jvm") version "1.7.20" + kotlin("jvm") version "1.7.22" idea jacoco id("net.serenity-bdd.serenity-gradle-plugin") version "3.4.2" @@ -11,16 +11,16 @@ repositories { dependencies { // Logging - implementation("org.slf4j:slf4j-log4j12:2.0.3") + implementation("org.slf4j:slf4j-log4j12:2.0.5") // Beautify async waits implementation("org.awaitility:awaitility-kotlin:4.2.0") // Test engines and reports testImplementation("junit:junit:4.13.2") - testImplementation("net.serenity-bdd:serenity-core:3.4.2") - testImplementation("net.serenity-bdd:serenity-cucumber:3.4.2") - implementation("net.serenity-bdd:serenity-single-page-report:3.4.2") - implementation("net.serenity-bdd:serenity-json-summary-report:3.4.2") - testImplementation("net.serenity-bdd:serenity-screenplay-rest:3.4.2") + implementation("net.serenity-bdd:serenity-core:3.4.3") + implementation("net.serenity-bdd:serenity-cucumber:3.4.3") + implementation("net.serenity-bdd:serenity-single-page-report:3.4.3") + implementation("net.serenity-bdd:serenity-json-summary-report:3.4.3") + implementation("net.serenity-bdd:serenity-screenplay-rest:3.4.3") // Beautify exceptions handling assertions testImplementation("org.assertj:assertj-core:3.23.1") } @@ -31,3 +31,7 @@ dependencies { serenity { reports = listOf("single-page-html", "json-summary") } + +tasks.test { + testLogging.showStandardStreams = true +} diff --git a/tests/e2e-tests/src/main/resources/serenity.properties b/tests/e2e-tests/serenity.properties similarity index 72% rename from tests/e2e-tests/src/main/resources/serenity.properties rename to tests/e2e-tests/serenity.properties index be39bcdb93..a4b2f36eb0 100644 --- a/tests/e2e-tests/src/main/resources/serenity.properties +++ b/tests/e2e-tests/serenity.properties @@ -4,5 +4,5 @@ serenity.console.colors=true simplified.stack.traces=false jira.url=https://input-output.atlassian.net jira.project=ATL -serenity.test.root = "features" -serenity.outputDirectory = target/site/reports +serenity.logging=VERBOSE +serenity.console.headings=normal diff --git a/tests/e2e-tests/src/main/kotlin/api_models/Connection.kt b/tests/e2e-tests/src/main/kotlin/api_models/Connection.kt index 83a81af4fd..e6f32a61e2 100644 --- a/tests/e2e-tests/src/main/kotlin/api_models/Connection.kt +++ b/tests/e2e-tests/src/main/kotlin/api_models/Connection.kt @@ -3,6 +3,7 @@ package api_models data class Connection( var connectionId: String = "", var createdAt: String = "", + var updatedAt: String = "", var invitation: Invitation = Invitation(), var kind: String = "", var self: String = "", diff --git a/tests/e2e-tests/src/test/kotlin/extentions/WithAgents.kt b/tests/e2e-tests/src/test/kotlin/extentions/WithAgents.kt new file mode 100644 index 0000000000..4813b3b5c0 --- /dev/null +++ b/tests/e2e-tests/src/test/kotlin/extentions/WithAgents.kt @@ -0,0 +1,52 @@ +package extentions + +import io.restassured.path.json.JsonPath +import net.serenitybdd.core.annotations.events.BeforeScenario +import net.serenitybdd.rest.SerenityRest +import net.serenitybdd.screenplay.Actor +import net.serenitybdd.screenplay.rest.abilities.CallAnApi +import net.thucydides.core.util.EnvironmentVariables +import org.awaitility.Awaitility +import org.awaitility.core.ConditionTimeoutException +import org.awaitility.kotlin.withPollInterval +import org.awaitility.pollinterval.FixedPollInterval +import java.time.Duration + +open class WithAgents { + + protected lateinit var acme: Actor + protected lateinit var bob: Actor + + @BeforeScenario + fun acmeAndBobAgents() { + val theRestApiBaseUrlIssuer = System.getenv("RESTAPI_URL_ISSUER") ?: "http://localhost:8080/prism-agent" + val theRestApiBaseUrlHolder = System.getenv("RESTAPI_URL_HOLDER") ?: "http://localhost:8090/prism-agent" + acme = Actor.named("Acme").whoCan(CallAnApi.at(theRestApiBaseUrlIssuer)) + bob = Actor.named("Bob").whoCan(CallAnApi.at(theRestApiBaseUrlHolder)) + } + + fun lastResponse(): JsonPath { + return SerenityRest.lastResponse().jsonPath() + } + + fun wait( + blockToWait: () -> Boolean, + errorMessage: String, + poolInterval: FixedPollInterval = FixedPollInterval(Duration.ofSeconds(7L)), + timeout: Duration = Duration.ofSeconds(60L) + ): Unit { + try { + Awaitility.await().withPollInterval(poolInterval) + .pollInSameThread() + .atMost(timeout) + .until { + blockToWait() + } + } catch (err: ConditionTimeoutException) { + throw ConditionTimeoutException( + errorMessage + ) + } + return Unit + } +} diff --git a/tests/e2e-tests/src/test/kotlin/features/did/ResolveDidSteps.kt b/tests/e2e-tests/src/test/kotlin/features/did/ResolveDidSteps.kt index e74bfa5856..5a3de3f3bb 100644 --- a/tests/e2e-tests/src/test/kotlin/features/did/ResolveDidSteps.kt +++ b/tests/e2e-tests/src/test/kotlin/features/did/ResolveDidSteps.kt @@ -1,36 +1,23 @@ package features.did -import io.cucumber.java.Before +import extentions.WithAgents import io.cucumber.java.en.Then import io.cucumber.java.en.When -import net.serenitybdd.screenplay.Actor -import net.serenitybdd.screenplay.rest.abilities.CallAnApi import net.serenitybdd.screenplay.rest.interactions.Get import net.serenitybdd.screenplay.rest.questions.ResponseConsequence -import net.thucydides.core.util.EnvironmentVariables -class ResolveDidSteps { - - private lateinit var environmentVariables: EnvironmentVariables - private lateinit var issuer: Actor - - @Before - fun configureBaseUrl() { - val theRestApiBaseUrl = environmentVariables.optionalProperty("restapi.baseurl") - .orElse("http://localhost:8080") - issuer = Actor.named("Issuer").whoCan(CallAnApi.at(theRestApiBaseUrl)) - } +class ResolveDidSteps: WithAgents() { @When("I resolve existing DID by DID reference") fun iResolveExistingDIDByDIDReference() { - issuer.attemptsTo( - Get.resource("/dids/did:prism:123") + acme.attemptsTo( + Get.resource("/connections") ) } @Then("Response code is 200") fun responseCodeIs() { - issuer.should( + acme.should( ResponseConsequence.seeThatResponse("DID has required fields") { it.statusCode(200) } diff --git a/tests/e2e-tests/src/test/kotlin/features/issue_credentials/IssueCredentialsSteps.kt b/tests/e2e-tests/src/test/kotlin/features/issue_credentials/IssueCredentialsSteps.kt index 4c6ce172f7..af2f6041ad 100644 --- a/tests/e2e-tests/src/test/kotlin/features/issue_credentials/IssueCredentialsSteps.kt +++ b/tests/e2e-tests/src/test/kotlin/features/issue_credentials/IssueCredentialsSteps.kt @@ -3,39 +3,14 @@ package features.issue_credentials import io.cucumber.java.en.Given import io.cucumber.java.en.Then import io.cucumber.java.en.When -import net.serenitybdd.rest.SerenityRest -import net.serenitybdd.screenplay.Actor -import net.serenitybdd.screenplay.rest.abilities.CallAnApi import net.serenitybdd.screenplay.rest.interactions.Get import net.serenitybdd.screenplay.rest.interactions.Post import net.serenitybdd.screenplay.rest.questions.ResponseConsequence -import net.thucydides.core.util.EnvironmentVariables import api_models.Connection import api_models.Credential -import io.restassured.path.json.JsonPath -import org.awaitility.Awaitility -import org.awaitility.core.ConditionTimeoutException -import org.awaitility.kotlin.withPollInterval -import org.awaitility.pollinterval.FixedPollInterval -import java.time.Duration +import extentions.WithAgents - -class IssueCredentialsSteps { - - private lateinit var environmentVariables: EnvironmentVariables - private lateinit var acme: Actor - private lateinit var bob: Actor - private lateinit var lastResponse: JsonPath - - @Given("Acme and Bob agents") - fun acmeAndBobAgents() { - val theRestApiBaseUrlIssuer = environmentVariables.optionalProperty("restapi.baseurl.issuer") - .orElse("http://localhost:8080") - val theRestApiBaseUrlHolder = environmentVariables.optionalProperty("restapi.baseurl.holder") - .orElse("http://localhost:8090") - acme = Actor.named("Acme").whoCan(CallAnApi.at(theRestApiBaseUrlIssuer)) - bob = Actor.named("Bob").whoCan(CallAnApi.at(theRestApiBaseUrlHolder)) - } +class IssueCredentialsSteps : WithAgents() { @Given("Acme and Bob have an existing connection") fun acmeAndBobHaveAnExistingConnection() { @@ -53,7 +28,7 @@ class IssueCredentialsSteps { it.statusCode(201) } ) - val acmeConnection = SerenityRest.lastResponse().jsonPath().getObject("", Connection::class.java) + val acmeConnection = lastResponse().getObject("", Connection::class.java) // Here out of band transfer of connection QR code is happening // and Bob (Holder) gets an invitation URL @@ -73,56 +48,46 @@ class IssueCredentialsSteps { it.statusCode(200) } ) - val bobConnection = SerenityRest.lastResponse().jsonPath().getObject("", Connection::class.java) + val bobConnection = lastResponse().getObject("", Connection::class.java) // Acme(Issuer) checks their connections to check if invitation was accepted by Bob(Holder) // and sends final connection response - try { - Awaitility.await().withPollInterval(FixedPollInterval(Duration.ofSeconds(3L))) - .atMost(Duration.ofSeconds(60L)) - .until { - acme.attemptsTo( - Get.resource("/connections/${acmeConnection.connectionId}") - ) - acme.should( - ResponseConsequence.seeThatResponse("Get issuer connections") { - it.statusCode(200) - } - ) - lastResponse = SerenityRest.lastResponse().jsonPath() - lastResponse.getObject("", Connection::class.java).state == "ConnectionResponseSent" - } - } catch (err: ConditionTimeoutException) { - throw ConditionTimeoutException( - "Issuer did not sent final connection confirmation! Connection didn't reach ConnectionResponseSent state." - ) - } - acme.remember("did", lastResponse.getObject("", Connection::class.java).myDid) - acme.remember("holderDid", lastResponse.getObject("", Connection::class.java).theirDid) + wait( + { + acme.attemptsTo( + Get.resource("/connections/${acmeConnection.connectionId}"), + ) + acme.should( + ResponseConsequence.seeThatResponse("Get issuer connections") { + it.statusCode(200) + } + ) + lastResponse() + .getObject("", Connection::class.java).state == "ConnectionResponseSent" + }, + "Issuer did not sent final connection confirmation! Connection didn't reach ConnectionResponseSent state." + ) + acme.remember("did", lastResponse().getObject("", Connection::class.java).myDid) + acme.remember("holderDid", lastResponse().getObject("", Connection::class.java).theirDid) // Bob (Holder) receives final connection response - try { - Awaitility.await().withPollInterval(FixedPollInterval(Duration.ofSeconds(3L))) - .atMost(Duration.ofSeconds(60L)) - .until { - bob.attemptsTo( - Get.resource("/connections/${bobConnection.connectionId}") - ) - bob.should( - ResponseConsequence.seeThatResponse("Get holder connections") { - it.statusCode(200) - } - ) - lastResponse = SerenityRest.lastResponse().jsonPath() - lastResponse.getObject("", Connection::class.java).state == "ConnectionResponseReceived" - } - } catch (err: ConditionTimeoutException) { - throw ConditionTimeoutException( - "Holder did not receive final connection confirmation! Connection didn't reach ConnectionResponseReceived state." - ) - } - bob.remember("did", lastResponse.getObject("", Connection::class.java).myDid) - bob.remember("issuerDid", lastResponse.getObject("", Connection::class.java).theirDid) + wait( + { + bob.attemptsTo( + Get.resource("/connections/${bobConnection.connectionId}") + ) + bob.should( + ResponseConsequence.seeThatResponse("Get holder connections") { + it.statusCode(200) + } + ) + lastResponse().getObject("", Connection::class.java).state == "ConnectionResponseReceived" + }, + "Holder did not receive final connection confirmation! Connection didn't reach ConnectionResponseReceived state." + ) + + bob.remember("did", lastResponse().getObject("", Connection::class.java).myDid) + bob.remember("issuerDid", lastResponse().getObject("", Connection::class.java).theirDid) // Connection established. Both parties exchanged their DIDs with each other } @@ -158,28 +123,23 @@ class IssueCredentialsSteps { @When("Bob requests the credential") fun bobRequestsTheCredential() { - try { - Awaitility.await().withPollInterval(FixedPollInterval(Duration.ofSeconds(3L))) - .atMost(Duration.ofSeconds(60L)) - .until { - bob.attemptsTo( - Get.resource("/issue-credentials/records") - ) - bob.should( - ResponseConsequence.seeThatResponse("Credential records") { - it.statusCode(200) - } - ) - lastResponse = SerenityRest.lastResponse().jsonPath() - lastResponse.getList("items", Credential::class.java).findLast { it.protocolState == "OfferReceived" } != null - } - } catch (err: ConditionTimeoutException) { - throw ConditionTimeoutException( - "Holder was unable to receive the credential offer from Issuer! Protocol state did not achieve OfferReceived state." - ) - } + wait( + { + bob.attemptsTo( + Get.resource("/issue-credentials/records") + ) + bob.should( + ResponseConsequence.seeThatResponse("Credential records") { + it.statusCode(200) + } + ) + lastResponse().getList("items", Credential::class.java).findLast { it.protocolState == "OfferReceived" } != null + }, + "Holder was unable to receive the credential offer from Issuer! Protocol state did not achieve OfferReceived state." + ) - val recordId = lastResponse.getList("items", Credential::class.java).findLast { it.protocolState == "OfferReceived" }!!.recordId + val recordId = lastResponse().getList("items", Credential::class.java) + .findLast { it.protocolState == "OfferReceived" }!!.recordId bob.remember("recordId", recordId) bob.attemptsTo( @@ -194,27 +154,23 @@ class IssueCredentialsSteps { @When("Acme issues the credential") fun acmeIssuesTheCredential() { - try { - Awaitility.await().withPollInterval(FixedPollInterval(Duration.ofSeconds(3L))) - .atMost(Duration.ofSeconds(60L)) - .until { - acme.attemptsTo( - Get.resource("/issue-credentials/records") - ) - acme.should( - ResponseConsequence.seeThatResponse("Credential records") { - it.statusCode(200) - } - ) - lastResponse = SerenityRest.lastResponse().jsonPath() - lastResponse.getList("items", Credential::class.java).findLast { it.protocolState == "RequestReceived" } != null - } - } catch (err: ConditionTimeoutException) { - throw ConditionTimeoutException( - "Issuer was unable to receive the credential request from Holder! Protocol state did not achieve RequestReceived state." - ) - } - val recordId = lastResponse.getList("items", Credential::class.java).findLast { it.protocolState == "RequestReceived" }!!.recordId + wait( + { + acme.attemptsTo( + Get.resource("/issue-credentials/records") + ) + acme.should( + ResponseConsequence.seeThatResponse("Credential records") { + it.statusCode(200) + } + ) + lastResponse().getList("items", Credential::class.java) + .findLast { it.protocolState == "RequestReceived" } != null + }, + "Issuer was unable to receive the credential request from Holder! Protocol state did not achieve RequestReceived state." + ) + val recordId = lastResponse().getList("items", Credential::class.java) + .findLast { it.protocolState == "RequestReceived" }!!.recordId acme.attemptsTo( Post.to("/issue-credentials/records/${recordId}/issue-credential") ) @@ -224,50 +180,39 @@ class IssueCredentialsSteps { } ) - try { - Awaitility.await().withPollInterval(FixedPollInterval(Duration.ofSeconds(3L))) - .atMost(Duration.ofSeconds(60L)) - .until { - acme.attemptsTo( - Get.resource("/issue-credentials/records/${recordId}") - ) - acme.should( - ResponseConsequence.seeThatResponse("Credential records") { - it.statusCode(200) - } - ) - lastResponse = SerenityRest.lastResponse().jsonPath() - lastResponse.getObject("", Credential::class.java).protocolState == "CredentialSent" - } - } catch (err: ConditionTimeoutException) { - throw ConditionTimeoutException( - "Issuer was unable to issue the credential! Protocol state did not achieve CredentialSent state." - ) - } + wait( + { + acme.attemptsTo( + Get.resource("/issue-credentials/records/${recordId}") + ) + acme.should( + ResponseConsequence.seeThatResponse("Credential records") { + it.statusCode(200) + } + ) + lastResponse().getObject("", Credential::class.java).protocolState == "CredentialSent" + }, + "Issuer was unable to issue the credential! Protocol state did not achieve CredentialSent state." + ) } @Then("Bob has the credential issued") fun bobHasTheCredentialIssued() { - try { - Awaitility.await().withPollInterval(FixedPollInterval(Duration.ofSeconds(3L))) - .atMost(Duration.ofSeconds(60L)) - .until { - bob.attemptsTo( - Get.resource("/issue-credentials/records/${bob.recall("recordId")}") - ) - bob.should( - ResponseConsequence.seeThatResponse("Credential records") { - it.statusCode(200) - } - ) - lastResponse = SerenityRest.lastResponse().jsonPath() - lastResponse.getObject("", Credential::class.java).protocolState == "CredentialReceived" - } - } catch (err: ConditionTimeoutException) { - throw ConditionTimeoutException( - "Holder was unable to receive the credential from Issuer! Protocol state did not achieve CredentialReceived state." - ) - } - println(lastResponse.getObject("items", Credential::class.java)) + wait( + { + bob.attemptsTo( + Get.resource("/issue-credentials/records/${bob.recall("recordId")}") + ) + bob.should( + ResponseConsequence.seeThatResponse("Credential records") { + it.statusCode(200) + } + ) + lastResponse().getObject("", Credential::class.java).protocolState == "CredentialReceived" + }, + "Holder was unable to receive the credential from Issuer! Protocol state did not achieve CredentialReceived state." + ) + val achievedCredential = lastResponse().getObject("", Credential::class.java) + println(achievedCredential) } } diff --git a/tests/e2e-tests/src/test/kotlin/runners/E2eTestsRunner.kt b/tests/e2e-tests/src/test/kotlin/runners/E2eTestsRunner.kt index 95adc98c0e..c99690a5bc 100644 --- a/tests/e2e-tests/src/test/kotlin/runners/E2eTestsRunner.kt +++ b/tests/e2e-tests/src/test/kotlin/runners/E2eTestsRunner.kt @@ -5,16 +5,15 @@ import net.serenitybdd.cucumber.CucumberWithSerenity import org.junit.runner.RunWith @CucumberOptions( - plugin = [ - "pretty", - "json:target/serenity-reports/cucumber_report.json", - "html:target/serenity-reports/cucumber_report.html" - ], features = [ "src/test/resources/features" ], glue = ["features"], - snippets = CucumberOptions.SnippetType.CAMELCASE + snippets = CucumberOptions.SnippetType.CAMELCASE, + plugin = [ + "pretty", + "json:target/serenity-reports/cucumber_report.json" + ], ) @RunWith(CucumberWithSerenity::class) class E2eTestsRunner diff --git a/tests/e2e-tests/src/test/resources/features/issue_credentials/issue_credentials.feature b/tests/e2e-tests/src/test/resources/features/issue_credentials/issue_credentials.feature index be25de38a8..f1b026b611 100644 --- a/tests/e2e-tests/src/test/resources/features/issue_credentials/issue_credentials.feature +++ b/tests/e2e-tests/src/test/resources/features/issue_credentials/issue_credentials.feature @@ -1,10 +1,9 @@ Feature: Issue Credentials - @RFC0453 @AcceptanceTest - Scenario: Issue a credential with the Issuer beginning with an offer with negotiation - Given Acme and Bob agents - And Acme and Bob have an existing connection - And Acme offers a credential - And Bob requests the credential - And Acme issues the credential - Then Bob has the credential issued + @RFC0453 @AcceptanceTest + Scenario: Issue a credential with the Issuer beginning with an offer + Given Acme and Bob have an existing connection + When Acme offers a credential + And Bob requests the credential + And Acme issues the credential + Then Bob has the credential issued