diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index ce047ef1a8..f448dfdae2 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -20,7 +20,7 @@ defaults: jobs: run-integration-tests: - name: "Run e2e tests" + name: "Run integration tests" runs-on: ubuntu-latest env: REPORTS_DIR: "tests/integration-tests/target/site/serenity" @@ -50,49 +50,17 @@ jobs: legacy: true # will also install in PATH as `docker-compose` - name: Build local version of PRISM Agent + id: build_local_prism_agent env: - ENV_FILE: "infrastructure/local/.env" PRISM_AGENT_PATH: "../.." + ENV_FILE: "infrastructure/local/.env" GITHUB_ACTOR: ${{ secrets.ATALA_GITHUB_ACTOR }} GITHUB_TOKEN: ${{ secrets.ATALA_GITHUB_TOKEN }} run: | cd "${PRISM_AGENT_PATH}" || exit 129 sbt docker:publishLocal - PRISM_AGENT_VERSION=$(cut version.sbt -d '=' -f2 | tr -d '" ') - sed -i.bak "s/PRISM_AGENT_VERSION=.*/PRISM_AGENT_VERSION=${PRISM_AGENT_VERSION}/" "${ENV_FILE}" && rm -f "${ENV_FILE}.bak" - cat "${ENV_FILE}" - - - name: Start Cloud Agent for issuer and verifier - env: - PORT: 8080 - ADMIN_TOKEN: "admin" - DEFAULT_WALLET_ENABLED: "false" - API_KEY_AUTO_PROVISIONING: "false" - API_KEY_ENABLED: "true" - DOCKERHOST: "host.docker.internal" - uses: isbang/compose-action@v1.4.1 - with: - compose-file: "./infrastructure/shared/docker-compose-demo.yml" - compose-flags: "--env-file ./infrastructure/local/.env -p issuer" - up-flags: "--wait" - down-flags: "--volumes" - - - name: Start Cloud Agent for holder - env: - PORT: 8090 - ADMIN_TOKEN: admin - DEFAULT_WALLET_ENABLED: true - DEFAULT_WALLET_WEBHOOK_URL: http://host.docker.internal:9956 - DEFAULT_WALLET_AUTH_API_KEY: default - API_KEY_AUTO_PROVISIONING: false - API_KEY_ENABLED: true - DOCKERHOST: "host.docker.internal" - uses: isbang/compose-action@v1.4.1 - with: - compose-file: "./infrastructure/shared/docker-compose-demo.yml" - compose-flags: "--env-file ./infrastructure/local/.env -p holder" - up-flags: "--wait" - down-flags: "--volumes" + echo "open_enterprise_agent_version=$(cut -d'=' -f2 version.sbt | tr -d '" ')" >> "${GITHUB_OUTPUT}" + echo "prism_node_version=$(grep PRISM_NODE_VERSION infrastructure/local/.env | cut -d'=' -f2 | tr -d ' ')" >> "${GITHUB_OUTPUT}" - uses: actions/setup-java@v3 with: @@ -101,6 +69,8 @@ jobs: - name: Run integration tests env: + PRISM_NODE_VERSION: ${{ steps.build_local_prism_agent.outputs.prism_node_version }} + OPEN_ENTERPRISE_AGENT_VERSION: ${{ steps.build_local_prism_agent.outputs.open_enterprise_agent_version }} ATALA_GITHUB_ACTOR: ${{ secrets.ATALA_GITHUB_ACTOR }} ATALA_GITHUB_TOKEN: ${{ secrets.ATALA_GITHUB_TOKEN }} continue-on-error: true @@ -164,6 +134,6 @@ jobs: Failed: ${{ steps.analyze_test_results.outputs.failures }} Errors in tests: ${{ steps.analyze_test_results.outputs.errors }} Skipped (known bugs): ${{ steps.analyze_test_results.outputs.skipped }} - SLACK_TITLE: "Atala PRISM V2 Integration tests: ${{ steps.analyze_test_results.outputs.conclusion }}" + SLACK_TITLE: "Open Enterprise Agent Integration Tests: ${{ steps.analyze_test_results.outputs.conclusion }}" SLACK_USERNAME: circleci SLACK_WEBHOOK: ${{ secrets.E2E_TESTS_SLACK_WEBHOOK }} diff --git a/.github/workflows/prism-unit-tests.yml b/.github/workflows/prism-unit-tests.yml deleted file mode 100644 index 13db3c8f0d..0000000000 --- a/.github/workflows/prism-unit-tests.yml +++ /dev/null @@ -1,21 +0,0 @@ -name: Atala PRISM unit tests - -# Cancel previously running workflows if new commit pushed to the branch -# this will help to push fixes earlier and stop previous workflows -concurrency: - group: ${{ github.head_ref }}${{ github.ref }}-atala-prism - cancel-in-progress: true - -on: - push: - branches: - - "main" - pull_request: - -jobs: - build-and-test-atala-prism: - uses: ./.github/workflows/unit-tests-common.yml - with: - component-name: "Atala PRISM" - component-dir: "." - secrets: inherit diff --git a/.github/workflows/unit-tests-common.yml b/.github/workflows/unit-tests-common.yml deleted file mode 100644 index 26c0ec134d..0000000000 --- a/.github/workflows/unit-tests-common.yml +++ /dev/null @@ -1,77 +0,0 @@ -name: Scala build and unit tests - -on: - workflow_call: - inputs: - component-name: - required: true - type: string - component-dir: - required: true - type: string - measure-coverage: - required: false - type: boolean - default: true - -jobs: - build-and-unit-tests: - name: "Build and unit tests for ${{ inputs.component-name }}" - runs-on: self-hosted - container: - image: ghcr.io/hyperledger-labs/ci-debian-jdk-22:0.1.0 - volumes: - - /nix:/nix - credentials: - username: ${{ secrets.ATALA_GITHUB_ACTOR }} - password: ${{ secrets.ATALA_GITHUB_TOKEN }} - env: - GITHUB_TOKEN: ${{ secrets.ATALA_GITHUB_TOKEN }} - TESTCONTAINERS_RYUK_DISABLED: true - defaults: - run: - working-directory: ${{ inputs.component-dir }} - steps: - - name: Git checkout (merge) - uses: actions/checkout@v3 - if: github.event_name != 'pull_request' - with: - fetch-depth: 0 - - - name: Git checkout (PR) - uses: actions/checkout@v3 - if: github.event_name == 'pull_request' - with: - fetch-depth: 0 - # see: https://frontside.com/blog/2020-05-26-github-actions-pull_request/#how-does-pull_request-affect-actionscheckout - ref: ${{ github.event.pull_request.head.sha }} - - - name: Run Scala formatter - run: sbt scalafmtCheckAll - - - name: Run Unit Tests - run: | - # Workaround for container runners to correctly restore - # https://github.com/actions/runner/issues/863 - echo HOME=/root >> "${GITHUB_ENV}" - sbt -v coverage test coverageAggregate - - - name: Upload coverage data to Coveralls - run: sbt coveralls - env: - COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }} - - - name: Aggregate test reports - if: always() - uses: ./.github/actions/aggregate-test-reports - with: - tests-dir: ${{ inputs.component-dir }} - - - name: Publish test results - # Publish even if the previous test step fails - if: always() - uses: EnricoMi/publish-unit-test-result-action@v2 - with: - junit_files: "${{ inputs.component-dir }}/target/test-reports/**/TEST-*.xml" - comment_title: "${{ inputs.component-name }} Test Results" - check_name: "${{ inputs.component-name }} Test Results" diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml new file mode 100644 index 0000000000..9d0c73dd1a --- /dev/null +++ b/.github/workflows/unit-tests.yml @@ -0,0 +1,69 @@ +name: Unit tests + +# Cancel previously running workflows if new commit pushed to the branch +# this will help to push fixes earlier and stop previous workflows +concurrency: + group: ${{ github.head_ref }}${{ github.ref }}-unit-tests + cancel-in-progress: true + +on: + push: + branches: + - "main" + pull_request: + +jobs: + build-and-unit-tests: + name: "Build and unit tests" + runs-on: self-hosted + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + container: + image: ghcr.io/hyperledger-labs/ci-debian-jdk-22:0.1.0 + volumes: + - /nix:/nix + env: + TESTCONTAINERS_RYUK_DISABLED: true + steps: + - name: Git checkout (merge) + uses: actions/checkout@v3 + if: github.event_name != 'pull_request' + with: + fetch-depth: 0 + + - name: Git checkout (PR) + uses: actions/checkout@v3 + if: github.event_name == 'pull_request' + with: + fetch-depth: 0 + # see: https://frontside.com/blog/2020-05-26-github-actions-pull_request/#how-does-pull_request-affect-actionscheckout + ref: ${{ github.event.pull_request.head.sha }} + + - name: Download dependencies + run: sbt +update + + - name: Check formatting + run: sbt scalafmtCheckAll + + - name: Run unit tests + env: + HOME: /root + run: | + sbt -v coverage test coverageAggregate + + - name: Upload coverage to Coveralls + env: + COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }} + run: sbt coveralls + + - name: Aggregate test reports + if: always() + uses: ./.github/actions/aggregate-test-reports + + - name: Publish test results + if: always() + uses: EnricoMi/publish-unit-test-result-action@v2 + with: + junit_files: "./target/test-reports/**/TEST-*.xml" + comment_title: "Unit Test Results" + check_name: "Unit Test Results" diff --git a/README.md b/README.md index 19a45729d3..07420f4b1e 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@

