From 6c61a60ba67ce1649c0a63da662bedef0dffcd22 Mon Sep 17 00:00:00 2001 From: patlo-iog Date: Mon, 1 Jul 2024 20:53:48 +0700 Subject: [PATCH] test: add oid4vci issuer crud scenario (#1234) Signed-off-by: Hyperledger Bot Signed-off-by: Pat Losoponkul Co-authored-by: Hyperledger Bot Signed-off-by: Pat Losoponkul --- tests/integration-tests/build.gradle.kts | 2 +- .../src/test/kotlin/config/Role.kt | 1 + .../test/kotlin/config/services/Keycloak.kt | 5 +- .../test/kotlin/config/services/Service.kt | 1 + .../src/test/kotlin/steps/Setup.kt | 17 +- .../oid4vci/ManageCredentialConfigSteps.kt | 75 +++++++++ .../kotlin/steps/oid4vci/ManageIssuerSteps.kt | 146 ++++++++++++++++++ .../src/test/resources/configs/basic.conf | 11 +- .../test/resources/configs/mt_keycloak.conf | 11 +- .../resources/containers/keycloak-oid4vci.yml | 13 ++ .../oid4vci/manage_credential_config.feature | 16 ++ .../features/oid4vci/manage_issuer.feature | 19 +++ 12 files changed, 309 insertions(+), 8 deletions(-) create mode 100644 tests/integration-tests/src/test/kotlin/steps/oid4vci/ManageCredentialConfigSteps.kt create mode 100644 tests/integration-tests/src/test/kotlin/steps/oid4vci/ManageIssuerSteps.kt create mode 100644 tests/integration-tests/src/test/resources/containers/keycloak-oid4vci.yml create mode 100644 tests/integration-tests/src/test/resources/features/oid4vci/manage_credential_config.feature create mode 100644 tests/integration-tests/src/test/resources/features/oid4vci/manage_issuer.feature diff --git a/tests/integration-tests/build.gradle.kts b/tests/integration-tests/build.gradle.kts index 177a614b9c..00ba4ab3cd 100644 --- a/tests/integration-tests/build.gradle.kts +++ b/tests/integration-tests/build.gradle.kts @@ -33,7 +33,7 @@ dependencies { testImplementation("io.ktor:ktor-server-netty:2.3.0") testImplementation("io.ktor:ktor-client-apache:2.3.0") // RestAPI client - testImplementation("org.hyperledger.identus:cloud-agent-client-kotlin:1.36.1") + testImplementation("org.hyperledger.identus:cloud-agent-client-kotlin:1.37.0") // Test helpers library testImplementation("io.iohk.atala:atala-automation:0.4.0") // Hoplite for configuration diff --git a/tests/integration-tests/src/test/kotlin/config/Role.kt b/tests/integration-tests/src/test/kotlin/config/Role.kt index 34d2063f3d..22c1369404 100644 --- a/tests/integration-tests/src/test/kotlin/config/Role.kt +++ b/tests/integration-tests/src/test/kotlin/config/Role.kt @@ -10,4 +10,5 @@ data class Role( val authHeader: String = "apikey", val webhook: Webhook?, val agentRole: AgentRole?, + val oid4vciAuthServer: URL?, ) diff --git a/tests/integration-tests/src/test/kotlin/config/services/Keycloak.kt b/tests/integration-tests/src/test/kotlin/config/services/Keycloak.kt index e3a2d8475d..fbe8c94602 100644 --- a/tests/integration-tests/src/test/kotlin/config/services/Keycloak.kt +++ b/tests/integration-tests/src/test/kotlin/config/services/Keycloak.kt @@ -18,10 +18,11 @@ data class Keycloak( @ConfigAlias("client_id") val clientId: String = "prism-agent", @ConfigAlias("client_secret") val clientSecret: String = "prism-agent-demo-secret", @ConfigAlias("keep_running") override val keepRunning: Boolean = false, + @ConfigAlias("compose_file") val keycloakComposeFile: String = "src/test/resources/containers/keycloak.yml", + @ConfigAlias("extra_envs") val extraEnvs: Map = emptyMap(), ) : ServiceBase() { private val logger = Logger.get() - private val keycloakComposeFile = "src/test/resources/containers/keycloak.yml" - private val keycloakEnvConfig: Map = mapOf( + private val keycloakEnvConfig: Map = extraEnvs + mapOf( "KEYCLOAK_HTTP_PORT" to httpPort.toString(), ) override val logServices: List = listOf("keycloak") diff --git a/tests/integration-tests/src/test/kotlin/config/services/Service.kt b/tests/integration-tests/src/test/kotlin/config/services/Service.kt index ee897f6e91..ba54392358 100644 --- a/tests/integration-tests/src/test/kotlin/config/services/Service.kt +++ b/tests/integration-tests/src/test/kotlin/config/services/Service.kt @@ -5,5 +5,6 @@ import com.sksamuel.hoplite.ConfigAlias data class Service( @ConfigAlias("prism_node") val prismNode: VerifiableDataRegistry?, val keycloak: Keycloak?, + @ConfigAlias("keycloak_oid4vci") val keycloakOid4vci: Keycloak, val vault: Vault?, ) diff --git a/tests/integration-tests/src/test/kotlin/steps/Setup.kt b/tests/integration-tests/src/test/kotlin/steps/Setup.kt index 57b035ab9d..37701683fd 100644 --- a/tests/integration-tests/src/test/kotlin/steps/Setup.kt +++ b/tests/integration-tests/src/test/kotlin/steps/Setup.kt @@ -4,8 +4,7 @@ import abilities.ListenToEvents import com.sksamuel.hoplite.ConfigException import com.sksamuel.hoplite.ConfigLoader import common.TestConstants -import config.AgentRole -import config.Config +import config.* import io.cucumber.java.AfterAll import io.cucumber.java.BeforeAll import io.restassured.RestAssured @@ -35,6 +34,7 @@ object Setup { */ fun initServices() { config.services?.keycloak?.setUsers(config.roles)?.start() + config.services?.keycloakOid4vci?.setUsers(config.roles.filter { it.name == "Holder" })?.start() config.services?.prismNode?.start() config.services?.vault?.start() config.agents?.forEach { @@ -100,7 +100,10 @@ object Setup { config.roles.forEach { role -> val actor = cast.actorNamed(role.name) try { - actor.remember("BEARER_TOKEN", config.services.keycloak.getKeycloakAuthToken(actor.name, actor.name)) + actor.remember( + "BEARER_TOKEN", + config.services.keycloak.getKeycloakAuthToken(actor.name, actor.name), + ) } catch (e: NullPointerException) { throw ConfigException("Keycloak is configured, but no token found for user ${actor.name}!") } @@ -126,6 +129,14 @@ object Setup { } actor.remember("baseUrl", role.url.toExternalForm()) } + if (config.services?.keycloakOid4vci != null) { + val role = config.roles.find { it.name == "Issuer" } ?: throw ConfigException("Issuer role does not exist") + val url = role.oid4vciAuthServer ?: throw ConfigException("Issuer's oid4vci_auth_server must be provided") + val actor = cast.actorNamed(role.name) + actor.remember("OID4VCI_AUTH_SERVER_URL", url.toExternalForm()) + actor.remember("OID4VCI_AUTH_SERVER_CLIENT_ID", config.services.keycloakOid4vci.clientId) + actor.remember("OID4VCI_AUTH_SERVER_CLIENT_SECRET", config.services.keycloakOid4vci.clientSecret) + } OnStage.setTheStage(cast) } diff --git a/tests/integration-tests/src/test/kotlin/steps/oid4vci/ManageCredentialConfigSteps.kt b/tests/integration-tests/src/test/kotlin/steps/oid4vci/ManageCredentialConfigSteps.kt new file mode 100644 index 0000000000..470babb703 --- /dev/null +++ b/tests/integration-tests/src/test/kotlin/steps/oid4vci/ManageCredentialConfigSteps.kt @@ -0,0 +1,75 @@ +package steps.oid4vci + +import common.CredentialSchema +import interactions.* +import io.cucumber.java.en.* +import io.iohk.atala.automation.extensions.get +import io.iohk.atala.automation.serenity.ensure.Ensure +import net.serenitybdd.rest.SerenityRest +import net.serenitybdd.screenplay.Actor +import org.apache.http.HttpStatus +import org.hyperledger.identus.client.models.* + +class ManageCredentialConfigSteps { + @Given("{actor} has {string} credential configuration created from {}") + fun issuerHasExistingCredentialConfig(issuer: Actor, configurationId: String, schema: CredentialSchema) { + ManageIssuerSteps().issuerHasExistingCredentialIssuer(issuer) + issuerCreateCredentialConfiguration(issuer, schema, configurationId) + } + + @When("{actor} uses {} to create a credential configuration {string}") + fun issuerCreateCredentialConfiguration(issuer: Actor, schema: CredentialSchema, configurationId: String) { + val credentialIssuer = issuer.recall("oid4vciCredentialIssuer") + val schemaGuid = issuer.recall(schema.name) + val baseUrl = issuer.recall("baseUrl") + issuer.attemptsTo( + Post.to("/oid4vci/issuers/${credentialIssuer.id}/credential-configurations") + .with { + it.body( + CreateCredentialConfigurationRequest( + configurationId = configurationId, + format = CredentialFormat.JWT_VC_JSON, + schemaId = "$baseUrl/schema-registry/schemas/$schemaGuid/schema", + ), + ) + }, + Ensure.thatTheLastResponse().statusCode().isEqualTo(HttpStatus.SC_CREATED), + ) + } + + @When("{actor} deletes {string} credential configuration") + fun issuerDeletesCredentialConfiguration(issuer: Actor, configurationId: String) { + val credentialIssuer = issuer.recall("oid4vciCredentialIssuer") + issuer.attemptsTo( + Delete("/oid4vci/issuers/${credentialIssuer.id}/credential-configurations/$configurationId"), + Ensure.thatTheLastResponse().statusCode().isEqualTo(HttpStatus.SC_OK), + ) + } + + @Then("{actor} sees the {string} configuration on IssuerMetadata endpoint") + fun issuerSeesCredentialConfiguration(issuer: Actor, configurationId: String) { + val credentialIssuer = issuer.recall("oid4vciCredentialIssuer") + issuer.attemptsTo( + Get("/oid4vci/issuers/${credentialIssuer.id}/.well-known/openid-credential-issuer"), + Ensure.thatTheLastResponse().statusCode().isEqualTo(HttpStatus.SC_OK), + ) + val metadata = SerenityRest.lastResponse().get() + val credConfig = metadata.credentialConfigurationsSupported[configurationId]!! + issuer.attemptsTo( + Ensure.that(credConfig.scope).isEqualTo(configurationId), + ) + } + + @Then("{actor} cannot see the {string} configuration on IssuerMetadata endpoint") + fun issuerCannotSeeCredentialConfiguration(issuer: Actor, configurationId: String) { + val credentialIssuer = issuer.recall("oid4vciCredentialIssuer") + issuer.attemptsTo( + Get("/oid4vci/issuers/${credentialIssuer.id}/.well-known/openid-credential-issuer"), + Ensure.thatTheLastResponse().statusCode().isEqualTo(HttpStatus.SC_OK), + ) + val metadata = SerenityRest.lastResponse().get() + issuer.attemptsTo( + Ensure.that(metadata.credentialConfigurationsSupported.keys).doesNotContain(configurationId), + ) + } +} diff --git a/tests/integration-tests/src/test/kotlin/steps/oid4vci/ManageIssuerSteps.kt b/tests/integration-tests/src/test/kotlin/steps/oid4vci/ManageIssuerSteps.kt new file mode 100644 index 0000000000..6fd2f2a3b1 --- /dev/null +++ b/tests/integration-tests/src/test/kotlin/steps/oid4vci/ManageIssuerSteps.kt @@ -0,0 +1,146 @@ +package steps.oid4vci + +import interactions.* +import io.cucumber.java.en.* +import io.iohk.atala.automation.extensions.get +import io.iohk.atala.automation.serenity.ensure.Ensure +import net.serenitybdd.rest.SerenityRest +import net.serenitybdd.screenplay.Actor +import org.apache.http.HttpStatus +import org.apache.http.HttpStatus.SC_CREATED +import org.apache.http.HttpStatus.SC_OK +import org.hyperledger.identus.client.models.* + +class ManageIssuerSteps { + private val UPDATE_AUTH_SERVER_URL = "http://example.com" + private val UPDATE_AUTH_SERVER_CLIENT_ID = "foo" + private val UPDATE_AUTH_SERVER_CLIENT_SECRET = "bar" + + @Given("{actor} has an existing oid4vci issuer") + fun issuerHasExistingCredentialIssuer(issuer: Actor) { + issuerCreateCredentialIssuer(issuer) + } + + @When("{actor} creates an oid4vci issuer") + fun issuerCreateCredentialIssuer(issuer: Actor) { + issuer.attemptsTo( + Post.to("/oid4vci/issuers") + .with { + it.body( + CreateCredentialIssuerRequest( + authorizationServer = AuthorizationServer( + url = issuer.recall("OID4VCI_AUTH_SERVER_URL"), + clientId = issuer.recall("OID4VCI_AUTH_SERVER_CLIENT_ID"), + clientSecret = issuer.recall("OID4VCI_AUTH_SERVER_CLIENT_SECRET"), + ), + ), + ) + }, + Ensure.thatTheLastResponse().statusCode().isEqualTo(SC_CREATED), + ) + val credentialIssuer = SerenityRest.lastResponse().get() + issuer.remember("oid4vciCredentialIssuer", credentialIssuer) + } + + @Then("{actor} sees the oid4vci issuer exists on the agent") + fun issuerSeesCredentialIssuerExists(issuer: Actor) { + val credentialIssuer = issuer.recall("oid4vciCredentialIssuer") + issuer.attemptsTo( + Get("/oid4vci/issuers"), + Ensure.thatTheLastResponse().statusCode().isEqualTo(SC_OK), + ) + val matchedIssuers = SerenityRest.lastResponse().get().contents!! + .filter { it.id == credentialIssuer.id } + issuer.attemptsTo( + Ensure.that(matchedIssuers).hasSize(1), + ) + } + + @Then("{actor} sees the oid4vci issuer on IssuerMetadata endpoint") + fun issuerSeesCredentialIssuerExistsOnMetadataEndpoint(issuer: Actor) { + val credentialIssuer = issuer.recall("oid4vciCredentialIssuer") + issuer.attemptsTo( + Get("/oid4vci/issuers/${credentialIssuer.id}/.well-known/openid-credential-issuer"), + Ensure.thatTheLastResponse().statusCode().isEqualTo(SC_OK), + ) + } + + @When("{actor} updates the oid4vci issuer") + fun issuerUpdateCredentialIssuer(issuer: Actor) { + val credentialIssuer = issuer.recall("oid4vciCredentialIssuer") + issuer.attemptsTo( + Patch.to("/oid4vci/issuers/${credentialIssuer.id}") + .with { + it.body( + PatchCredentialIssuerRequest( + authorizationServer = PatchAuthorizationServer( + url = UPDATE_AUTH_SERVER_URL, + clientId = UPDATE_AUTH_SERVER_CLIENT_ID, + clientSecret = UPDATE_AUTH_SERVER_CLIENT_SECRET, + ), + ), + ) + }, + Ensure.thatTheLastResponse().statusCode().isEqualTo(HttpStatus.SC_OK), + ) + } + + @When("{actor} deletes the oid4vci issuer") + fun issuerDeleteCredentialIssuer(issuer: Actor) { + val credentialIssuer = issuer.recall("oid4vciCredentialIssuer") + issuer.attemptsTo( + Delete("/oid4vci/issuers/${credentialIssuer.id}"), + Ensure.thatTheLastResponse().statusCode().isEqualTo(HttpStatus.SC_OK), + ) + } + + @Then("{actor} sees the oid4vci issuer updated with new values") + fun issuerSeesUpdatedCredentialIssuer(issuer: Actor) { + val credentialIssuer = issuer.recall("oid4vciCredentialIssuer") + issuer.attemptsTo( + Get("/oid4vci/issuers"), + Ensure.thatTheLastResponse().statusCode().isEqualTo(HttpStatus.SC_OK), + ) + val updatedIssuer = SerenityRest.lastResponse().get().contents!! + .find { it.id == credentialIssuer.id }!! + issuer.attemptsTo( + Ensure.that(updatedIssuer.authorizationServerUrl).isEqualTo(UPDATE_AUTH_SERVER_URL), + ) + } + + @Then("{actor} sees the oid4vci IssuerMetadata endpoint updated with new values") + fun issuerSeesUpdatedCredentialIssuerMetadata(issuer: Actor) { + val credentialIssuer = issuer.recall("oid4vciCredentialIssuer") + issuer.attemptsTo( + Get("/oid4vci/issuers/${credentialIssuer.id}/.well-known/openid-credential-issuer"), + Ensure.thatTheLastResponse().statusCode().isEqualTo(HttpStatus.SC_OK), + ) + val metadata = SerenityRest.lastResponse().get() + issuer.attemptsTo( + Ensure.that(metadata.authorizationServers?.first()!!).isEqualTo(UPDATE_AUTH_SERVER_URL), + ) + } + + @Then("{actor} cannot see the oid4vci issuer on the agent") + fun issuerCannotSeeCredentialIssuer(issuer: Actor) { + val credentialIssuer = issuer.recall("oid4vciCredentialIssuer") + issuer.attemptsTo( + Get("/oid4vci/issuers"), + Ensure.thatTheLastResponse().statusCode().isEqualTo(HttpStatus.SC_OK), + ) + val matchedIssuers = SerenityRest.lastResponse().get().contents!! + .filter { it.id == credentialIssuer.id } + issuer.attemptsTo( + Ensure.that(matchedIssuers).isEmpty(), + ) + } + + @Then("{actor} cannot see the oid4vci IssuerMetadata endpoint") + fun issuerCannotSeeIssuerMetadata(issuer: Actor) { + val credentialIssuer = issuer.recall("oid4vciCredentialIssuer") + issuer.attemptsTo( + Get("/oid4vci/issuers/${credentialIssuer.id}/.well-known/openid-credential-issuer"), + Ensure.thatTheLastResponse().statusCode().isEqualTo(HttpStatus.SC_NOT_FOUND), + ) + } +} diff --git a/tests/integration-tests/src/test/resources/configs/basic.conf b/tests/integration-tests/src/test/resources/configs/basic.conf index 1b0436df79..954af03916 100644 --- a/tests/integration-tests/src/test/resources/configs/basic.conf +++ b/tests/integration-tests/src/test/resources/configs/basic.conf @@ -4,6 +4,14 @@ services = { http_port = 50053 version = "${PRISM_NODE_VERSION}" } + keycloak_oid4vci = { + http_port = 9981 + compose_file = "src/test/resources/containers/keycloak-oid4vci.yml" + realm = "oid4vci-holder" + extra_envs = { + IDENTUS_URL = "${ISSUER_AGENT_URL:-http://localhost:8080}" + } + } } # Specify agents that are required to be created before running tests @@ -23,7 +31,7 @@ roles = [ url = "${ADMIN_AGENT_URL:-http://localhost:8080}" apikey = "${ADMIN_API_KEY:-admin}" auth_header = "x-admin-api-key" - } + }, { name = "Issuer" url = "${ISSUER_AGENT_URL:-http://localhost:8080}" @@ -32,6 +40,7 @@ roles = [ url = "${ISSUER_WEBHOOK_URL:-http://host.docker.internal:9955}" init_required = true } + oid4vci_auth_server = "http://localhost:9981" }, { name = "Holder" diff --git a/tests/integration-tests/src/test/resources/configs/mt_keycloak.conf b/tests/integration-tests/src/test/resources/configs/mt_keycloak.conf index fa4f207525..b240ff1705 100644 --- a/tests/integration-tests/src/test/resources/configs/mt_keycloak.conf +++ b/tests/integration-tests/src/test/resources/configs/mt_keycloak.conf @@ -7,6 +7,14 @@ services = { keycloak = { http_port = 9980 } + keycloak_oid4vci = { + http_port = 9981 + compose_file = "src/test/resources/containers/keycloak-oid4vci.yml" + realm = "oid4vci-holder" + extra_envs = { + IDENTUS_URL = "${ISSUER_AGENT_URL:-http://localhost:8080}" + } + } } # Specify agents that are required to be created before running tests @@ -27,7 +35,7 @@ roles = [ url = "${ADMIN_AGENT_URL:-http://localhost:8080}" apikey = "${ADMIN_API_KEY:-admin}" auth_header = "x-admin-api-key" - } + }, { name = "Issuer" url = "${ISSUER_AGENT_URL:-http://localhost:8080}" @@ -35,6 +43,7 @@ roles = [ url = "${ISSUER_WEBHOOK_URL:-http://host.docker.internal:9955}" init_required = true } + oid4vci_auth_server = "http://localhost:9981" }, { name = "Holder" diff --git a/tests/integration-tests/src/test/resources/containers/keycloak-oid4vci.yml b/tests/integration-tests/src/test/resources/containers/keycloak-oid4vci.yml new file mode 100644 index 0000000000..2dae22b4ae --- /dev/null +++ b/tests/integration-tests/src/test/resources/containers/keycloak-oid4vci.yml @@ -0,0 +1,13 @@ +--- +version: "3.8" + +services: + keycloak: + image: ghcr.io/hyperledger/identus-keycloak-plugins:0.1.0 + ports: + - "${KEYCLOAK_HTTP_PORT}:8080" + environment: + KEYCLOAK_ADMIN: admin + KEYCLOAK_ADMIN_PASSWORD: admin + IDENTUS_URL: + command: start-dev --health-enabled=true --hostname-url=http://localhost:${KEYCLOAK_HTTP_PORT} diff --git a/tests/integration-tests/src/test/resources/features/oid4vci/manage_credential_config.feature b/tests/integration-tests/src/test/resources/features/oid4vci/manage_credential_config.feature new file mode 100644 index 0000000000..3253069abe --- /dev/null +++ b/tests/integration-tests/src/test/resources/features/oid4vci/manage_credential_config.feature @@ -0,0 +1,16 @@ +@oid4vci +Feature: Manage OID4VCI credential configuration + +Background: + Given Issuer has a published DID for JWT + And Issuer has published STUDENT_SCHEMA schema + And Issuer has an existing oid4vci issuer + +Scenario: Successfully create credential configuration + When Issuer uses STUDENT_SCHEMA to create a credential configuration "StudentProfile" + Then Issuer sees the "StudentProfile" configuration on IssuerMetadata endpoint + +Scenario: Successfully delete credential configuration + Given Issuer has "StudentProfile" credential configuration created from STUDENT_SCHEMA + When Issuer deletes "StudentProfile" credential configuration + Then Issuer cannot see the "StudentProfile" configuration on IssuerMetadata endpoint diff --git a/tests/integration-tests/src/test/resources/features/oid4vci/manage_issuer.feature b/tests/integration-tests/src/test/resources/features/oid4vci/manage_issuer.feature new file mode 100644 index 0000000000..d2b6bd4aa6 --- /dev/null +++ b/tests/integration-tests/src/test/resources/features/oid4vci/manage_issuer.feature @@ -0,0 +1,19 @@ +@oid4vci +Feature: Manage OID4VCI credential issuer + +Scenario: Successfully create credential issuer + When Issuer creates an oid4vci issuer + Then Issuer sees the oid4vci issuer exists on the agent + And Issuer sees the oid4vci issuer on IssuerMetadata endpoint + +Scenario: Successfully update credential issuer + Given Issuer has an existing oid4vci issuer + When Issuer updates the oid4vci issuer + Then Issuer sees the oid4vci issuer updated with new values + And Issuer sees the oid4vci IssuerMetadata endpoint updated with new values + +Scenario: Successfully delete credential issuer + Given Issuer has an existing oid4vci issuer + When Issuer deletes the oid4vci issuer + Then Issuer cannot see the oid4vci issuer on the agent + And Issuer cannot see the oid4vci IssuerMetadata endpoint