Coverage Status - Unit tests + Unit tests End-to-end tests Performance tests

diff --git a/tests/integration-tests/build.gradle.kts b/tests/integration-tests/build.gradle.kts index ea7b9a924f..221265ae72 100644 --- a/tests/integration-tests/build.gradle.kts +++ b/tests/integration-tests/build.gradle.kts @@ -44,6 +44,8 @@ dependencies { // Hoplite for configuration implementation("com.sksamuel.hoplite:hoplite-core:2.7.5") implementation("com.sksamuel.hoplite:hoplite-hocon:2.7.5") + // Kotlin compose + testImplementation("org.testcontainers:testcontainers:1.19.1") } buildscript { diff --git a/tests/integration-tests/src/test/kotlin/config/AgentConf.kt b/tests/integration-tests/src/test/kotlin/config/AgentConf.kt index 3918ef0960..38d5e87649 100644 --- a/tests/integration-tests/src/test/kotlin/config/AgentConf.kt +++ b/tests/integration-tests/src/test/kotlin/config/AgentConf.kt @@ -5,7 +5,7 @@ import java.net.URL data class AgentConf( val url: URL, + val apikey: String?, @ConfigAlias("webhook_url") val webhookUrl: URL?, - var apikey: String?, - @ConfigAlias("multi-tenant") val multiTenant: Boolean?, + val init: AgentInitConf?, ) diff --git a/tests/integration-tests/src/test/kotlin/config/AgentInitConf.kt b/tests/integration-tests/src/test/kotlin/config/AgentInitConf.kt new file mode 100644 index 0000000000..3ba21e3cbe --- /dev/null +++ b/tests/integration-tests/src/test/kotlin/config/AgentInitConf.kt @@ -0,0 +1,12 @@ +package config + +import com.sksamuel.hoplite.ConfigAlias + +data class AgentInitConf( + val version: String, + @ConfigAlias("http_port") val httpPort: Int, + @ConfigAlias("didcomm_port") val didcommPort: Int, + @ConfigAlias("secret_storage_backend") val secretStorageBackend: String, + @ConfigAlias("auth_enabled") val authEnabled: Boolean, + @ConfigAlias("keycloak_enabled") val keycloakEnabled: Boolean, +) diff --git a/tests/integration-tests/src/test/kotlin/config/Config.kt b/tests/integration-tests/src/test/kotlin/config/Config.kt index c4abe8b62d..b5f7cd8130 100644 --- a/tests/integration-tests/src/test/kotlin/config/Config.kt +++ b/tests/integration-tests/src/test/kotlin/config/Config.kt @@ -2,8 +2,10 @@ package config data class Config( val global: GlobalConf, + val admin: AgentConf, val issuer: AgentConf, val holder: AgentConf, val verifier: AgentConf, - val admin: AgentConf + val agents: List, + val services: ServicesConf ) diff --git a/tests/integration-tests/src/test/kotlin/config/GlobalConf.kt b/tests/integration-tests/src/test/kotlin/config/GlobalConf.kt index e3e6d89a34..ade1a77c88 100644 --- a/tests/integration-tests/src/test/kotlin/config/GlobalConf.kt +++ b/tests/integration-tests/src/test/kotlin/config/GlobalConf.kt @@ -3,8 +3,6 @@ package config import com.sksamuel.hoplite.ConfigAlias data class GlobalConf( - @ConfigAlias("auth_required") val authRequired: Boolean, - @ConfigAlias("auth_header") val authHeader: String, - @ConfigAlias("admin_auth_header") val adminAuthHeader: String, - @ConfigAlias("admin_apikey") val adminApiKey: String + @ConfigAlias("auth_header") val authHeader: String = "apikey", + @ConfigAlias("admin_auth_header") val adminAuthHeader: String = "x-admin-api-key", ) diff --git a/tests/integration-tests/src/test/kotlin/config/ServicesConf.kt b/tests/integration-tests/src/test/kotlin/config/ServicesConf.kt new file mode 100644 index 0000000000..691afbc4e1 --- /dev/null +++ b/tests/integration-tests/src/test/kotlin/config/ServicesConf.kt @@ -0,0 +1,31 @@ +package config + +import com.sksamuel.hoplite.ConfigAlias + +data class ServicesConf( + @ConfigAlias("prism_node") val prismNode: PrismNodeConf?, + val keycloak: KeycloakConf?, + val vault: VaultConf?, +) + +data class PrismNodeConf( + @ConfigAlias("http_port") val httpPort: Int, + val version: String, +) + +data class KeycloakConf( + @ConfigAlias("http_port") val httpPort: Int, + val realm: String, + @ConfigAlias("client_id") val clientId: String, + @ConfigAlias("client_secret") val clientSecret: String, + val users: List +) + +data class KeycloakUser( + val username: String, + val password: String +) + +data class VaultConf( + @ConfigAlias("http_port") val httpPort: Int, +) diff --git a/tests/integration-tests/src/test/kotlin/features/CommonSteps.kt b/tests/integration-tests/src/test/kotlin/features/CommonSteps.kt index 95a9ab8762..440bea7212 100644 --- a/tests/integration-tests/src/test/kotlin/features/CommonSteps.kt +++ b/tests/integration-tests/src/test/kotlin/features/CommonSteps.kt @@ -2,12 +2,10 @@ package features import com.sksamuel.hoplite.ConfigLoader import common.ListenToEvents -import config.AgentConf -import config.Config +import config.* import features.connection.ConnectionSteps import features.credentials.IssueCredentialsSteps import features.did.PublishDidSteps -import features.multitenancy.EventsSteps import interactions.Get import io.cucumber.java.AfterAll import io.cucumber.java.BeforeAll @@ -23,71 +21,184 @@ import net.serenitybdd.screenplay.actors.Cast import net.serenitybdd.screenplay.actors.OnStage import net.serenitybdd.screenplay.rest.abilities.CallAnApi import org.apache.http.HttpStatus +import org.apache.http.HttpStatus.SC_CREATED import org.apache.http.HttpStatus.SC_OK +import org.testcontainers.containers.ComposeContainer +import org.testcontainers.containers.wait.strategy.Wait +import java.io.File import java.util.* -import kotlin.random.Random - -@OptIn(ExperimentalStdlibApi::class) -fun createWalletAndEntity(agentConf: AgentConf) { - val config = ConfigLoader().loadConfigOrThrow("/tests.conf") - val createWalletResponse = RestAssured - .given().body( - CreateWalletRequest( - name = UUID.randomUUID().toString(), - seed = Random.nextBytes(64).toHexString(), - id = UUID.randomUUID() - ) + +val environments: MutableList = mutableListOf() + +fun initializeVdr(prismNode: PrismNodeConf) { + val vdrEnvironment: ComposeContainer = ComposeContainer( + File("src/test/resources/containers/vdr.yml") + ).withEnv( + mapOf( + "PRISM_NODE_VERSION" to prismNode.version, + "PRISM_NODE_PORT" to prismNode.httpPort.toString() ) - .header(config.global.adminAuthHeader, config.global.adminApiKey) - .post("${agentConf.url}/wallets") - .thenReturn() - Ensure.that(createWalletResponse.statusCode).isEqualTo(HttpStatus.SC_CREATED) - val wallet = createWalletResponse.body.jsonPath().getObject("", WalletDetail::class.java) - val tenantResponse = RestAssured - .given().body( - CreateEntityRequest( - name = UUID.randomUUID().toString(), - walletId = wallet.id - ) + ).waitingFor( + "prism-node", Wait.forLogMessage(".*Server started, listening on.*", 1) + ) + environments.add(vdrEnvironment) + vdrEnvironment.start() +} + +fun initializeKeycloak(keycloakConf: KeycloakConf) { + val keycloakEnvironment: ComposeContainer = ComposeContainer( + File("src/test/resources/containers/keycloak.yml") + ).withEnv( + mapOf( + "KEYCLOAK_HTTP_PORT" to keycloakConf.httpPort.toString(), ) - .header(config.global.adminAuthHeader, config.global.adminApiKey) - .post("${agentConf.url}/iam/entities") - .thenReturn() - Ensure.that(tenantResponse.statusCode).isEqualTo(HttpStatus.SC_CREATED) - val entity = tenantResponse.body.jsonPath().getObject("", EntityResponse::class.java) - val addApiKeyResponse = + ).waitingFor( + "keycloak", Wait.forLogMessage(".*Running the server.*", 1) + ) + environments.add(keycloakEnvironment) + keycloakEnvironment.start() + + // Get admin token + val getAdminTokenResponse = + RestAssured + .given().body("grant_type=password&client_id=admin-cli&username=admin&password=admin") + .contentType("application/x-www-form-urlencoded") + .post("http://localhost:${keycloakConf.httpPort}/realms/master/protocol/openid-connect/token") + .thenReturn() + getAdminTokenResponse.then().statusCode(SC_OK) + val adminToken = getAdminTokenResponse.body.jsonPath().getString("access_token") + + // Create realm + val createRealmResponse = RestAssured .given().body( - ApiKeyAuthenticationRequest( - entityId = entity.id, - apiKey = agentConf.apikey!! + mapOf( + "realm" to keycloakConf.realm, + "enabled" to true, + "accessTokenLifespan" to 3600000 ) ) - .header(config.global.adminAuthHeader, config.global.adminApiKey) - .post("${agentConf.url}/iam/apikey-authentication") - .thenReturn() - Ensure.that(addApiKeyResponse.statusCode).isEqualTo(HttpStatus.SC_CREATED) - val registerIssuerWebhookResponse = + .header("Authorization", "Bearer $adminToken") + .contentType("application/json") + .post("http://localhost:${keycloakConf.httpPort}/admin/realms") + .then().statusCode(SC_CREATED) + + // Create client + val createClientResponse = + RestAssured + .given().body( + mapOf( + "id" to keycloakConf.clientId, + "directAccessGrantsEnabled" to true, + "authorizationServicesEnabled" to true, + "serviceAccountsEnabled" to true, + "secret" to keycloakConf.clientSecret, + )) + .header("Authorization", "Bearer $adminToken") + .contentType("application/json") + .post("http://localhost:${keycloakConf.httpPort}/admin/realms/${keycloakConf.realm}/clients") + .then().statusCode(SC_CREATED) + + // Create users + keycloakConf.users.forEach { keycloakUser -> + RestAssured + .given().body( + mapOf( + "id" to keycloakUser.username, + "username" to keycloakUser.username, + "firstName" to keycloakUser.username, + "enabled" to true, + "credentials" to listOf( + mapOf( + "value" to keycloakUser.password, + "temporary" to false + ) + ) + ) + ) + .header("Authorization", "Bearer $adminToken") + .contentType("application/json") + .post("http://localhost:${keycloakConf.httpPort}/admin/realms/${keycloakConf.realm}/users") + .then().statusCode(SC_CREATED) + } +} + +fun initializeAgent(agentInitConf: AgentInitConf) { + val config = ConfigLoader().loadConfigOrThrow(System.getenv("INTEGRATION_TESTS_CONFIG") ?: "/configs/basic.conf") + val agentConfMap: Map = mapOf( + "OPEN_ENTERPRISE_AGENT_VERSION" to agentInitConf.version, + "API_KEY_ENABLED" to agentInitConf.authEnabled.toString(), + "AUTH_HEADER" to config.global.authHeader, + "ADMIN_AUTH_HEADER" to config.global.adminAuthHeader, + "AGENT_DIDCOMM_PORT" to agentInitConf.didcommPort.toString(), + "AGENT_HTTP_PORT" to agentInitConf.httpPort.toString(), + "PRISM_NODE_PORT" to if (config.services.prismNode != null) + config.services.prismNode.httpPort.toString() else "", + "SECRET_STORAGE_BACKEND" to agentInitConf.secretStorageBackend, + "VAULT_HTTP_PORT" to if (config.services.vault != null && agentInitConf.secretStorageBackend == "vault") + config.services.vault.httpPort.toString() else "", + "KEYCLOAK_ENABLED" to agentInitConf.keycloakEnabled.toString(), + "KEYCLOAK_HTTP_PORT" to if (config.services.keycloak != null && agentInitConf.keycloakEnabled) + config.services.keycloak.httpPort.toString() else "", + "KEYCLOAK_REALM" to if (config.services.keycloak != null && agentInitConf.keycloakEnabled) + config.services.keycloak.realm else "", + "KEYCLOAK_CLIENT_ID" to if (config.services.keycloak != null && agentInitConf.keycloakEnabled) + config.services.keycloak.clientId else "", + "KEYCLOAK_CLIENT_SECRET" to if (config.services.keycloak != null && agentInitConf.keycloakEnabled) + config.services.keycloak.clientSecret else "", + ) + val environment: ComposeContainer = ComposeContainer( + File("src/test/resources/containers/agent.yml") + ).withEnv(agentConfMap).waitingFor("open-enterprise-agent", Wait.forHealthcheck()) + environments.add(environment) + environment.start() +} + +fun initializeWallet(agentConf: AgentConf, bearerToken: String? = "") { + val config = ConfigLoader().loadConfigOrThrow(System.getenv("INTEGRATION_TESTS_CONFIG") ?: "/configs/basic.conf") + val createWalletResponse = + RestAssured + .given().body( + CreateWalletRequest( + name = UUID.randomUUID().toString() + ) + ) + .header("Authorization", "Bearer $bearerToken") + .post("${agentConf.url}/wallets") + .then().statusCode(HttpStatus.SC_CREATED) +} + +fun initializeWebhook(agentConf: AgentConf, bearerToken: String? = "") { + val config = ConfigLoader().loadConfigOrThrow(System.getenv("INTEGRATION_TESTS_CONFIG") ?: "/configs/basic.conf") + val registerWebhookResponse = RestAssured .given().body( CreateWebhookNotification( url = agentConf.webhookUrl!!.toExternalForm() ) ) + .header("Authorization", "Bearer $bearerToken") .header(config.global.authHeader, agentConf.apikey) .post("${agentConf.url}/events/webhooks") + .then().statusCode(HttpStatus.SC_OK) +} + +fun getKeycloakAuthToken(keycloakConf: KeycloakConf, username: String, password: String): String { + val tokenResponse = + RestAssured + .given().body("grant_type=password&client_id=${keycloakConf.clientId}&client_secret=${keycloakConf.clientSecret}&username=${username}&password=${password}") + .contentType("application/x-www-form-urlencoded") + .header("Host", "localhost") + .post("http://localhost:${keycloakConf.httpPort}/realms/${keycloakConf.realm}/protocol/openid-connect/token") .thenReturn() - Ensure.that(registerIssuerWebhookResponse.statusCode).isEqualTo(HttpStatus.SC_CREATED) + tokenResponse.then().statusCode(HttpStatus.SC_OK) + return tokenResponse.body.jsonPath().getString("access_token") } @BeforeAll fun initAgents() { val cast = Cast() - val config = ConfigLoader().loadConfigOrThrow("/tests.conf") - cast.actorNamed( - "Admin", - CallAnApi.at(config.admin.url.toExternalForm()) - ) + val config = ConfigLoader().loadConfigOrThrow(System.getenv("INTEGRATION_TESTS_CONFIG") ?: "/configs/basic.conf") cast.actorNamed( "Acme", CallAnApi.at(config.issuer.url.toExternalForm()), @@ -103,27 +214,62 @@ fun initAgents() { CallAnApi.at(config.verifier.url.toExternalForm()), ListenToEvents.at(config.verifier.webhookUrl!!) ) + cast.actorNamed( + "Admin", + CallAnApi.at(config.admin.url.toExternalForm()) + ) OnStage.setTheStage(cast) - // Create issuer wallet and tenant - if (config.issuer.multiTenant!!) { - createWalletAndEntity(config.issuer) + if (config.services.keycloak != null) { + initializeKeycloak(config.services.keycloak) + } + + if (config.services.prismNode != null) { + initializeVdr(config.services.prismNode) + } + // Initialize the agents + config.agents.forEach { agent -> + initializeAgent(agent) } - // Create verifier wallet - if (config.verifier.multiTenant!!) { - createWalletAndEntity(config.verifier) + + if (config.services.keycloak != null) { + cast.actors.forEach { actor -> + actor.remember("KEYCLOAK_BEARER_TOKEN", getKeycloakAuthToken(config.services.keycloak, actor.name, actor.name)) + when (actor.name) { + "Acme" -> { + initializeWallet(config.issuer, cast.actorNamed(actor.name).recall("KEYCLOAK_BEARER_TOKEN")) + } + "Bob" -> { + initializeWallet(config.holder, cast.actorNamed(actor.name).recall("KEYCLOAK_BEARER_TOKEN")) + } + "Faber" -> { + initializeWallet(config.verifier, cast.actorNamed(actor.name).recall("KEYCLOAK_BEARER_TOKEN")) + } + } + } } + initializeWebhook(config.issuer, cast.actorNamed("Acme").recall("KEYCLOAK_BEARER_TOKEN")) + initializeWebhook(config.holder, cast.actorNamed("Bob").recall("KEYCLOAK_BEARER_TOKEN")) + initializeWebhook(config.verifier, cast.actorNamed("Faber").recall("KEYCLOAK_BEARER_TOKEN")) + cast.actors.forEach { actor -> when (actor.name) { "Acme" -> { actor.remember("AUTH_KEY", config.issuer.apikey) + actor.remember("AUTH_HEADER", config.global.authHeader) } "Bob" -> { actor.remember("AUTH_KEY", config.holder.apikey) + actor.remember("AUTH_HEADER", config.global.authHeader) } "Faber" -> { actor.remember("AUTH_KEY", config.verifier.apikey) + actor.remember("AUTH_HEADER", config.global.authHeader) + } + "Admin" -> { + actor.remember("AUTH_KEY", config.admin.apikey) + actor.remember("AUTH_HEADER", config.global.adminAuthHeader) } } } @@ -132,6 +278,9 @@ fun initAgents() { @AfterAll fun clearStage() { OnStage.drawTheCurtain() + environments.forEach { environment -> + environment.stop() + } } class CommonSteps { diff --git a/tests/integration-tests/src/test/kotlin/interactions/AuthRestInteraction.kt b/tests/integration-tests/src/test/kotlin/interactions/AuthRestInteraction.kt index 881c193dae..d1350973bd 100644 --- a/tests/integration-tests/src/test/kotlin/interactions/AuthRestInteraction.kt +++ b/tests/integration-tests/src/test/kotlin/interactions/AuthRestInteraction.kt @@ -2,23 +2,21 @@ package interactions import com.sksamuel.hoplite.ConfigLoader import config.Config -import io.ktor.util.* import io.restassured.specification.RequestSpecification import net.serenitybdd.screenplay.Actor import net.serenitybdd.screenplay.rest.interactions.RestInteraction abstract class AuthRestInteraction : RestInteraction() { - private val config = ConfigLoader().loadConfigOrThrow("/tests.conf") + private val config = ConfigLoader().loadConfigOrThrow(System.getenv("INTEGRATION_TESTS_CONFIG") ?: "/configs/basic.conf") fun specWithAuthHeaders(actor: T): RequestSpecification { val spec = rest() - if (actor!!.name.toLowerCasePreservingASCIIRules().contains("admin")) { - spec.header(config.global.adminAuthHeader, config.global.adminApiKey) - } else { - if (config.global.authRequired) { - spec.header(config.global.authHeader, actor.recall("AUTH_KEY")) - } + if (config.services.keycloak != null && actor!!.recall("KEYCLOAK_BEARER_TOKEN") != null) { + spec.header("Authorization", "Bearer ${actor!!.recall("KEYCLOAK_BEARER_TOKEN")}") + } + if (actor!!.recall("AUTH_KEY") != null) { + spec.header(actor.recall("AUTH_HEADER"), actor.recall("AUTH_KEY")) } return spec } diff --git a/tests/integration-tests/src/test/resources/configs/basic.conf b/tests/integration-tests/src/test/resources/configs/basic.conf new file mode 100644 index 0000000000..3b30de9fb5 --- /dev/null +++ b/tests/integration-tests/src/test/resources/configs/basic.conf @@ -0,0 +1,47 @@ +# Specify shared services that are used by all agents (if any) +services = { + prism_node = { + http_port = 50053 + version = "${PRISM_NODE_VERSION}" + } +} + +# Specify agents that are required to be created before running tests +agents = [ + { + version = "${OPEN_ENTERPRISE_AGENT_VERSION}" + http_port = 8080 + didcomm_port = 7080 + secret_storage_backend = "postgres" + auth_enabled = true + keycloak_enabled = false + } +] + +global { + auth_header = "${AUTH_HEADER:-apikey}" + admin_auth_header = "${ADMIN_AUTH_HEADER:-x-admin-api-key}" +} + +admin { + url = "${ADMIN_AGENT_URL:-http://localhost:8080}" + apikey = "${ADMIN_API_KEY:-admin}" +} + +issuer { + url = "${ISSUER_AGENT_URL:-http://localhost:8080}" + apikey = "${ISSUER_API_KEY:-${random.string(16)}}" + webhook_url = "${ISSUER_WEBHOOK_URL:-http://host.docker.internal:9955}" +} + +holder { + url = "${HOLDER_AGENT_URL:-http://localhost:8080}" + apikey = "${HOLDER_API_KEY:-${random.string(16)}}" + webhook_url = "${HOLDER_WEBHOOK_URL:-http://host.docker.internal:9956}" +} + +verifier { + url = "${VERIFIER_AGENT_URL:-http://localhost:8080}" + apikey = "${VERIFIER_API_KEY:-${random.string(16)}}" + webhook_url = "${VERIFIER_WEBHOOK_URL:-http://host.docker.internal:9957}" +} diff --git a/tests/integration-tests/src/test/resources/configs/double.conf b/tests/integration-tests/src/test/resources/configs/double.conf new file mode 100644 index 0000000000..871daf4dc7 --- /dev/null +++ b/tests/integration-tests/src/test/resources/configs/double.conf @@ -0,0 +1,75 @@ +# Specify shared services that are used by all agents (if any) +services = { + prism_node = { + http_port = 50053 + version = "${PRISM_NODE_VERSION}" + }, + keycloak = { + http_port = 9980 + realm = "atala-demo" + client_id = "prism-agent" + client_secret = "prism-agent-demo-secret" + users = [ + { + username = "Acme" + password = "Acme" + }, + { + username = "Bob" + password = "Bob" + }, + { + username = "Faber" + password = "Faber" + } + ] + } +} + +# Specify agents that are required to be created before running tests +agents = [ + { + version = "${OPEN_ENTERPRISE_AGENT_VERSION}" + http_port = 8080 + didcomm_port = 7080 + secret_storage_backend = "postgres" + auth_enabled = true + keycloak_enabled = false + }, + { + version = "${OPEN_ENTERPRISE_AGENT_VERSION}" + http_port = 8090 + didcomm_port = 7090 + secret_storage_backend = "postgres" + auth_enabled = false + keycloak_enabled = true + } +] + +global { + auth_header = "${AUTH_HEADER:-apikey}" + admin_auth_header = "${ADMIN_AUTH_HEADER:-x-admin-api-key}" +} + +admin { + url = "${ADMIN_AGENT_URL:-http://localhost:8080}" + apikey = "${ADMIN_API_KEY:-admin}" +} + +issuer { + url = "${ISSUER_AGENT_URL:-http://localhost:8080}" + apikey = "${ISSUER_API_KEY:-${random.string(16)}}" + webhook_url = "${ISSUER_WEBHOOK_URL:-http://host.docker.internal:9955}" +} + +holder { + url = "${HOLDER_AGENT_URL:-http://localhost:8090}" + apikey = "${HOLDER_API_KEY:-${random.string(16)}}" + webhook_url = "${HOLDER_WEBHOOK_URL:-http://host.docker.internal:9956}" +} + +verifier { + url = "${VERIFIER_AGENT_URL:-http://localhost:8080}" + apikey = "${VERIFIER_API_KEY:-${random.string(16)}}" + webhook_url = "${VERIFIER_WEBHOOK_URL:-http://host.docker.internal:9957}" +} diff --git a/tests/integration-tests/src/test/resources/configs/keycloak.conf b/tests/integration-tests/src/test/resources/configs/keycloak.conf new file mode 100644 index 0000000000..5f42199e48 --- /dev/null +++ b/tests/integration-tests/src/test/resources/configs/keycloak.conf @@ -0,0 +1,70 @@ +# Specify shared services that are used by all agents (if any) +services = { + prism_node = { + http_port = 50053 + version = "${PRISM_NODE_VERSION}" + } + keycloak = { + http_port = 9980 + realm = "atala-demo" + client_id = "prism-agent" + client_secret = "prism-agent-demo-secret" + users = [ + { + username = "Acme" + password = "Acme" + }, + { + username = "Bob" + password = "Bob" + }, + { + username = "Faber" + password = "Faber" + } + ] + } + vault = { + http_port = 8200 + } +} + +# Specify agents that are required to be created before running tests +agents = [ + { + version = "${OPEN_ENTERPRISE_AGENT_VERSION}" + http_port = 8080 + didcomm_port = 7080 + secret_storage_backend = "postgres" # can be vault + auth_enabled = false + keycloak_enabled = true + } +] + +global { + auth_header = "${AUTH_HEADER:-apikey}" + admin_auth_header = "${ADMIN_AUTH_HEADER:-x-admin-api-key}" +} + +admin { + url = "${ADMIN_AGENT_URL:-http://localhost:8080}" + apikey = "${ADMIN_API_KEY:-admin}" +} + +issuer { + url = "${ISSUER_AGENT_URL:-http://localhost:8080}" + apikey = "${ISSUER_API_KEY:-${random.string(16)}}" + webhook_url = "${ISSUER_WEBHOOK_URL:-http://host.docker.internal:9955}" +} + +holder { + url = "${HOLDER_AGENT_URL:-http://localhost:8080}" + apikey = "${HOLDER_API_KEY:-${random.string(16)}}" + webhook_url = "${HOLDER_WEBHOOK_URL:-http://host.docker.internal:9956}" +} + +verifier { + url = "${VERIFIER_AGENT_URL:-http://localhost:8080}" + apikey = "${VERIFIER_API_KEY:-${random.string(16)}}" + webhook_url = "${VERIFIER_WEBHOOK_URL:-http://host.docker.internal:9957}" +} diff --git a/tests/integration-tests/src/test/resources/containers/agent.yml b/tests/integration-tests/src/test/resources/containers/agent.yml new file mode 100644 index 0000000000..c82e740a54 --- /dev/null +++ b/tests/integration-tests/src/test/resources/containers/agent.yml @@ -0,0 +1,66 @@ +--- +version: "3.8" + +services: + + # Mandatory PostgreSQL database for the Open Enterprise Agent + postgres: + image: postgres:13 + environment: + POSTGRES_MULTIPLE_DATABASES: "castor,pollux,connect,agent" + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + volumes: + - ./postgres/init-script.sh:/docker-entrypoint-initdb.d/init-script.sh + - ./postgres/max_conns.sql:/docker-entrypoint-initdb.d/max_conns.sql + healthcheck: + test: ["CMD", "pg_isready", "-U", "postgres", "-d", "agent"] + interval: 10s + timeout: 5s + retries: 5 + + # Secret storage - hashicorp + + # Open Enterprise Agent + open-enterprise-agent: + image: ghcr.io/input-output-hk/prism-agent:${OPEN_ENTERPRISE_AGENT_VERSION} + environment: + PRISM_NODE_HOST: host.docker.internal + PRISM_NODE_PORT: + CASTOR_DB_HOST: postgres + POLLUX_DB_HOST: postgres + CONNECT_DB_HOST: postgres + AGENT_DB_HOST: postgres + VAULT_TOKEN: root + # Configuration parameters + AGENT_DIDCOMM_PORT: + AGENT_HTTP_PORT: + DIDCOMM_SERVICE_URL: "http://host.docker.internal:${AGENT_DIDCOMM_PORT}" + REST_SERVICE_URL: "http://host.docker.internal:${AGENT_HTTP_PORT}" + AUTH_HEADER: + ADMIN_AUTH_HEADER: + API_KEY_ENABLED: + # Secret storage configuration + SECRET_STORAGE_BACKEND: + VAULT_ADDR: "http://host.docker.internal:${VAULT_HTTP_PORT}" + # Keycloak configuration + KEYCLOAK_ENABLED: + KEYCLOAK_URL: "http://host.docker.internal:${KEYCLOAK_HTTP_PORT}" + KEYCLOAK_REALM: + KEYCLOAK_CLIENT_ID: + KEYCLOAK_CLIENT_SECRET: + KEYCLOAK_UMA_AUTO_UPGRADE_RPT: true # no configurable at the moment + depends_on: + postgres: + condition: service_healthy + ports: + - "${AGENT_DIDCOMM_PORT}:${AGENT_DIDCOMM_PORT}" + - "${AGENT_HTTP_PORT}:${AGENT_HTTP_PORT}" + healthcheck: + test: ["CMD", "curl", "-f", "http://open-enterprise-agent:${AGENT_HTTP_PORT}/_system/health"] + interval: 10s + timeout: 5s + retries: 5 + # Extra hosts for Linux networking + extra_hosts: + - "host.docker.internal:host-gateway" diff --git a/tests/integration-tests/src/test/resources/containers/keycloak.yml b/tests/integration-tests/src/test/resources/containers/keycloak.yml new file mode 100644 index 0000000000..b98a83db3f --- /dev/null +++ b/tests/integration-tests/src/test/resources/containers/keycloak.yml @@ -0,0 +1,13 @@ +--- +version: "3.8" + +services: + + keycloak: + image: quay.io/keycloak/keycloak:22.0.4 + ports: + - "${KEYCLOAK_HTTP_PORT}:8080" + environment: + KEYCLOAK_ADMIN: admin + KEYCLOAK_ADMIN_PASSWORD: admin + command: start-dev --health-enabled=true --hostname-url=http://localhost:${KEYCLOAK_HTTP_PORT} diff --git a/tests/integration-tests/src/test/resources/containers/postgres/init-script.sh b/tests/integration-tests/src/test/resources/containers/postgres/init-script.sh new file mode 100755 index 0000000000..408264cf1e --- /dev/null +++ b/tests/integration-tests/src/test/resources/containers/postgres/init-script.sh @@ -0,0 +1,24 @@ +#!/bin/bash + +set -e +set -u + +function create_user_and_database() { + local database=$1 + local app_user=${database}-application-user + echo " Creating user and database '$database'" + psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" <<-EOSQL + CREATE USER "$app_user" WITH PASSWORD 'password'; + CREATE DATABASE $database; + \c $database + ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO "$app_user"; + EOSQL +} + +if [ -n "$POSTGRES_MULTIPLE_DATABASES" ]; then + echo "Multiple database creation requested: $POSTGRES_MULTIPLE_DATABASES" + for db in $(echo $POSTGRES_MULTIPLE_DATABASES | tr ',' ' '); do + create_user_and_database $db + done + echo "Multiple databases created" +fi diff --git a/tests/integration-tests/src/test/resources/containers/postgres/max_conns.sql b/tests/integration-tests/src/test/resources/containers/postgres/max_conns.sql new file mode 100644 index 0000000000..f2a343e505 --- /dev/null +++ b/tests/integration-tests/src/test/resources/containers/postgres/max_conns.sql @@ -0,0 +1 @@ +ALTER SYSTEM SET max_connections = 500; diff --git a/tests/integration-tests/src/test/resources/containers/vault.yml b/tests/integration-tests/src/test/resources/containers/vault.yml new file mode 100644 index 0000000000..a762c5dc0b --- /dev/null +++ b/tests/integration-tests/src/test/resources/containers/vault.yml @@ -0,0 +1,20 @@ +--- +version: "3.8" + +services: + + vault-server: + image: hashicorp/vault:1.13.3 + ports: + - "${VAULT_PORT}:8200" + environment: + VAULT_ADDR: "http://0.0.0.0:8200" + VAULT_DEV_ROOT_TOKEN_ID: root + command: server -dev -dev-root-token-id=root + cap_add: + - IPC_LOCK + healthcheck: + test: ["CMD", "vault", "status"] + interval: 10s + timeout: 5s + retries: 5 diff --git a/tests/integration-tests/src/test/resources/containers/vdr.yml b/tests/integration-tests/src/test/resources/containers/vdr.yml new file mode 100644 index 0000000000..2bc432bbc6 --- /dev/null +++ b/tests/integration-tests/src/test/resources/containers/vdr.yml @@ -0,0 +1,28 @@ +--- +version: "3.8" + +services: + + node-db: + image: postgres:13 + environment: + POSTGRES_DB: "node_db" + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + healthcheck: + test: ["CMD", "pg_isready", "-U", "postgres", "-d", "node_db"] + interval: 10s + timeout: 5s + retries: 5 + + prism-node: + image: ghcr.io/input-output-hk/prism-node:${PRISM_NODE_VERSION} + environment: + NODE_PSQL_HOST: node-db:5432 + NODE_REFRESH_AND_SUBMIT_PERIOD: 1s + NODE_MOVE_SCHEDULED_TO_PENDING_PERIOD: 1s + ports: + - "${PRISM_NODE_PORT}:50053" + depends_on: + node-db: + condition: service_healthy diff --git a/tests/integration-tests/src/test/resources/tests.conf b/tests/integration-tests/src/test/resources/tests.conf deleted file mode 100644 index bd1065b8e6..0000000000 --- a/tests/integration-tests/src/test/resources/tests.conf +++ /dev/null @@ -1,32 +0,0 @@ -global { - auth_required = true - auth_header = "${AUTH_HEADER:-apikey}" - admin_auth_header = "${ADMIN_AUTH_HEADER:-x-admin-api-key}" - admin_apikey = "${ADMIN_API_KEY:-admin}" -} - -admin { - url = "${ADMIN_AGENT_URL:-http://localhost:8080/prism-agent}" - apikey = "${ISSUER_API_KEY:-${random.string(16)}}" -} - -issuer { - url = "${ISSUER_AGENT_URL:-http://localhost:8080/prism-agent}" - webhook_url = "${ISSUER_WEBHOOK_URL:-http://host.docker.internal:9955}" - apikey = "${ISSUER_API_KEY:-${random.string(16)}}" - multi-tenant = true -} - -verifier { - url = "${VERIFIER_AGENT_URL:-http://localhost:8080/prism-agent}" - webhook_url = "${VERIFIER_WEBHOOK_URL:-http://host.docker.internal:9957}" - apikey = "${VERIFIER_API_KEY:-${random.string(16)}}" - multi-tenant = true -} - -holder { - url = "${HOLDER_AGENT_URL:-http://localhost:8090/prism-agent}" - webhook_url = "${HOLDER_WEBHOOK_URL:-http://host.docker.internal:9956}" - apikey = "${HOLDER_API_KEY:-default}" - multi-tenant = false -}