diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index 48424e4fff..813af6813e 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -28,6 +28,7 @@ jobs: runs-on: ubuntu-latest if: ${{ !contains(github.event.pull_request.title, '[skip ci]') }} env: + LOGS_DIR: "tests/integration-tests/target/logs" REPORTS_DIR: "tests/integration-tests/target/site/serenity" steps: - name: Checkout @@ -131,12 +132,21 @@ jobs: comment_title: "Integration Test Results" check_name: "Integration Test Results" - - name: Upload artifacts + - name: Upload serenity report if: github.ref_name == 'main' || failure() uses: actions/upload-artifact@v4 with: name: integration-tests-result path: ${{ env.REPORTS_DIR }} + compression-level: 9 + + - name: Upload logs + if: github.ref_name == 'main' || failure() + uses: actions/upload-artifact@v4 + with: + name: docker-logs + path: ${{ env.LOGS_DIR }} + compression-level: 9 - name: Slack Notification if: github.ref_name == 'main' && failure() diff --git a/tests/integration-tests/README.md b/tests/integration-tests/README.md index 91d175e678..2fef57bf71 100644 --- a/tests/integration-tests/README.md +++ b/tests/integration-tests/README.md @@ -30,8 +30,10 @@ The Screenplay pattern is used to write the tests. The pattern is described in d * The Screenplay Pattern promotes the use of high-level, business-oriented language in test scripts, making them more understandable to non-technical stakeholders.

-Screenplay pattern overview
-Pic. 1. Screenplay pattern overview + + Screenplay pattern overview +
+ Screenplay pattern overview

## Project structure @@ -67,8 +69,10 @@ The main idea of the framework is to test the ICA as a black box. The tests interact with the ICA through the API and webhook messages.

-Screenplay pattern overview
-Pic. 2. Overview of the system under test. Roles, Agents and Services communication. + + Screenplay pattern overview +
+ Overview of the system under test. Roles, Agents and Services communication.

### ICA Roles in Tests @@ -397,7 +401,7 @@ The following variables must be set before running the tests: * `AGENT_VERSION`: version of the ICA docker image to use. ```shell -TESTS_CONFIG=/configs/basic.conf PRISM_NODE_VERSION=2.3.0 AGENT_VERSION=1.30.1 ./gradlew test +TESTS_CONFIG=/configs/basic.conf PRISM_NODE_VERSION=2.3.0 AGENT_VERSION=1.36.1 ./gradlew test ``` > Please note: there is no need to pass environment variables if you're using already running agents. @@ -414,17 +418,37 @@ To simplify the execution, each configuration file creates a new `gradle` task. It's possible to execute the configuration file as ```shell -PRISM_NODE_VERSION=2.3.0 AGENT_VERSION=1.30.1 ./gradlew test_basic +PRISM_NODE_VERSION=2.3.0 AGENT_VERSION=1.36.1 ./gradlew test_basic ``` -Also, it's possible to execute the integration tests to all configurations files. The task is named `regression`, it should take a lot of time to execute. - -Note: report is not working due constrains in Serenity BDD reporting system. +Also, it's possible to execute the integration tests to all configurations files. The task is named `regression`, it should take a long time to execute. ```shell -PRISM_NODE_VERSION=2.3.0 AGENT_VERSION=1.30.1 ./gradlew regression +PRISM_NODE_VERSION=2.3.0 AGENT_VERSION=1.36.1 ./gradlew regression +``` + +#### Regression report + +Running the regression tasks implies running the same features multiple times. +To enable a full report of the regression execution, `context` variable was introduced to the report. + +To run all scenarios, even if there's a failure, it's required to add `--continue` to the execution + +Example +```bash +AGENT_VERSION=v1.36.1 PRISM_NODE_VERSION=v2.3.0 ./gradlew regression --continue ``` +Each `context` is based on the configuration used for the current execution and will be displayed in the +Serenity report: + +

+ + Serenity Regression report contexts +
+ Serenity Regression report with contexts +

+ ### Running scenarios in IntelliJ IDEA To run the scenarios in IntelliJ IDEA, you need to create a new run configuration. @@ -434,8 +458,10 @@ It is easy to do by executing `IntegrationTestsRunner` class and selecting the r The required configuration will be created, but you have to edit it to set the environment variables.

-Running tests in IntelliJ IDEA
-Pic. 3. Running tests through IntelliJ IDEA. + + Running tests in IntelliJ IDEA +
+ Running tests through IntelliJ IDEA.

You could edit `@CucumberOptions` annotation to specify the features to run, as well as specify tags to include or exclude: @@ -444,8 +470,9 @@ For example, here is how you can run only connection scenarios: ```kotlin @CucumberOptions( features = ["src/test/resources/features/connection"], - ... + // ... ) +class IntegrationTestsRunner ``` If you would like to run only particular scenarios from the feature or combine multiple scenarios from different feature file, @@ -454,8 +481,9 @@ you could use tags: @CucumberOptions( features = ["src/test/resources/features"], tags = ["@connection and @credentials"], - ... + // ... ) +class IntegrationTestsRunner ``` > Please note: if you use custom tags, you need to specify them in the feature files as well. @@ -489,20 +517,26 @@ You could start by opening `index.html` file in your browser. On the main report page you could see the summary of the test run as well as the functional coverage table:

-Report summary
-Pic. 4. HTML-report summary example. + + Report summary +
+ HTML-report summary example.

-Functional coverage
-Pic. 5. Functional coverage example. + + Functional coverage +
+ Functional coverage example.

Then, you can go deeper to each scenario and open each step to see the details of the test execution:

-REST requests analysis
-Pic. 6. REST requests analysis example. + + REST requests analysis +
+ REST requests analysis example.

### Summary reports @@ -530,3 +564,24 @@ And summary reports themselves will be available in `./target/site/serenity` fol JUnit XML reports are also generated under `./target/site/serenity` folder with names `SERENITY-JUNIT-*.xml`. > For more information about the reports, please refer to [Serenity BDD reports documentation](https://serenity-bdd.github.io/docs/reporting/the_serenity_reports). + +### Docker logs + +Docker logs are now redirected to `target/logs` folder. + +If you're running the test using the custom config goals it will have the context added +to the path as `target/logs/basic`. + +Example +```bash +AGENT_VERSION=v1.36.1 PRISM_NODE_VERSION=v2.3.0 ./gradlew test_basic +``` + +Will have the logs output as such: + +

+ + Docker logs directory +
+ Docker logs directory +

diff --git a/tests/integration-tests/build.gradle.kts b/tests/integration-tests/build.gradle.kts index 24d137624b..177a614b9c 100644 --- a/tests/integration-tests/build.gradle.kts +++ b/tests/integration-tests/build.gradle.kts @@ -11,8 +11,8 @@ version = "1.0-SNAPSHOT" buildscript { dependencies { - classpath("net.serenity-bdd:serenity-single-page-report:4.1.4") - classpath("net.serenity-bdd:serenity-json-summary-report:4.1.4") + classpath("net.serenity-bdd:serenity-single-page-report:4.0.46") + classpath("net.serenity-bdd:serenity-json-summary-report:4.0.46") } } @@ -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.33.1") + testImplementation("org.hyperledger.identus:cloud-agent-client-kotlin:1.36.1") // Test helpers library testImplementation("io.iohk.atala:atala-automation:0.4.0") // Hoplite for configuration @@ -41,6 +41,10 @@ dependencies { testImplementation("com.sksamuel.hoplite:hoplite-hocon:2.7.5") // Kotlin compose testImplementation("org.testcontainers:testcontainers:1.19.1") + // Crypto + testImplementation("com.nimbusds:nimbus-jose-jwt:9.40") + testImplementation("org.bouncycastle:bcprov-jdk18on:1.78.1") + testImplementation("com.google.crypto.tink:tink:1.13.0") } serenity { @@ -54,7 +58,7 @@ tasks.register("cleanTarget") { tasks.test { dependsOn("cleanTarget") - finalizedBy("reports") + finalizedBy("aggregate", "reports") testLogging.showStandardStreams = true systemProperty("cucumber.filter.tags", System.getProperty("cucumber.filter.tags")) } @@ -82,11 +86,13 @@ afterEvaluate { tasks.register("test_$fileName") { group = "verification" testLogging.showStandardStreams = true + systemProperty("context", fileName) systemProperty("TESTS_CONFIG", "/configs/$fileName.conf") systemProperty("PRISM_NODE_VERSION", System.getenv("PRISM_NODE_VERSION") ?: "") systemProperty("AGENT_VERSION", System.getenv("AGENT_VERSION") ?: "") systemProperty("cucumber.filter.tags", System.getProperty("cucumber.filter.tags")) finalizedBy("aggregate", "reports") + outputs.upToDateWhen { false } } } @@ -94,7 +100,7 @@ afterEvaluate { * Runs the integration suite for each config file present * Restrictions: aggregation of all executions doesn't work because of serenity configuration */ - tasks.register("regression") { + tasks.register("regression") { dependsOn("cleanTarget") group = "verification" configFiles.forEach { diff --git a/tests/integration-tests/docs/static/logs.png b/tests/integration-tests/docs/static/logs.png new file mode 100644 index 0000000000..776e0643d2 Binary files /dev/null and b/tests/integration-tests/docs/static/logs.png differ diff --git a/tests/integration-tests/docs/static/serenity_context.png b/tests/integration-tests/docs/static/serenity_context.png new file mode 100644 index 0000000000..1470f2e2af Binary files /dev/null and b/tests/integration-tests/docs/static/serenity_context.png differ diff --git a/tests/integration-tests/serenity.properties b/tests/integration-tests/serenity.properties index c8682e00d5..7cde73aebd 100644 --- a/tests/integration-tests/serenity.properties +++ b/tests/integration-tests/serenity.properties @@ -1,8 +1,9 @@ -serenity.project.name=Open Enterprise Agent Integration tests +serenity.project.name=Identus Integration tests jira.url=https://input-output.atlassian.net jira.project=ATL serenity.reports.show.step.details=true serenity.report.show.manual.tests=false +serenity.verbose.steps=true serenity.simplified.stack.traces=false serenity.report.accessibility=true json.pretty.printing=true diff --git a/tests/integration-tests/src/test/kotlin/common/CredentialSchema.kt b/tests/integration-tests/src/test/kotlin/common/CredentialSchema.kt index 972f806069..05a6bbf37c 100644 --- a/tests/integration-tests/src/test/kotlin/common/CredentialSchema.kt +++ b/tests/integration-tests/src/test/kotlin/common/CredentialSchema.kt @@ -30,10 +30,15 @@ enum class CredentialSchema { tags = listOf("school", "students"), version = "1.0.0", ) + override val claims: Map = linkedMapOf( + "name" to "Name", + "age" to 18, + ) }, ; abstract val credentialSchema: CredentialSchemaInput abstract val schema: JsonSchema abstract val credentialSchemaType: String abstract val schemaType: String + abstract val claims: Map } diff --git a/tests/integration-tests/src/test/kotlin/common/DidPurpose.kt b/tests/integration-tests/src/test/kotlin/common/DidPurpose.kt new file mode 100644 index 0000000000..dee1243f98 --- /dev/null +++ b/tests/integration-tests/src/test/kotlin/common/DidPurpose.kt @@ -0,0 +1,25 @@ +package common + +import org.hyperledger.identus.client.models.* + +enum class DidPurpose { + EMPTY { + override val publicKeys = emptyList() + override val services = emptyList() + }, + JWT { + override val publicKeys = listOf( + ManagedDIDKeyTemplate("auth-1", Purpose.AUTHENTICATION, Curve.SECP256K1), + ManagedDIDKeyTemplate("auth-2", Purpose.AUTHENTICATION, Curve.ED25519), + ManagedDIDKeyTemplate("assertion-1", Purpose.ASSERTION_METHOD, Curve.SECP256K1), + ) + override val services = emptyList() + }, + ANONCRED { + override val publicKeys = emptyList() + override val services = emptyList() + }, ; + + abstract val publicKeys: List + abstract val services: List +} diff --git a/tests/integration-tests/src/test/kotlin/common/JwtCredentialProblem.kt b/tests/integration-tests/src/test/kotlin/common/JwtCredentialProblem.kt new file mode 100644 index 0000000000..eb9435d881 --- /dev/null +++ b/tests/integration-tests/src/test/kotlin/common/JwtCredentialProblem.kt @@ -0,0 +1,120 @@ +package common + +import com.nimbusds.jose.JWSAlgorithm +import com.nimbusds.jose.jwk.Curve +import models.JwtCredential +import org.hyperledger.identus.client.models.VcVerification +import java.time.OffsetDateTime + +enum class JwtCredentialProblem { + ALGORITHM_VERIFICATION { + override fun jwt(): String { + val jwt = VerifiableJwt.jwtVCv1() + return jwt.sign(JWSAlgorithm.HS256, null) + } + override val verification = VcVerification.ALGORITHM_VERIFICATION + }, + AUDIENCE_CHECK { + override fun jwt(): String { + val jwt = VerifiableJwt.jwtVCv1() + jwt.audience("did:wrong") + return jwt.sign(DEFAULT_ALGORITHM, DEFAULT_CURVE) + } + override val verification = VcVerification.AUDIENCE_CHECK + }, + COMPLIANCE_WITH_STANDARDS { + override fun jwt(): String { + TODO("Not supported yet") + } + + override val verification = VcVerification.COMPLIANCE_WITH_STANDARDS + }, + EXPIRATION_CHECK { + override fun jwt(): String { + val jwt = VerifiableJwt.jwtVCv1() + jwt.expirationTime(OffsetDateTime.now().plusYears(10)) + return jwt.sign(DEFAULT_ALGORITHM, DEFAULT_CURVE) + } + + override val verification = VcVerification.EXPIRATION_CHECK + }, + INTEGRITY_OF_CLAIMS { + override fun jwt(): String { + TODO("Not supported yet") + } + override val verification = VcVerification.INTEGRITY_OF_CLAIMS + }, + ISSUER_IDENTIFICATION { + override fun jwt(): String { + val jwt = VerifiableJwt.jwtVCv1() + jwt.issuer("did:wrong") + return jwt.sign(DEFAULT_ALGORITHM, DEFAULT_CURVE) + } + override val verification = VcVerification.ISSUER_IDENTIFICATION + }, + NOT_BEFORE_CHECK { + override fun jwt(): String { + val jwt = VerifiableJwt.jwtVCv1() + jwt.notBefore(OffsetDateTime.now().minusYears(10)) + return jwt.sign(DEFAULT_ALGORITHM, DEFAULT_CURVE) + } + override val verification = VcVerification.NOT_BEFORE_CHECK + }, + REVOCATION_CHECK { + override fun jwt(): String { + TODO("Not supported yet") + } + override val verification = VcVerification.REVOCATION_CHECK + }, + SCHEMA_CHECK { + override fun jwt(): String { + TODO("Not supported yet") + } + override val verification = VcVerification.SCHEMA_CHECK + }, + SEMANTIC_CHECK_OF_CLAIMS { + override fun jwt(): String { + val jwt = VerifiableJwt.jwtVCv1() + val jwtCredential = JwtCredential() + val claims = mutableMapOf() + claims.putAll(jwt.claimSetBuilder.claims) + claims.remove("iss") + jwtCredential.claims(claims) + return jwt.sign(DEFAULT_ALGORITHM, DEFAULT_CURVE) + } + override val verification = VcVerification.SEMANTIC_CHECK_OF_CLAIMS + }, + SIGNATURE_VERIFICATION { + override fun jwt(): String { + val jwt = VerifiableJwt.jwtVCv1() + return jwt.sign(DEFAULT_ALGORITHM, DEFAULT_CURVE) + } + override val verification = VcVerification.SIGNATURE_VERIFICATION + }, + SUBJECT_VERIFICATION { + override fun jwt(): String { + TODO("Not yet implemented") + } + override val verification = VcVerification.SUBJECT_VERIFICATION + }, ; + + companion object { + init { + // forcefully check if JwtCredentialProblems has all VcVerification + // cases since it's not possible to inherit final class + VcVerification.entries.forEach { + try { + JwtCredentialProblem.valueOf(it.name) + } catch (e: IllegalArgumentException) { + throw IllegalArgumentException("JwtCredentialProblem does not contain the new ${it.name} VcVerification case") + } + } + } + } + + protected val DEFAULT_ALGORITHM = JWSAlgorithm.ES256K + protected val DEFAULT_CURVE = Curve.SECP256K1 + + abstract fun jwt(): String + abstract val verification: VcVerification +} diff --git a/tests/integration-tests/src/test/kotlin/common/SchemaErrorTemplate.kt b/tests/integration-tests/src/test/kotlin/common/SchemaErrorTemplate.kt new file mode 100644 index 0000000000..c76c614113 --- /dev/null +++ b/tests/integration-tests/src/test/kotlin/common/SchemaErrorTemplate.kt @@ -0,0 +1,78 @@ +package common + +import com.google.gson.Gson +import com.google.gson.JsonObject +import common.CredentialSchema.STUDENT_SCHEMA +import net.serenitybdd.screenplay.Actor + +enum class SchemaErrorTemplate { + TYPE_AND_PROPERTIES_WITHOUT_SCHEMA_TYPE { + override fun inner_schema(): String { + return """ + { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "age": { + "type": "integer" + } + }, + "required": ["name"] + } + """.trimIndent() + } + }, + CUSTOM_WORDS_NOT_DEFINED { + override fun inner_schema(): String { + return """ + { + "${"$"}schema": "http://json-schema.org/draft-2020-12/schema#", + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "age": { + "type": "integer" + } + }, + "customKeyword": "value" + } + """.trimIndent() + } + }, + MISSING_REQUIRED_FOR_MANDATORY_PROPERTY { + override fun inner_schema(): String { + return """ + { + "${"$"}schema": "http://json-schema.org/draft-2020-12/schema#", + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "age": { + "type": "integer" + } + } + } + """ + } + }, ; + + abstract fun inner_schema(): String + + fun schema(actor: Actor): String { + val innerSchema = Gson().fromJson(inner_schema(), JsonObject::class.java) + val json = getJson(actor) + json.add("schema", innerSchema) + return json.toString() + } + + private fun getJson(actor: Actor): JsonObject { + val jsonString = Gson().toJson(STUDENT_SCHEMA.credentialSchema.copy(author = actor.recall("shortFormDid"))) + return Gson().fromJson(jsonString, JsonObject::class.java) + } +} diff --git a/tests/integration-tests/src/test/kotlin/common/TestConstants.kt b/tests/integration-tests/src/test/kotlin/common/TestConstants.kt index 56a81a0a73..15fed21943 100644 --- a/tests/integration-tests/src/test/kotlin/common/TestConstants.kt +++ b/tests/integration-tests/src/test/kotlin/common/TestConstants.kt @@ -18,18 +18,6 @@ object TestConstants { ), ) - val PRISM_DID_AUTH_KEY = ManagedDIDKeyTemplate("auth-1", Purpose.AUTHENTICATION) - val PRISM_DID_SERVICE_FOR_UPDATE = Service( - "https://update.com", - listOf("LinkedDomains"), - Json("https://update.com/"), - ) - val PRISM_DID_UPDATE_NEW_SERVICE_URL = "https://bar.foo.com/" - val PRISM_DID_UPDATE_NEW_SERVICE = Service( - "https://new.service.com", - listOf("LinkedDomains"), - Json("https://new.service.com/"), - ) val EVENT_TYPE_CONNECTION_UPDATED = "ConnectionUpdated" val EVENT_TYPE_ISSUE_CREDENTIAL_RECORD_UPDATED = "IssueCredentialRecordUpdated" val EVENT_TYPE_PRESENTATION_UPDATED = "PresentationUpdated" diff --git a/tests/integration-tests/src/test/kotlin/common/VerifiableJwt.kt b/tests/integration-tests/src/test/kotlin/common/VerifiableJwt.kt new file mode 100644 index 0000000000..4aa5d2483e --- /dev/null +++ b/tests/integration-tests/src/test/kotlin/common/VerifiableJwt.kt @@ -0,0 +1,132 @@ +package common + +import com.google.gson.JsonArray +import com.google.gson.JsonObject +import com.google.gson.annotations.SerializedName +import com.google.gson.reflect.TypeToken +import io.iohk.atala.automation.restassured.CustomGsonObjectMapperFactory +import models.JwtCredential +import java.time.OffsetDateTime + +object VerifiableJwt { + fun schemaVCv1(): JwtCredential { + val favoriteColorEnum = JsonArray() + listOf("red", "orange", "green", "blue", "yellow", "purple").forEach { + favoriteColorEnum.add(it) + } + + val favoriteColor = JsonObject() + favoriteColor.addProperty("type", "string") + favoriteColor.add("enum", favoriteColorEnum) + + val jsonSchemaPropertiesCredentialSubjectProperties = JsonObject() + jsonSchemaPropertiesCredentialSubjectProperties.add("favoriteColor", favoriteColor) + + val required = JsonArray() + required.add("favoriteColor") + + val jsonSchemaPropertiesCredentialSubject = JsonObject() + jsonSchemaPropertiesCredentialSubject.addProperty("type", "object") + jsonSchemaPropertiesCredentialSubject.add("properties", jsonSchemaPropertiesCredentialSubjectProperties) + jsonSchemaPropertiesCredentialSubject.add("required", required) + + val jsonSchemaProperties = JsonObject() + jsonSchemaProperties.add("credentialSubject", jsonSchemaPropertiesCredentialSubject) + + val jsonSchema = JsonObject() + jsonSchema.addProperty("${"$"}id", "https://example.com/schemas/favorite-color-schema.json") + jsonSchema.addProperty("${"$"}schema", "https://json-schema.org/draft/2020-12/schema") + jsonSchema.addProperty("title", "Favorite Color Schema") + jsonSchema.addProperty("description", "Favorite Color using JsonSchemaCredential") + jsonSchema.addProperty("type", "object") + jsonSchema.add("properties", jsonSchemaProperties) + + val credentialSubject = JsonObject() + credentialSubject.addProperty("id", "https://example.com/schemas/favorite-color-schema.json") + credentialSubject.addProperty("type", "JsonSchema") + credentialSubject.add("jsonSchema", jsonSchema) + + val credentialSchema = JsonObject() + credentialSchema.addProperty( + "id", + "https://www.w3.org/2022/credentials/v2/json-schema-credential-schema.json", + ) + credentialSchema.addProperty("type", "JsonSchema") + credentialSchema.addProperty( + "digestSRI", + "sha384-S57yQDg1MTzF56Oi9DbSQ14u7jBy0RDdx0YbeV7shwhCS88G8SCXeFq82PafhCrW", + ) + + val verifiableSchema = VerifiableSchemaV1( + credentialSubject = credentialSubject, + credentialSchema = credentialSchema, + type = listOf("VerifiableCredential", "JsonSchemaCredential"), + context = listOf("https://www.w3.org/ns/credentials/v2", "https://www.w3.org/ns/credentials/examples/v2"), + id = "https://example.com/credentials/3734", + issuer = "https://example.com/issuers/14", + issuanceDate = OffsetDateTime.now(), + ) + + val typeToken = object : TypeToken>() {}.type + val gson = CustomGsonObjectMapperFactory().create(null, null) + val json = gson.toJsonTree(verifiableSchema) + val claims = gson.fromJson>(json, typeToken) + val jwt = JwtCredential().claims(claims) + return jwt + } + + fun jwtVCv1(): JwtCredential { + val credentialSubject = JsonObject() + credentialSubject.addProperty("id", "did:subject") + credentialSubject.addProperty("firstName", "John") + credentialSubject.addProperty("lastName", "Doe") + + val vc = VerifiableCredentialV1( + credentialSubject = credentialSubject, + type = listOf("VerifiableCredential", "VerifiablePresentation"), + context = listOf("https://www.w3.org/2018/credentials/v1"), + credentialStatus = CredentialStatus( + statusPurpose = "Revocation", + statusListIndex = 1, + id = "https://example.com/credential-status/4a6ad192-14b5-4804-8c78-8873c82d2250#1", + type = "StatusList2021Entry", + statusListCredential = "https://example.com/credential-status/4a6ad192-14b5-4804-8c78-8873c82d2250", + ), + ) + + val jwt = JwtCredential().issuer("did:prism:issuer").jwtID("jti").subject("did:subject") + .audience("did:prism:verifier").issueTime(OffsetDateTime.now()).expirationTime(OffsetDateTime.now()) + .notBefore(OffsetDateTime.now()).claim("vc", vc) + + return jwt + } + + // --- Types to mimic JWT-VC + + // https://www.w3.org/2018/credentials/v1 + // https://www.w3.org/TR/2023/WD-vc-jwt-20230501/ + data class VerifiableCredentialV1( + val credentialSubject: Any, + val type: Collection, + @SerializedName("@context") val context: Collection, + val credentialStatus: CredentialStatus, + ) + + data class CredentialStatus( + val statusPurpose: String, + val statusListIndex: Int, + val id: String, + val type: String, + val statusListCredential: String, + ) + + data class VerifiableSchemaV1( + @SerializedName("@context") val context: Collection, + val id: String, + val type: Collection, + val issuer: String, + val issuanceDate: OffsetDateTime, + val credentialSchema: Any, + val credentialSubject: Any, + ) +} diff --git a/tests/integration-tests/src/test/kotlin/config/services/Agent.kt b/tests/integration-tests/src/test/kotlin/config/services/Agent.kt index 52e3072e44..f4d7cc0962 100644 --- a/tests/integration-tests/src/test/kotlin/config/services/Agent.kt +++ b/tests/integration-tests/src/test/kotlin/config/services/Agent.kt @@ -4,7 +4,7 @@ import com.sksamuel.hoplite.ConfigAlias import config.VaultAuthType import org.testcontainers.containers.ComposeContainer import org.testcontainers.containers.wait.strategy.Wait -import java.io.File +import java.io.* data class Agent( val version: String, @@ -13,12 +13,13 @@ data class Agent( @ConfigAlias("didcomm_service_url") val didcommServiceUrl: String?, @ConfigAlias("rest_service_url") val restServiceUrl: String?, @ConfigAlias("auth_enabled") val authEnabled: Boolean, - @ConfigAlias("prism_node") val prismNode: PrismNode?, + @ConfigAlias("prism_node") val prismNode: VerifiableDataRegistry?, val keycloak: Keycloak?, val vault: Vault?, @ConfigAlias("keep_running") override val keepRunning: Boolean = false, -) : ServiceBase { +) : ServiceBase() { + override val logServices = listOf("identus-cloud-agent") override val container: ComposeContainer init { @@ -50,9 +51,7 @@ data class Agent( env["VAULT_APPROLE_SECRET_ID"] = "agent-secret" } - container = ComposeContainer( - File("src/test/resources/containers/agent.yml"), - ) + container = ComposeContainer(File("src/test/resources/containers/agent.yml")) .withEnv(env) .waitingFor("identus-cloud-agent", Wait.forHealthcheck()) } 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 289cb1cc68..e3a2d8475d 100644 --- a/tests/integration-tests/src/test/kotlin/config/services/Keycloak.kt +++ b/tests/integration-tests/src/test/kotlin/config/services/Keycloak.kt @@ -18,13 +18,14 @@ 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, -) : ServiceBase { +) : ServiceBase() { private val logger = Logger.get() private val keycloakComposeFile = "src/test/resources/containers/keycloak.yml" private val keycloakEnvConfig: Map = mapOf( "KEYCLOAK_HTTP_PORT" to httpPort.toString(), ) - private val keycloakClientRoles: List = AgentRole.values().map { it.roleName } + override val logServices: List = listOf("keycloak") + private val keycloakClientRoles: List = AgentRole.entries.map { it.roleName } override val container: ComposeContainer = ComposeContainer(File(keycloakComposeFile)).withEnv(keycloakEnvConfig) .waitingFor("keycloak", Wait.forLogMessage(".*Running the server.*", 1)) 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 cef1310379..ee897f6e91 100644 --- a/tests/integration-tests/src/test/kotlin/config/services/Service.kt +++ b/tests/integration-tests/src/test/kotlin/config/services/Service.kt @@ -3,7 +3,7 @@ package config.services import com.sksamuel.hoplite.ConfigAlias data class Service( - @ConfigAlias("prism_node") val prismNode: PrismNode?, + @ConfigAlias("prism_node") val prismNode: VerifiableDataRegistry?, val keycloak: Keycloak?, val vault: Vault?, ) diff --git a/tests/integration-tests/src/test/kotlin/config/services/ServiceBase.kt b/tests/integration-tests/src/test/kotlin/config/services/ServiceBase.kt index af1b7b0b0d..8c598b61d6 100644 --- a/tests/integration-tests/src/test/kotlin/config/services/ServiceBase.kt +++ b/tests/integration-tests/src/test/kotlin/config/services/ServiceBase.kt @@ -1,25 +1,46 @@ package config.services -import com.sksamuel.hoplite.ConfigAlias import org.testcontainers.containers.ComposeContainer import org.testcontainers.lifecycle.Startable +import java.io.* -interface ServiceBase : Startable { +abstract class ServiceBase : Startable { + companion object { + private val context = System.getProperties().getOrDefault("context", "") + private val logDir = File("target/logs/$context") + init { + logDir.deleteRecursively() + logDir.mkdirs() + } + } - val container: ComposeContainer + abstract val container: ComposeContainer + abstract val keepRunning: Boolean - @ConfigAlias("keep_running") - val keepRunning: Boolean + open val logServices: List = emptyList() + private val logWriters: MutableList = mutableListOf() override fun start() { + logServices.forEach { + val output = File(logDir, "$it.log") + output.createNewFile() + val writer = FileOutputStream(output, true).bufferedWriter() + logWriters.add(writer) + container.withLogConsumer(it) { logLine -> + writer.append(logLine.utf8String) + } + } container.start() postStart() } - fun postStart() { + open fun postStart() { } override fun stop() { + logWriters.forEach { + it.close() + } if (!keepRunning) { container.stop() } diff --git a/tests/integration-tests/src/test/kotlin/config/services/Vault.kt b/tests/integration-tests/src/test/kotlin/config/services/Vault.kt index ebd7234de6..85f1a02b27 100644 --- a/tests/integration-tests/src/test/kotlin/config/services/Vault.kt +++ b/tests/integration-tests/src/test/kotlin/config/services/Vault.kt @@ -13,10 +13,11 @@ import java.io.File data class Vault( @ConfigAlias("http_port") val httpPort: Int, - @ConfigAlias("keep_running") override val keepRunning: Boolean = false, @ConfigAlias("vault_auth_type") val authType: VaultAuthType = VaultAuthType.APP_ROLE, -) : ServiceBase { + @ConfigAlias("keep_running") override val keepRunning: Boolean = false, +) : ServiceBase() { private val logger = Logger.get() + override val logServices: List = listOf("vault") private val vaultComposeFile: String = "src/test/resources/containers/vault.yml" override val container: ComposeContainer = ComposeContainer(File(vaultComposeFile)).withEnv( mapOf( diff --git a/tests/integration-tests/src/test/kotlin/config/services/PrismNode.kt b/tests/integration-tests/src/test/kotlin/config/services/VerifiableDataRegistry.kt similarity index 86% rename from tests/integration-tests/src/test/kotlin/config/services/PrismNode.kt rename to tests/integration-tests/src/test/kotlin/config/services/VerifiableDataRegistry.kt index b9e712bea7..2997f567cc 100644 --- a/tests/integration-tests/src/test/kotlin/config/services/PrismNode.kt +++ b/tests/integration-tests/src/test/kotlin/config/services/VerifiableDataRegistry.kt @@ -5,11 +5,12 @@ import org.testcontainers.containers.ComposeContainer import org.testcontainers.containers.wait.strategy.Wait import java.io.File -data class PrismNode( +data class VerifiableDataRegistry( @ConfigAlias("http_port") val httpPort: Int, val version: String, @ConfigAlias("keep_running") override val keepRunning: Boolean = false, -) : ServiceBase { +) : ServiceBase() { + override val logServices: List = listOf("prism-node") private val vdrComposeFile = "src/test/resources/containers/vdr.yml" override val container: ComposeContainer = ComposeContainer(File(vdrComposeFile)).withEnv( mapOf( diff --git a/tests/integration-tests/src/test/kotlin/interactions/AuthRestExtensions.kt b/tests/integration-tests/src/test/kotlin/interactions/AuthRestExtensions.kt new file mode 100644 index 0000000000..19318c0c5a --- /dev/null +++ b/tests/integration-tests/src/test/kotlin/interactions/AuthRestExtensions.kt @@ -0,0 +1,15 @@ +package interactions + +import net.serenitybdd.screenplay.rest.interactions.RestInteraction + +fun Post.body(obj: Any): RestInteraction { + return this.with { + it.header("Content-Type", "application/json").body(obj) + } +} + +fun Patch.body(obj: Any): RestInteraction { + return this.with { + it.header("Content-Type", "application/json").body(obj) + } +} diff --git a/tests/integration-tests/src/test/kotlin/models/JwtCredential.kt b/tests/integration-tests/src/test/kotlin/models/JwtCredential.kt index 741f3f4c5f..e4395e27e0 100644 --- a/tests/integration-tests/src/test/kotlin/models/JwtCredential.kt +++ b/tests/integration-tests/src/test/kotlin/models/JwtCredential.kt @@ -1,20 +1,210 @@ package models -import com.jayway.jsonpath.DocumentContext -import com.jayway.jsonpath.JsonPath +import com.google.gson.Gson +import com.nimbusds.jose.* +import com.nimbusds.jose.crypto.* +import com.nimbusds.jose.crypto.impl.* +import com.nimbusds.jose.jwk.* +import com.nimbusds.jose.jwk.gen.ECKeyGenerator +import com.nimbusds.jose.jwk.gen.OctetKeyPairGenerator +import com.nimbusds.jose.util.Base64URL +import com.nimbusds.jwt.JWTClaimsSet +import com.nimbusds.jwt.SignedJWT +import org.bouncycastle.jce.provider.BouncyCastleProvider +import org.hyperledger.identus.client.models.VerificationMethod +import java.io.Serializable +import java.security.Provider +import java.security.SecureRandom +import java.time.OffsetDateTime import java.util.Base64 +import java.util.Date +import kotlin.reflect.KClass -class JwtCredential(base64: String) { - private val payload: DocumentContext +class JwtCredential { + // helper classes + interface Key { + val value: T + } + + class EC(override val value: ECKey) : Key + class OKP(override val value: OctetKeyPair) : Key + class Secret(override val value: ByteArray) : Key + + // properties + var header: JWSHeader? = null + var payload: Payload? = null + var signature: Base64URL? = null + var claimSetBuilder = JWTClaimsSet.Builder() + + companion object { + val provider: Provider = BouncyCastleProvider() + val keys: MutableMap> = mutableMapOf() + + fun parseBase64(base64: String): JwtCredential { + return JwtCredential().parseBase64(base64) + } + + fun parseJwt(jwt: String): JwtCredential { + return JwtCredential().parseJwt(jwt) + } + + fun verify(jwt: String, verification: List): Boolean { + val signedJWT = SignedJWT.parse(jwt) + verification + .map { Gson().toJson(it.publicKeyJwk) } + .forEach { + val result = signedJWT.verify(verifier(it)) + if (result) return true + } + return false + } + + fun verify(jwt: String, verifier: JWSVerifier): Boolean { + verifier.jcaContext.provider = provider + val signedJWT = SignedJWT.parse(jwt) + val result = signedJWT.verify(verifier) + return result + } + + private fun type(algorithm: JWSAlgorithm): KClass { + if (MACProvider.SUPPORTED_ALGORITHMS.contains(algorithm)) { + return MACProvider::class + } + if (ECDSAProvider.SUPPORTED_ALGORITHMS.contains(algorithm)) { + return ECDSAProvider::class + } + + if (EdDSAProvider.SUPPORTED_ALGORITHMS.contains(algorithm)) { + return EdDSAProvider::class + } + throw RuntimeException("Requested [$algorithm] not supported.") + } + + private fun generateBytes(bytes: Int): ByteArray { + val randomBytes = ByteArray(bytes) + SecureRandom().nextBytes(randomBytes) + return randomBytes + } + + private fun key(algorithm: JWSAlgorithm, curve: Curve?): Key { + val key = keys + .getOrPut("${algorithm.name}-${curve?.name}") { + when (type(algorithm)) { + MACProvider::class -> Secret( + generateBytes(128), + ) + + ECDSAProvider::class -> EC( + ECKeyGenerator(curve).provider(provider).keyUse(KeyUse.SIGNATURE).generate(), + ) + + EdDSAProvider::class -> OKP( + OctetKeyPairGenerator(curve).provider(provider).keyUse(KeyUse.SIGNATURE).generate(), + ) + + else -> throw RuntimeException("Requested [$algorithm] not supported.") + } + } + return key + } + + fun signer(algorithm: JWSAlgorithm, curve: Curve?): JWSSigner { + val signer: JWSSigner = when (val key = key(algorithm, curve)) { + is Secret -> MACSigner(key.value) + is EC -> ECDSASigner(key.value) + is OKP -> Ed25519Signer(key.value) + else -> throw RuntimeException("Unsupported key algorithm: $algorithm and curve: $curve") + } + signer.jcaContext.provider = provider + return signer + } + + private fun parseKey(key: String): JWK { + try { return ECKey.parse(key) } catch (_: Error) { } + try { return OctetKeyPair.parse(key) } catch (_: Error) { } + throw RuntimeException("Invalid key [$key]") + } + + private fun verifier(key: String): JWSVerifier { + val verifier: JWSVerifier = when (val jwk = parseKey(key)) { + is ECKey -> ECDSAVerifier(jwk) + is OctetKeyPair -> Ed25519Verifier(jwk) + else -> throw RuntimeException("Invalid key [$key]") + } + verifier.jcaContext.provider = provider + return verifier + } + } + + fun sign(algorithm: JWSAlgorithm, curve: Curve?): String { + val jwt = SignedJWT( + JWSHeader.Builder(algorithm).build(), + claimSetBuilder.build(), + ) + jwt.sign(signer(algorithm, curve)) + return jwt.serialize() + } - init { + fun parseBase64(base64: String): JwtCredential { val jwt = String(Base64.getDecoder().decode(base64)) - val parts = jwt.split(".") - payload = JsonPath.parse(String(Base64.getUrlDecoder().decode(parts[1]))) + return parseJwt(jwt) + } + + fun parseJwt(jwt: String): JwtCredential { + val signedJWT = SignedJWT.parse(jwt) + claimSetBuilder = JWTClaimsSet.Builder(signedJWT.jwtClaimsSet) + header = signedJWT.header + payload = signedJWT.payload + signature = signedJWT.signature + return this + } + + fun issuer(iss: String): JwtCredential { + claimSetBuilder.issuer(iss) + return this + } + + fun jwtID(jti: String): JwtCredential { + claimSetBuilder.jwtID(jti) + return this + } + + fun audience(aud: String): JwtCredential { + claimSetBuilder.audience(aud) + return this + } + + fun issueTime(iat: OffsetDateTime): JwtCredential { + claimSetBuilder.issueTime(Date.from(iat.toInstant())) + return this + } + + fun subject(sub: String): JwtCredential { + claimSetBuilder.subject(sub) + return this + } + + fun expirationTime(exp: OffsetDateTime): JwtCredential { + claimSetBuilder.expirationTime(Date.from(exp.toInstant())) + return this + } + + fun notBefore(nbf: OffsetDateTime): JwtCredential { + claimSetBuilder.notBeforeTime(Date.from(nbf.toInstant())) + return this + } + + fun claim(key: String, data: Any): JwtCredential { + claimSetBuilder.claim(key, Gson().fromJson(Gson().toJson(data), Object::class.java)) + return this + } + + fun claims(claims: Map): JwtCredential { + claims.forEach { (key, value) -> claimSetBuilder.claim(key, value) } + return this } - fun statusListId(): String { - val listUrl = payload.read("$.vc.credentialStatus.statusListCredential") - return listUrl.split("/credential-status/")[1] + fun serialize(): String { + return SignedJWT(header!!.toBase64URL(), payload!!.toBase64URL(), signature!!).serialize() } } diff --git a/tests/integration-tests/src/test/kotlin/steps/Setup.kt b/tests/integration-tests/src/test/kotlin/steps/Setup.kt index ce1034a63e..57b035ab9d 100644 --- a/tests/integration-tests/src/test/kotlin/steps/Setup.kt +++ b/tests/integration-tests/src/test/kotlin/steps/Setup.kt @@ -20,7 +20,15 @@ import org.hyperledger.identus.client.models.CreateWebhookNotification import java.util.UUID object Setup { - val config = ConfigLoader().loadConfigOrThrow(TestConstants.TESTS_CONFIG) + private val config: Config + + init { + try { + config = ConfigLoader().loadConfigOrThrow(TestConstants.TESTS_CONFIG) + } catch (e: Exception) { + throw RuntimeException(e) + } + } /** * This function starts all services and actors before all tests. diff --git a/tests/integration-tests/src/test/kotlin/steps/common/CommonSteps.kt b/tests/integration-tests/src/test/kotlin/steps/common/CommonSteps.kt index e9883293c9..bc885b15ee 100644 --- a/tests/integration-tests/src/test/kotlin/steps/common/CommonSteps.kt +++ b/tests/integration-tests/src/test/kotlin/steps/common/CommonSteps.kt @@ -1,46 +1,55 @@ package steps.common +import common.CredentialSchema +import common.DidPurpose import interactions.Get -import io.cucumber.java.ParameterType import io.cucumber.java.en.Given 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 net.serenitybdd.screenplay.actors.OnStage import org.apache.http.HttpStatus -import org.hyperledger.identus.client.models.* +import org.hyperledger.identus.client.models.Connection +import org.hyperledger.identus.client.models.ConnectionsPage import steps.connection.ConnectionSteps import steps.credentials.IssueCredentialsSteps import steps.did.PublishDidSteps -import java.lang.IllegalArgumentException +import steps.schemas.CredentialSchemasSteps class CommonSteps { - @ParameterType(".*") - fun actor(actorName: String): Actor { - return OnStage.theActorCalled(actorName) - } + @Given("{actor} has a jwt issued credential from {actor}") + fun holderHasIssuedCredentialFromIssuer(holder: Actor, issuer: Actor) { + actorsHaveExistingConnection(issuer, holder) - @ParameterType(".*") - fun curve(value: String): Curve { - return Curve.decode(value) ?: throw IllegalArgumentException("$value is not a valid Curve value") - } + val publishDidSteps = PublishDidSteps() + publishDidSteps.agentHasAnUnpublishedDID(holder, DidPurpose.JWT) + publishDidSteps.agentHasAPublishedDID(issuer, DidPurpose.JWT) - @ParameterType(".*") - fun purpose(value: String): Purpose { - return Purpose.decode(value) ?: throw IllegalArgumentException("$value is not a valid Purpose value") + val issueSteps = IssueCredentialsSteps() + issueSteps.issuerOffersACredential(issuer, holder, "short") + issueSteps.holderReceivesCredentialOffer(holder) + issueSteps.holderAcceptsCredentialOfferForJwt(holder) + issueSteps.acmeIssuesTheCredential(issuer) + issueSteps.bobHasTheCredentialIssued(holder) } - @Given("{actor} has an issued credential from {actor}") - fun holderHasIssuedCredentialFromIssuer(holder: Actor, issuer: Actor) { + @Given("{actor} has a jwt issued credential with {} schema from {actor}") + fun holderHasIssuedCredentialFromIssuerWithSchema( + holder: Actor, + schema: CredentialSchema, + issuer: Actor, + ) { actorsHaveExistingConnection(issuer, holder) val publishDidSteps = PublishDidSteps() - publishDidSteps.createsUnpublishedDid(holder) - publishDidSteps.agentHasAPublishedDID(issuer) + publishDidSteps.agentHasAnUnpublishedDID(holder, DidPurpose.JWT) + publishDidSteps.agentHasAPublishedDID(issuer, DidPurpose.JWT) + + val schemaSteps = CredentialSchemasSteps() + schemaSteps.agentHasAPublishedSchema(issuer, schema) val issueSteps = IssueCredentialsSteps() - issueSteps.issuerOffersACredential(issuer, holder, "short") + issueSteps.issuerOffersCredentialToHolderUsingSchema(issuer, holder, "short", schema) issueSteps.holderReceivesCredentialOffer(holder) issueSteps.holderAcceptsCredentialOfferForJwt(holder) issueSteps.acmeIssuesTheCredential(issuer) diff --git a/tests/integration-tests/src/test/kotlin/steps/common/ParameterSteps.kt b/tests/integration-tests/src/test/kotlin/steps/common/ParameterSteps.kt new file mode 100644 index 0000000000..d580395788 --- /dev/null +++ b/tests/integration-tests/src/test/kotlin/steps/common/ParameterSteps.kt @@ -0,0 +1,29 @@ +package steps.common + +import io.cucumber.java.DataTableType +import io.cucumber.java.ParameterType +import net.serenitybdd.screenplay.Actor +import net.serenitybdd.screenplay.actors.OnStage +import org.hyperledger.identus.client.models.* + +class ParameterSteps { + @ParameterType(".*") + fun actor(actorName: String): Actor { + return OnStage.theActorCalled(actorName) + } + + @ParameterType(".*") + fun curve(value: String): Curve { + return Curve.decode(value) ?: throw IllegalArgumentException("$value is not a valid Curve value") + } + + @ParameterType(".*") + fun purpose(value: String): Purpose { + return Purpose.decode(value) ?: throw IllegalArgumentException("$value is not a valid Purpose value") + } + + @DataTableType + fun vcVerification(cell: String): VcVerification { + return VcVerification.valueOf(cell) + } +} diff --git a/tests/integration-tests/src/test/kotlin/steps/connection/ConnectionSteps.kt b/tests/integration-tests/src/test/kotlin/steps/connection/ConnectionSteps.kt index 99d0ed603e..886bb64b86 100644 --- a/tests/integration-tests/src/test/kotlin/steps/connection/ConnectionSteps.kt +++ b/tests/integration-tests/src/test/kotlin/steps/connection/ConnectionSteps.kt @@ -3,6 +3,7 @@ package steps.connection import abilities.ListenToEvents import interactions.Get import interactions.Post +import interactions.body import io.cucumber.java.en.Then import io.cucumber.java.en.When import io.iohk.atala.automation.extensions.get @@ -22,13 +23,9 @@ class ConnectionSteps { // Acme(Issuer) initiates a connection // and sends it to Bob(Holder) out-of-band, e.g. using QR-code val connectionLabel = "Connection with ${invitee.name}" + inviter.attemptsTo( - Post.to("/connections") - .with { - it.body( - CreateConnectionRequest(label = connectionLabel), - ) - }, + Post.to("/connections").body(CreateConnectionRequest(label = connectionLabel)), ) val connection = SerenityRest.lastResponse().get() diff --git a/tests/integration-tests/src/test/kotlin/steps/credentials/IssueCredentialsSteps.kt b/tests/integration-tests/src/test/kotlin/steps/credentials/IssueCredentialsSteps.kt index 147e5989cf..1688e3a585 100644 --- a/tests/integration-tests/src/test/kotlin/steps/credentials/IssueCredentialsSteps.kt +++ b/tests/integration-tests/src/test/kotlin/steps/credentials/IssueCredentialsSteps.kt @@ -3,6 +3,7 @@ package steps.credentials import abilities.ListenToEvents import common.CredentialSchema import interactions.Post +import interactions.body import io.cucumber.java.en.Then import io.cucumber.java.en.When import io.iohk.atala.automation.extensions.get @@ -50,10 +51,7 @@ class IssueCredentialsSteps { ) issuer.attemptsTo( - Post.to("/issue-credentials/credential-offers") - .with { - it.body(credentialOfferRequest) - }, + Post.to("/issue-credentials/credential-offers").body(credentialOfferRequest), ) } @@ -85,11 +83,8 @@ class IssueCredentialsSteps { format: String, schema: CredentialSchema, ) { - val schemaGuid = issuer.recall(schema.name)!! - val claims = linkedMapOf( - "name" to "Name", - "age" to 18, - ) + val schemaGuid = issuer.recall(schema.name) + val claims = schema.claims sendCredentialOffer(issuer, holder, format, schemaGuid, claims) saveCredentialOffer(issuer, holder) } @@ -217,6 +212,7 @@ class IssueCredentialsSteps { it.data.thid == issuer.recall("thid") } issuer.remember("issuedCredential", credentialEvent!!.data) + credentialEvent != null && credentialEvent!!.data.protocolState == IssueCredentialRecord.ProtocolState.CREDENTIAL_SENT } diff --git a/tests/integration-tests/src/test/kotlin/steps/credentials/RevokeCredentialSteps.kt b/tests/integration-tests/src/test/kotlin/steps/credentials/RevokeCredentialSteps.kt index fcad462ec7..ab8d16d289 100644 --- a/tests/integration-tests/src/test/kotlin/steps/credentials/RevokeCredentialSteps.kt +++ b/tests/integration-tests/src/test/kotlin/steps/credentials/RevokeCredentialSteps.kt @@ -4,6 +4,7 @@ import interactions.* import io.cucumber.java.en.Then import io.cucumber.java.en.When import io.iohk.atala.automation.extensions.get +import io.iohk.atala.automation.extensions.toJsonPath import io.iohk.atala.automation.serenity.ensure.Ensure import io.iohk.atala.automation.utils.Wait import models.JwtCredential @@ -17,8 +18,8 @@ class RevokeCredentialSteps { @When("{actor} revokes the credential issued to {actor}") fun issuerRevokesCredentialsIssuedToHolder(issuer: Actor, holder: Actor) { val issuedCredential = issuer.recall("issuedCredential") - val jwtCred = JwtCredential(issuedCredential.credential!!) - val statusListId = jwtCred.statusListId() + val jwtCred = JwtCredential.parseBase64(issuedCredential.credential!!) + val statusListId = statusListId(jwtCred) issuer.remember("statusListId", statusListId) issuer.attemptsTo( @@ -59,7 +60,6 @@ class RevokeCredentialSteps { Get.resource("/credential-status/$statusListId"), ) val actualEncodedList: String = SerenityRest.lastResponse().jsonPath().get("credentialSubject.encodedList") - println("actual encoded $actualEncodedList | before encoded $encodedStatusList") actualEncodedList != encodedStatusList } } @@ -67,12 +67,18 @@ class RevokeCredentialSteps { @Then("{actor} should see the credential is not revoked") fun issuerShouldSeeTheCredentialIsNotRevoked(issuer: Actor) { val issuedCredential = issuer.recall("issuedCredential") - val jwtCred = JwtCredential(issuedCredential.credential!!) - val statusListId = jwtCred.statusListId() + val jwtCred = JwtCredential.parseBase64(issuedCredential.credential!!) + val statusListId = statusListId(jwtCred) issuer.remember("statusListId", statusListId) - issuer.attemptsTo( Get.resource("/credential-status/$statusListId"), ) } + + private fun statusListId(jwtCredential: JwtCredential): String { + val listUrl = jwtCredential.payload!! + .toJSONObject().toJsonPath() + .read("$.vc.credentialStatus.statusListCredential") + return listUrl.split("/credential-status/")[1] + } } diff --git a/tests/integration-tests/src/test/kotlin/steps/did/ManageDidSteps.kt b/tests/integration-tests/src/test/kotlin/steps/did/ManageDidSteps.kt index b77cf8fe74..b9589a8754 100644 --- a/tests/integration-tests/src/test/kotlin/steps/did/ManageDidSteps.kt +++ b/tests/integration-tests/src/test/kotlin/steps/did/ManageDidSteps.kt @@ -2,6 +2,7 @@ package steps.did import interactions.Get import interactions.Post +import interactions.body import io.cucumber.java.en.* import io.iohk.atala.automation.extensions.get import io.iohk.atala.automation.serenity.ensure.Ensure @@ -31,10 +32,7 @@ class ManageDidSteps { val createDidRequest = createPrismDidRequest(curve, purpose) actor.attemptsTo( - Post.to("/did-registrar/dids") - .with { - it.body(createDidRequest) - }, + Post.to("/did-registrar/dids").body(createDidRequest), ) if (SerenityRest.lastResponse().statusCode() == SC_CREATED) { diff --git a/tests/integration-tests/src/test/kotlin/steps/did/PublishDidSteps.kt b/tests/integration-tests/src/test/kotlin/steps/did/PublishDidSteps.kt index 0b766d65b4..cd4302d194 100644 --- a/tests/integration-tests/src/test/kotlin/steps/did/PublishDidSteps.kt +++ b/tests/integration-tests/src/test/kotlin/steps/did/PublishDidSteps.kt @@ -1,9 +1,10 @@ package steps.did import abilities.ListenToEvents -import common.TestConstants +import common.DidPurpose import interactions.Get import interactions.Post +import interactions.body import io.cucumber.java.en.* import io.iohk.atala.automation.extensions.get import io.iohk.atala.automation.serenity.ensure.Ensure @@ -17,28 +18,44 @@ import org.hyperledger.identus.client.models.* import kotlin.time.Duration.Companion.seconds class PublishDidSteps { + + @Given("{actor} has a published DID for {}") + fun agentHasAPublishedDID(agent: Actor, didPurpose: DidPurpose) { + if (agent.recallAll().containsKey("hasPublishedDid") && actualDidHasSamePurpose(agent, didPurpose)) { + return + } + agentHasAnUnpublishedDID(agent, didPurpose) + hePublishesDidToLedger(agent) + } + + @Given("{actor} has an unpublished DID for {}") + fun agentHasAnUnpublishedDID(agent: Actor, didPurpose: DidPurpose) { + if (agent.recallAll().containsKey("shortFormDid") || agent.recallAll().containsKey("longFormDid")) { + // is not published and has the same purpose + if (!agent.recallAll().containsKey("hasPublishedDid") && actualDidHasSamePurpose(agent, didPurpose)) { + return + } + } + agentCreatesUnpublishedDid(agent, didPurpose) + } + + private fun actualDidHasSamePurpose(agent: Actor, didPurpose: DidPurpose): Boolean { + val actualPurpose: DidPurpose = agent.recall("didPurpose") ?: return false + return actualPurpose == didPurpose + } + @Given("{actor} creates unpublished DID") - fun createsUnpublishedDid(actor: Actor) { + fun agentCreatesEmptyUnpublishedDid(actor: Actor) { + agentCreatesUnpublishedDid(actor, DidPurpose.EMPTY) + } + + @Given("{actor} creates unpublished DID for {}") + fun agentCreatesUnpublishedDid(actor: Actor, didPurpose: DidPurpose) { val createDidRequest = CreateManagedDidRequest( - CreateManagedDidRequestDocumentTemplate( - publicKeys = listOf( - ManagedDIDKeyTemplate("auth-1", Purpose.AUTHENTICATION, Curve.SECP256K1), - ManagedDIDKeyTemplate("auth-2", Purpose.AUTHENTICATION, Curve.ED25519), - ManagedDIDKeyTemplate("assertion-1", Purpose.ASSERTION_METHOD, Curve.SECP256K1), - ManagedDIDKeyTemplate("comm-1", Purpose.KEY_AGREEMENT, Curve.X25519), - ), - services = listOf( - Service("https://foo.bar.com", listOf("LinkedDomains"), Json("https://foo.bar.com/")), - Service("https://update.com", listOf("LinkedDomains"), Json("https://update.com/")), - Service("https://remove.com", listOf("LinkedDomains"), Json("https://remove.com/")), - ), - ), + CreateManagedDidRequestDocumentTemplate(didPurpose.publicKeys, services = didPurpose.services), ) actor.attemptsTo( - Post.to("/did-registrar/dids") - .with { - it.body(createDidRequest) - }, + Post.to("/did-registrar/dids").body(createDidRequest), Ensure.thatTheLastResponse().statusCode().isEqualTo(SC_CREATED), ) @@ -54,42 +71,18 @@ class PublishDidSteps { actor.remember("longFormDid", managedDid.longFormDid) actor.remember("shortFormDid", did.did) + actor.remember("didPurpose", didPurpose) actor.forget("hasPublishedDid") } - @Given("{actor} has a published DID") - fun agentHasAPublishedDID(agent: Actor) { - if (agent.recallAll().containsKey("hasPublishedDid")) { - return - } - if (!agent.recallAll().containsKey("shortFormDid") && - !agent.recallAll().containsKey("longFormDid") - ) { - createsUnpublishedDid(agent) - } - hePublishesDidToLedger(agent) - } - - @Given("{actor} has an unpublished DID") - fun agentHasAnUnpublishedDID(agent: Actor) { - if (agent.recallAll().containsKey("shortFormDid") || - agent.recallAll().containsKey("longFormDid") - ) { - // is not published - if (!agent.recallAll().containsKey("hasPublishedDid")) { - return - } - } - createsUnpublishedDid(agent) - } - @When("{actor} publishes DID to ledger") fun hePublishesDidToLedger(actor: Actor) { + val shortFormDid = actor.recall("shortFormDid") actor.attemptsTo( - Post.to("/did-registrar/dids/${actor.recall("shortFormDid")}/publications"), + Post.to("/did-registrar/dids/$shortFormDid/publications"), ) - val didOperationResponse = SerenityRest.lastResponse().get() + val didOperationResponse = SerenityRest.lastResponse().get() actor.attemptsTo( Ensure.thatTheLastResponse().statusCode().isEqualTo(HttpStatus.SC_ACCEPTED), Ensure.that(didOperationResponse.scheduledOperation.didRef).isNotEmpty(), @@ -115,7 +108,7 @@ class PublishDidSteps { Ensure.thatTheLastResponse().statusCode().isEqualTo(SC_OK), Ensure.that(didDocument.id).isEqualTo(actor.recall("shortFormDid")), ) - + actor.remember("didVerification", didDocument.verificationMethod) actor.remember("hasPublishedDid", true) } @@ -126,9 +119,6 @@ class PublishDidSteps { val shortFormDid = actor.recall("shortFormDid") actor.attemptsTo( Ensure.that(didDocument.id).isEqualTo(shortFormDid), - Ensure.that(didDocument.authentication!![0]) - .isEqualTo("$shortFormDid#${TestConstants.PRISM_DID_AUTH_KEY.id}"), - Ensure.that(didDocument.verificationMethod!![0].controller).isEqualTo(shortFormDid), Ensure.that(didResolutionResult.didDocumentMetadata.deactivated!!).isFalse(), ) } diff --git a/tests/integration-tests/src/test/kotlin/steps/did/UpdateDidSteps.kt b/tests/integration-tests/src/test/kotlin/steps/did/UpdateDidSteps.kt index cb3ddf0131..b2a73bf072 100644 --- a/tests/integration-tests/src/test/kotlin/steps/did/UpdateDidSteps.kt +++ b/tests/integration-tests/src/test/kotlin/steps/did/UpdateDidSteps.kt @@ -1,8 +1,8 @@ package steps.did -import common.TestConstants import interactions.Get import interactions.Post +import interactions.body import io.cucumber.java.en.Then import io.cucumber.java.en.When import io.iohk.atala.automation.extensions.get @@ -19,85 +19,75 @@ class UpdateDidSteps { @When("{actor} updates PRISM DID by adding new key with {curve} curve and {purpose} purpose") fun actorUpdatesPrismDidByAddingNewKeys(actor: Actor, curve: Curve, purpose: Purpose) { val newDidKeyId = UUID.randomUUID().toString() - val didKey = ManagedDIDKeyTemplate( - id = newDidKeyId, - purpose = purpose, - curve = curve, - ) val updatePrismDidAction = UpdateManagedDIDRequestAction( actionType = ActionType.ADD_KEY, - addKey = didKey, + addKey = ManagedDIDKeyTemplate( + id = newDidKeyId, + purpose = purpose, + curve = curve, + ), ) actor.remember("newDidKeyId", newDidKeyId) - actor.remember("updatePrismDidAction", updatePrismDidAction) + actorSubmitsPrismDidUpdateOperation(actor, updatePrismDidAction) } @When("{actor} updates PRISM DID by removing keys") fun actorUpdatesPrismDidByRemovingKeys(actor: Actor) { + val didKeyId = actor.recall("newDidKeyId") val updatePrismDidAction = UpdateManagedDIDRequestAction( actionType = ActionType.REMOVE_KEY, - removeKey = RemoveEntryById(TestConstants.PRISM_DID_AUTH_KEY.id), + removeKey = RemoveEntryById(didKeyId), ) - actor.remember("updatePrismDidAction", updatePrismDidAction) + actorSubmitsPrismDidUpdateOperation(actor, updatePrismDidAction) } @When("{actor} updates PRISM DID with new services") fun actorUpdatesPrismDidWithNewServices(actor: Actor) { + val serviceId = UUID.randomUUID().toString() val updatePrismDidAction = UpdateManagedDIDRequestAction( actionType = ActionType.ADD_SERVICE, - addService = TestConstants.PRISM_DID_UPDATE_NEW_SERVICE, + addService = Service( + id = serviceId, + type = listOf("LinkedDomains"), + serviceEndpoint = Json("https://service.com/"), + ), ) - actor.remember("updatePrismDidAction", updatePrismDidAction) + actor.remember("newServiceId", serviceId) + actorSubmitsPrismDidUpdateOperation(actor, updatePrismDidAction) } @When("{actor} updates PRISM DID by removing services") fun actorUpdatesPrismDidByRemovingServices(actor: Actor) { + val serviceId = actor.recall("newServiceId") val updatePrismDidAction = UpdateManagedDIDRequestAction( actionType = ActionType.REMOVE_SERVICE, - removeService = RemoveEntryById(TestConstants.PRISM_DID_UPDATE_NEW_SERVICE.id), + removeService = RemoveEntryById(serviceId), ) - actor.remember("updatePrismDidAction", updatePrismDidAction) + actorSubmitsPrismDidUpdateOperation(actor, updatePrismDidAction) } @When("{actor} updates PRISM DID by updating services") fun actorUpdatesPrismDidByUpdatingServices(actor: Actor) { - val newService = UpdateManagedDIDServiceAction( - id = TestConstants.PRISM_DID_SERVICE_FOR_UPDATE.id, - type = TestConstants.PRISM_DID_SERVICE_FOR_UPDATE.type, - serviceEndpoint = Json( - TestConstants.PRISM_DID_UPDATE_NEW_SERVICE_URL, - ), - ) + val serviceId = actor.recall("newServiceId") + val newServiceUrl = "https://update.com" val updatePrismDidAction = UpdateManagedDIDRequestAction( actionType = ActionType.UPDATE_SERVICE, - updateService = newService, + updateService = UpdateManagedDIDServiceAction( + id = serviceId, + type = listOf("LinkedDomains"), + serviceEndpoint = Json(newServiceUrl), + ), ) - actor.remember("updatePrismDidAction", updatePrismDidAction) - } - @When("{actor} submits PRISM DID update operation") - fun actorSubmitsPrismDidUpdateOperation(actor: Actor) { - actor.attemptsTo( - Post.to("/did-registrar/dids/${actor.recall("shortFormDid")}/updates") - .with { - it.body(UpdateManagedDIDRequest(listOf(actor.recall("updatePrismDidAction")))) - }, - ) - val didOperationResponse = SerenityRest.lastResponse().get() - actor.attemptsTo( - Ensure.thatTheLastResponse().statusCode().isEqualTo(HttpStatus.SC_ACCEPTED), - Ensure.that(didOperationResponse.scheduledOperation.didRef).isNotEmpty(), - Ensure.that(didOperationResponse.scheduledOperation.id).isNotEmpty(), - ) + actor.remember("newServiceUrl", newServiceUrl) + actorSubmitsPrismDidUpdateOperation(actor, updatePrismDidAction) } @Then("{actor} sees PRISM DID was successfully updated with new keys of {purpose} purpose") fun actorSeesDidSuccessfullyUpdatedWithNewKeys(actor: Actor, purpose: Purpose) { val newDidKeyId = actor.recall("newDidKeyId") - var i = 0 - Wait.until( - errorMessage = "ERROR: DID UPDATE operation did not succeed on the ledger!", - ) { + + Wait.until(errorMessage = "ERROR: DID UPDATE operation did not succeed on the ledger!") { actor.attemptsTo( Get.resource("/dids/${actor.recall("shortFormDid")}"), ) @@ -115,27 +105,30 @@ class UpdateDidSteps { } } - @Then("{actor} sees PRISM DID was successfully updated and keys removed") - fun actorSeesDidSuccessfullyUpdatedAndKeysRemoved(actor: Actor) { - Wait.until( - errorMessage = "ERROR: DID UPDATE operation did not succeed on the ledger!", - ) { + @Then("{actor} sees PRISM DID was successfully updated and keys removed with {purpose} purpose") + fun actorSeesDidSuccessfullyUpdatedAndKeysRemoved(actor: Actor, purpose: Purpose) { + val newDidKeyId = actor.recall("newDidKeyId") + Wait.until(errorMessage = "ERROR: DID UPDATE operation did not succeed on the ledger!") { actor.attemptsTo( Get.resource("/dids/${actor.recall("shortFormDid")}"), ) - val authUris = SerenityRest.lastResponse().get().didDocument!!.authentication!! - val verificationMethods = SerenityRest.lastResponse() - .get().didDocument!!.verificationMethod!!.map { it.id } - authUris.none { - it == "${actor.recall("shortFormDid")}#${TestConstants.PRISM_DID_AUTH_KEY.id}" - } && verificationMethods.none { - it == "${actor.recall("shortFormDid")}#${TestConstants.PRISM_DID_AUTH_KEY.id}" + val didKey = "${actor.recall("shortFormDid")}#$newDidKeyId" + val didDocument = SerenityRest.lastResponse().get().didDocument!! + val verificationMethodNotPresent = didDocument.verificationMethod!!.map { it.id }.none { it == didKey } + + verificationMethodNotPresent && when (purpose) { + Purpose.ASSERTION_METHOD -> didDocument.assertionMethod!!.none { it == didKey } + Purpose.AUTHENTICATION -> didDocument.authentication!!.none { it == didKey } + Purpose.CAPABILITY_DELEGATION -> didDocument.capabilityDelegation!!.none { it == didKey } + Purpose.CAPABILITY_INVOCATION -> didDocument.capabilityInvocation!!.none { it == didKey } + Purpose.KEY_AGREEMENT -> didDocument.keyAgreement!!.none { it == didKey } } } } - @Then("{actor} sees PRISM DID was successfully updated with new services") + @Then("{actor} sees that PRISM DID should have the new service") fun actorSeesDidSuccessfullyUpdatedWithNewServices(actor: Actor) { + val serviceId = actor.recall("newServiceId") Wait.until( errorMessage = "ERROR: DID UPDATE operation did not succeed on the ledger!", ) { @@ -145,13 +138,14 @@ class UpdateDidSteps { val serviceIds = SerenityRest.lastResponse().get().didDocument!!.service!!.map { it.id } serviceIds.any { - it == "${actor.recall("shortFormDid")}#${TestConstants.PRISM_DID_UPDATE_NEW_SERVICE.id}" + it == "${actor.recall("shortFormDid")}#$serviceId" } } } - @Then("{actor} sees PRISM DID was successfully updated by removing services") + @Then("{actor} sees the PRISM DID should have the service removed") fun actorSeesDidSuccessfullyUpdatedByRemovingServices(actor: Actor) { + val serviceId = actor.recall("newServiceId") Wait.until( errorMessage = "ERROR: DID UPDATE operation did not succeed on the ledger!", ) { @@ -161,13 +155,14 @@ class UpdateDidSteps { val serviceIds = SerenityRest.lastResponse().get().didDocument!!.service!!.map { it.id } serviceIds.none { - it == "${actor.recall("shortFormDid")}#${TestConstants.PRISM_DID_UPDATE_NEW_SERVICE.id}" + it == "${actor.recall("shortFormDid")}#$serviceId" } } } - @Then("{actor} sees PRISM DID was successfully updated by updating services") + @Then("{actor} sees the PRISM DID should have the service updated") fun actorSeesDidSuccessfullyUpdatedByUpdatingServices(actor: Actor) { + val serviceUrl = actor.recall("newServiceUrl") Wait.until( errorMessage = "ERROR: DID UPDATE operation did not succeed on the ledger!", ) { @@ -175,7 +170,36 @@ class UpdateDidSteps { Get.resource("/dids/${actor.recall("shortFormDid")}"), ) val service = SerenityRest.lastResponse().get().didDocument!!.service!! - service.any { it.serviceEndpoint.value.contains(TestConstants.PRISM_DID_UPDATE_NEW_SERVICE_URL) } + service.any { it.serviceEndpoint.value.contains(serviceUrl) } } } + + private fun actorSubmitsPrismDidUpdateOperation(actor: Actor, updatePrismDidAction: UpdateManagedDIDRequestAction) { + actor.attemptsTo( + Post.to("/did-registrar/dids/${actor.recall("shortFormDid")}/updates") + .body(UpdateManagedDIDRequest(listOf(updatePrismDidAction))), + ) + } + + @Then("{actor} sees the PRISM DID should have been updated successfully") + fun checkIfUpdateWasSuccessful(actor: Actor) { + val didOperationResponse = SerenityRest.lastResponse().get() + actor.attemptsTo( + Ensure.thatTheLastResponse().statusCode().isEqualTo(HttpStatus.SC_ACCEPTED), + Ensure.that(didOperationResponse.scheduledOperation.didRef).isNotEmpty(), + Ensure.that(didOperationResponse.scheduledOperation.id).isNotEmpty(), + ) + } + + @Then("{actor} sees the PRISM DID was not successfully updated") + fun checkIfUpdateWasNotSuccessful(actor: Actor) { + val detail: String = SerenityRest.lastResponse().get("detail") + actor.attemptsTo( + Ensure.thatTheLastResponse().statusCode().isEqualTo(HttpStatus.SC_BAD_REQUEST), + Ensure.that(detail) + .contains( + "Ed25519 must be used in [Authentication, AssertionMethod]. X25519 must be used in [KeyAgreement]", + ), + ) + } } diff --git a/tests/integration-tests/src/test/kotlin/steps/proofs/PresentProofSteps.kt b/tests/integration-tests/src/test/kotlin/steps/proofs/PresentProofSteps.kt index d6452a45e8..b0599b7627 100644 --- a/tests/integration-tests/src/test/kotlin/steps/proofs/PresentProofSteps.kt +++ b/tests/integration-tests/src/test/kotlin/steps/proofs/PresentProofSteps.kt @@ -3,6 +3,7 @@ package steps.proofs import abilities.ListenToEvents import interactions.Patch import interactions.Post +import interactions.body import io.cucumber.java.en.Then import io.cucumber.java.en.When import io.iohk.atala.automation.extensions.get @@ -11,16 +12,17 @@ import io.iohk.atala.automation.utils.Wait import models.PresentationStatusAdapter import net.serenitybdd.rest.SerenityRest import net.serenitybdd.screenplay.Actor -import org.apache.http.HttpStatus.SC_CREATED +import org.apache.http.HttpStatus.* import org.hyperledger.identus.client.models.* import kotlin.time.Duration.Companion.seconds class PresentProofSteps { @When("{actor} sends a request for proof presentation to {actor}") - fun faberSendsARequestForProofPresentationToBob(faber: Actor, bob: Actor) { + fun verifierSendsARequestForProofPresentationToHolder(verifier: Actor, holder: Actor) { + val verifierConnectionToHolder = verifier.recall("connection-with-${holder.name}").connectionId val presentationRequest = RequestPresentationInput( - connectionId = faber.recall("connection-with-${bob.name}").connectionId, + connectionId = verifierConnectionToHolder, options = Options( challenge = "11c91493-01b3-4c4d-ac36-b336bab5bddf", domain = "https://example-verifier.com", @@ -32,54 +34,47 @@ class PresentProofSteps { ), ), ) - faber.attemptsTo( - Post.to("/present-proof/presentations") - .with { - it.body( - presentationRequest, - ) - }, - ) - faber.attemptsTo( + verifier.attemptsTo( + Post.to("/present-proof/presentations").body(presentationRequest), Ensure.thatTheLastResponse().statusCode().isEqualTo(SC_CREATED), ) val presentationStatus = SerenityRest.lastResponse().get() - faber.remember("thid", presentationStatus.thid) - bob.remember("thid", presentationStatus.thid) + verifier.remember("thid", presentationStatus.thid) + holder.remember("thid", presentationStatus.thid) } - @When("{actor} receives the request") - fun bobReceivesTheRequest(bob: Actor) { + @When("{actor} receives the presentation proof request") + fun holderReceivesTheRequest(holder: Actor) { Wait.until( timeout = 30.seconds, errorMessage = "ERROR: Bob did not achieve any presentation request!", ) { - val proofEvent = ListenToEvents.with(bob).presentationEvents.lastOrNull { - it.data.thid == bob.recall("thid") + val proofEvent = ListenToEvents.with(holder).presentationEvents.lastOrNull { + it.data.thid == holder.recall("thid") } - bob.remember("presentationId", proofEvent?.data?.presentationId) + holder.remember("presentationId", proofEvent?.data?.presentationId) proofEvent?.data?.status == PresentationStatusAdapter.Status.REQUEST_RECEIVED } } - @When("{actor} makes the presentation of the proof to {actor}") - fun bobMakesThePresentationOfTheProof(bob: Actor, faber: Actor) { + @When("{actor} makes the presentation of the proof") + fun holderMakesThePresentationOfTheProofToVerifier(holder: Actor) { val requestPresentationAction = RequestPresentationAction( - proofId = listOf(bob.recall("issuedCredential").recordId), + proofId = listOf(holder.recall("issuedCredential").recordId), action = RequestPresentationAction.Action.REQUEST_MINUS_ACCEPT, ) - - bob.attemptsTo( - Patch.to("/present-proof/presentations/${bob.recall("presentationId")}").with { - it.body(requestPresentationAction) - }, + val presentationId: String = holder.recall("presentationId") + holder.attemptsTo( + Patch.to("/present-proof/presentations/$presentationId").body(requestPresentationAction), + Ensure.thatTheLastResponse().statusCode().isEqualTo(SC_OK), ) } @When("{actor} rejects the proof") - fun bobRejectsProof(bob: Actor) { - bob.attemptsTo( - Patch.to("/present-proof/presentations/${bob.recall("presentationId")}").with { + fun holderRejectsProof(holder: Actor) { + val presentationId: String = holder.recall("presentationId") + holder.attemptsTo( + Patch.to("/present-proof/presentations/$presentationId").with { it.body( RequestPresentationAction(action = RequestPresentationAction.Action.REQUEST_MINUS_REJECT), ) diff --git a/tests/integration-tests/src/test/kotlin/steps/schemas/CredentialSchemasSteps.kt b/tests/integration-tests/src/test/kotlin/steps/schemas/CredentialSchemasSteps.kt index 3fecf2e436..ae14368831 100644 --- a/tests/integration-tests/src/test/kotlin/steps/schemas/CredentialSchemasSteps.kt +++ b/tests/integration-tests/src/test/kotlin/steps/schemas/CredentialSchemasSteps.kt @@ -1,14 +1,9 @@ package steps.schemas -import com.google.gson.Gson -import com.google.gson.JsonObject -import common.CredentialSchema +import common.* import common.CredentialSchema.STUDENT_SCHEMA -import interactions.Get -import interactions.Post -import io.cucumber.java.en.Given -import io.cucumber.java.en.Then -import io.cucumber.java.en.When +import interactions.* +import io.cucumber.java.en.* import io.iohk.atala.automation.extensions.get import io.iohk.atala.automation.serenity.ensure.Ensure import models.JsonSchema @@ -32,22 +27,16 @@ class CredentialSchemasSteps { @When("{actor} creates a new credential {} schema") fun agentCreatesANewCredentialSchema(actor: Actor, schema: CredentialSchema) { actor.attemptsTo( - Post.to("/schema-registry/schemas").with { - it.body( - schema.credentialSchema.copy(author = actor.recall("shortFormDid")), - ) - }, + Post.to("/schema-registry/schemas").body( + schema.credentialSchema.copy(author = actor.recall("shortFormDid")), + ), ) actor.remember(schema.name, SerenityRest.lastResponse().get("guid")) } @When("{actor} creates a schema containing '{}' issue") fun agentCreatesASchemaContainingIssue(actor: Actor, schema: SchemaErrorTemplate) { - actor.attemptsTo( - Post.to("/schema-registry/schemas").with { - it.body(schema.schema(actor)) - }, - ) + actor.attemptsTo(Post.to("/schema-registry/schemas").body(schema.schema(actor))) } @Then("{actor} sees new credential schema is available") @@ -76,22 +65,17 @@ class CredentialSchemasSteps { fun acmeCreatesMultipleSchemas(actor: Actor, numberOfSchemas: Int) { val createdSchemas: MutableList = mutableListOf() repeat(numberOfSchemas) { i: Int -> - actor.attemptsTo( - Post.to("/schema-registry/schemas").with { - it.body( - CredentialSchemaInput( - author = actor.recall("shortFormDid"), - name = "${UUID.randomUUID()} $i", - description = "Simple student credentials schema", - type = STUDENT_SCHEMA.credentialSchemaType, - schema = STUDENT_SCHEMA.schema, - tags = listOf("school", "students"), - version = "1.0.0", - ), - ) - }, + val credentialSchemaInput = CredentialSchemaInput( + author = actor.recall("shortFormDid"), + name = "${UUID.randomUUID()} $i", + description = "Simple student credentials schema", + type = STUDENT_SCHEMA.credentialSchemaType, + schema = STUDENT_SCHEMA.schema, + tags = listOf("school", "students"), + version = "1.0.0", ) actor.attemptsTo( + Post.to("/schema-registry/schemas").body(credentialSchemaInput), Ensure.thatTheLastResponse().statusCode().isEqualTo(SC_CREATED), ) createdSchemas.add(SerenityRest.lastResponse().get()) @@ -104,8 +88,6 @@ class CredentialSchemasSteps { actor.recall>("createdSchemas").forEach { schema -> actor.attemptsTo( Get.resource("/schema-registry/schemas/${schema.guid}"), - ) - actor.attemptsTo( Ensure.thatTheLastResponse().statusCode().isEqualTo(SC_OK), ) } @@ -118,75 +100,3 @@ class CredentialSchemasSteps { ) } } - -enum class SchemaErrorTemplate { - TYPE_AND_PROPERTIES_WITHOUT_SCHEMA_TYPE { - override fun inner_schema(): String { - return """ - { - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "age": { - "type": "integer" - } - }, - "required": ["name"] - } - """.trimIndent() - } - }, - CUSTOM_WORDS_NOT_DEFINED { - override fun inner_schema(): String { - return """ - { - "${"$"}schema": "http://json-schema.org/draft-2020-12/schema#", - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "age": { - "type": "integer" - } - }, - "customKeyword": "value" - } - """.trimIndent() - } - }, - MISSING_REQUIRED_FOR_MANDATORY_PROPERTY { - override fun inner_schema(): String { - return """ - { - "${"$"}schema": "http://json-schema.org/draft-2020-12/schema#", - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "age": { - "type": "integer" - } - } - } - """ - } - }, ; - - abstract fun inner_schema(): String - - fun schema(actor: Actor): String { - val innerSchema = Gson().fromJson(inner_schema(), JsonObject::class.java) - val json = getJson(actor) - json.add("schema", innerSchema) - return json.toString() - } - - private fun getJson(actor: Actor): JsonObject { - val jsonString = Gson().toJson(STUDENT_SCHEMA.credentialSchema.copy(author = actor.recall("shortFormDid"))) - return Gson().fromJson(jsonString, JsonObject::class.java) - } -} diff --git a/tests/integration-tests/src/test/kotlin/steps/verification/VcVerificationSteps.kt b/tests/integration-tests/src/test/kotlin/steps/verification/VcVerificationSteps.kt index fa3addf6ef..32f285a197 100644 --- a/tests/integration-tests/src/test/kotlin/steps/verification/VcVerificationSteps.kt +++ b/tests/integration-tests/src/test/kotlin/steps/verification/VcVerificationSteps.kt @@ -1,106 +1,139 @@ package steps.verification -import com.google.gson.Gson -import io.cucumber.java.en.When +import com.nimbusds.jose.JWSAlgorithm +import com.nimbusds.jose.jwk.Curve +import common.* +import interactions.Post +import interactions.body +import io.cucumber.datatable.DataTable +import io.cucumber.java.en.* +import io.iohk.atala.automation.extensions.getList import io.iohk.atala.automation.serenity.ensure.Ensure -import io.restassured.http.Header +import models.JwtCredential import net.serenitybdd.rest.SerenityRest import net.serenitybdd.screenplay.Actor -import net.serenitybdd.screenplay.rest.interactions.Post +import org.apache.http.HttpStatus.SC_BAD_REQUEST import org.apache.http.HttpStatus.SC_OK import org.hyperledger.identus.client.models.* +import org.hyperledger.identus.client.models.VcVerification.* import java.time.OffsetDateTime +typealias Verification = ParameterizableVcVerification + class VcVerificationSteps { - @When("{actor} verifies VcVerificationRequest") - fun agentVerifiesVerifiableCredential(actor: Actor) { - val signedJwtCredential = - "eyJhbGciOiJFUzI1NksifQ.eyJpc3MiOiJkaWQ6cHJpc206NDE1ODg1OGI1ZjBkYWMyZTUwNDdmMjI4NTk4OWVlMzlhNTNkZWJhNzY0NjFjN2FmMDM5NjU0ZGYzYjU5MjI1YyIsImF1ZCI6ImRpZDpwcmlzbTp2ZXJpZmllciIsIm5iZiI6MTI2MjMwNDAwMCwiZXhwIjoxMjYzMjU0NDAwLCJ2YyI6eyJAY29udGV4dCI6WyJodHRwczovL3d3dy53My5vcmcvMjAxOC9jcmVkZW50aWFscy92MSIsImh0dHBzOi8vd3d3LnczLm9yZy8yMDE4L2NyZWRlbnRpYWxzL2V4YW1wbGVzL3YxIl0sInR5cGUiOlsiVmVyaWZpYWJsZUNyZWRlbnRpYWwiLCJVbml2ZXJzaXR5RGVncmVlQ3JlZGVudGlhbCJdLCJjcmVkZW50aWFsU2NoZW1hIjp7ImlkIjoiZGlkOndvcms6TURQOEFzRmhIemh3VXZHTnVZa1g3VDtpZD0wNmUxMjZkMS1mYTQ0LTQ4ODItYTI0My0xZTMyNmZiZTIxZGI7dmVyc2lvbj0xLjAiLCJ0eXBlIjoiSnNvblNjaGVtYVZhbGlkYXRvcjIwMTgifSwiY3JlZGVudGlhbFN1YmplY3QiOnsidXNlck5hbWUiOiJCb2IiLCJhZ2UiOjQyLCJlbWFpbCI6ImVtYWlsIn0sImNyZWRlbnRpYWxTdGF0dXMiOnsiaWQiOiJkaWQ6d29yazpNRFA4QXNGaEh6aHdVdkdOdVlrWDdUO2lkPTA2ZTEyNmQxLWZhNDQtNDg4Mi1hMjQzLTFlMzI2ZmJlMjFkYjt2ZXJzaW9uPTEuMCIsInR5cGUiOiJTdGF0dXNMaXN0MjAyMUVudHJ5Iiwic3RhdHVzUHVycG9zZSI6IlJldm9jYXRpb24iLCJzdGF0dXNMaXN0SW5kZXgiOjAsInN0YXR1c0xpc3RDcmVkZW50aWFsIjoiaHR0cHM6Ly9leGFtcGxlLmNvbS9jcmVkZW50aWFscy9zdGF0dXMvMyJ9LCJyZWZyZXNoU2VydmljZSI6eyJpZCI6Imh0dHBzOi8vZXhhbXBsZS5lZHUvcmVmcmVzaC8zNzMyIiwidHlwZSI6Ik1hbnVhbFJlZnJlc2hTZXJ2aWNlMjAxOCJ9fSwianRpIjoiaHR0cDovL2V4YW1wbGUuZWR1L2NyZWRlbnRpYWxzLzM3MzIifQ.HBxrn8Nu6y1RvUAU8XcwUDPOiiHhC1OgHN757lWai6i8P-pHL4TBzIDartYtrMiZUKpNx9Onb19sJYywtqFkpg" - - val request = - arrayOf( - VcVerificationRequest( - signedJwtCredential, - listOf( - ParameterizableVcVerification( - VcVerification.NOT_BEFORE_CHECK, - DateTimeParameter(dateTime = OffsetDateTime.parse("2010-01-01T00:00:00Z")), - ), - ParameterizableVcVerification( - VcVerification.EXPIRATION_CHECK, - DateTimeParameter(dateTime = OffsetDateTime.parse("2010-01-01T00:00:00Z")), - ), - ParameterizableVcVerification( - VcVerification.AUDIENCE_CHECK, - DidParameter(did = "did:prism:verifier"), - ), - ParameterizableVcVerification( - VcVerification.ISSUER_IDENTIFICATION, - DidParameter(did = "did:prism:4158858b5f0dac2e5047f2285989ee39a53deba76461c7af039654df3b59225c"), - ), - ), - ), - VcVerificationRequest( - signedJwtCredential, - listOf( - ParameterizableVcVerification( - VcVerification.EXPIRATION_CHECK, - DateTimeParameter(dateTime = OffsetDateTime.parse("2010-01-13T00:00:00Z")), - ), - ParameterizableVcVerification( - VcVerification.NOT_BEFORE_CHECK, - DateTimeParameter(dateTime = OffsetDateTime.parse("2009-01-01T00:00:00Z")), - ), - ParameterizableVcVerification( - VcVerification.AUDIENCE_CHECK, - DidParameter(did = "BAD AUDIENCE"), - ), - ParameterizableVcVerification( - VcVerification.ISSUER_IDENTIFICATION, - DidParameter(did = "BAD ISSUER"), - ), - ), - ), - ) + @Given("{actor} uses that JWT VC issued from {actor} for Verification API") + fun holderUsesThatJWTVCForVerificationAPI(holder: Actor, issuer: Actor) { + val issuedCredential = holder.recall("issuedCredential") + val jwt = JwtCredential.parseBase64(issuedCredential.credential!!).serialize() + val issuerDid = issuer.recall("shortFormDid") + holder.remember("jwt", jwt) + holder.remember("issuerDid", issuerDid) + } - val post = - Post.to("/verification/credential").with { - it.header(Header("apiKey", "pylnapbvyudwmfrt")) - it.body(request) - } + @Given("{actor} has a JWT VC for Verification API") + fun holderHasAJWTVCForVerificationAPI(holder: Actor) { + val jwtCredential = VerifiableJwt.jwtVCv1() + val jwt = jwtCredential.sign(JWSAlgorithm.ES256K, Curve.SECP256K1) + holder.remember("jwt", jwt) + holder.remember("issuerDid", "did:prism:issuer") + } + + @Given("{actor} has a Verifiable Schema for Verification API") + fun holderHasAVerifiableSchemaForVerificationAPI(holder: Actor) { + val jwtCredential = VerifiableJwt.schemaVCv1() + val jwt = jwtCredential.sign(JWSAlgorithm.ES384, Curve.SECP256K1) + holder.remember("jwt", jwt) + holder.remember("issuerDid", "did:prism:issuer") + } - actor.attemptsTo( - post, + @Given("{actor} has a {} problem in the Verifiable Credential") + fun holderHasProblemInTheVerifiableCredential(holder: Actor, problem: JwtCredentialProblem) { + val jwt = problem.jwt() + holder.remember("jwt", jwt) + holder.remember("issuerDid", "did:prism:issuer") + } + + @When("{actor} sends the JWT Credential to {actor} Verification API") + fun holderSendsJwtCredentialToVerificationAPI(holder: Actor, verifier: Actor, dataTable: DataTable) { + // add type to datatable + val checks = dataTable.asMap(VcVerification::class.java, Boolean::class.java) + val jwt: String = holder.recall("jwt") + val issuerDid: String = holder.recall("issuerDid") + verifyJwt(verifier, jwt, issuerDid, checks) + holder.remember("checks", checks) + } + + @Then("{actor} should see that verification has failed with {} problem") + fun holderShouldSeeThatVerificationHasFailedWithProblem(holder: Actor, problem: JwtCredentialProblem) { + } + + @Then("{actor} should see that all checks have passed") + fun holderShouldSeeThatVerificationHasPassed(holder: Actor) { + val jwt: String = holder.recall("jwt") + analyzeResponse(holder, jwt) + } + + @Then("{actor} should see the check has failed") + fun holderShouldSeeTheCheckHasFailed(holder: Actor) { + holder.attemptsTo( + Ensure.thatTheLastResponse().statusCode().isEqualTo(SC_BAD_REQUEST), ) + } - val expected = listOf( - VcVerificationResponse( - signedJwtCredential, - listOf( - VcVerificationResult(VcVerification.NOT_BEFORE_CHECK, true), - VcVerificationResult(VcVerification.EXPIRATION_CHECK, true), - VcVerificationResult(VcVerification.AUDIENCE_CHECK, true), - VcVerificationResult(VcVerification.ISSUER_IDENTIFICATION, true), - ), - ), - VcVerificationResponse( - signedJwtCredential, - listOf( - VcVerificationResult(VcVerification.EXPIRATION_CHECK, false), - VcVerificationResult(VcVerification.NOT_BEFORE_CHECK, false), - VcVerificationResult(VcVerification.AUDIENCE_CHECK, false), - VcVerificationResult(VcVerification.ISSUER_IDENTIFICATION, false), - ), - ), + private fun verifyJwt( + verifier: Actor, + jwt: String, + issuerDid: String, + verifications: Map, + ) { + val now = OffsetDateTime.now() + + // creates the checks based on the data table from feature file + val checks = verifications.map { (key, value) -> + when (key) { + ALGORITHM_VERIFICATION -> Verification(key) to value + AUDIENCE_CHECK -> Verification(key, DidParameter(did = "did:prism:verifier")) to value + COMPLIANCE_WITH_STANDARDS -> Verification(key) to value + EXPIRATION_CHECK -> Verification(key, DateTimeParameter(dateTime = now.minusDays(5))) to value + INTEGRITY_OF_CLAIMS -> Verification(key) to value + ISSUER_IDENTIFICATION -> Verification(key, DidParameter(did = issuerDid)) to value + NOT_BEFORE_CHECK -> Verification(key, DateTimeParameter(dateTime = now.plusDays(5))) to value + REVOCATION_CHECK -> Verification(key) to value + SCHEMA_CHECK -> Verification(key) to value + SEMANTIC_CHECK_OF_CLAIMS -> Verification(key) to value + SIGNATURE_VERIFICATION -> Verification(key) to value + SUBJECT_VERIFICATION -> Verification(key, DidParameter(did = "did:prism:something")) to value + } + }.toMap() + + val vcVerificationRequest = VcVerificationRequest(jwt, checks.keys.toList()) + val payload = listOf(vcVerificationRequest) + + verifier.attemptsTo( + Post.to("/verification/credential").body(payload), ) - actor.attemptsTo( + } + + private fun analyzeResponse(holder: Actor, jwt: String) { + val checks = holder.recall>("checks") + val actual = SerenityRest.lastResponse().getList() + + holder.attemptsTo( Ensure.thatTheLastResponse().statusCode().isEqualTo(SC_OK), - Ensure.that( - SerenityRest.lastResponse().body().asString(), - ).isEqualTo( - Gson().toJson(expected), - ), + Ensure.that(actual[0].credential).isEqualTo(jwt), ) + + // check each verification result from the requested checks + actual[0].result!!.forEach { + val expected = checks.getOrElse(it.verification) { + throw RuntimeException("Couldn't find ${it.verification} in verification request.") + } + + holder.attemptsTo( + Ensure.that(it.success).isEqualTo(expected) + .withReportedError("Expected [${it.verification}] to be [$expected] but got [${it.success}]"), + ) + } } } diff --git a/tests/integration-tests/src/test/resources/features/credentials/issue_anoncred_with_published_did.feature b/tests/integration-tests/src/test/resources/features/credentials/issue_anoncred_with_published_did.feature new file mode 100644 index 0000000000..051e8df412 --- /dev/null +++ b/tests/integration-tests/src/test/resources/features/credentials/issue_anoncred_with_published_did.feature @@ -0,0 +1,15 @@ +@RFC0453 @AIP20 @credentials +Feature: Issue Anoncred with published DID + + Background: + Given Issuer and Holder have an existing connection + And Issuer has a published DID for ANONCRED + And Holder has an unpublished DID for ANONCRED + + Scenario: Issuing anoncred with published PRISM DID + Given Issuer has an anoncred schema definition + When Issuer offers anoncred to Holder + And Holder receives the credential offer + And Holder accepts credential offer for anoncred + And Issuer issues the credential + Then Holder receives the issued credential diff --git a/tests/integration-tests/src/test/resources/features/credentials/issue_published_did.feature b/tests/integration-tests/src/test/resources/features/credentials/issue_jwt_with_published_did.feature similarity index 90% rename from tests/integration-tests/src/test/resources/features/credentials/issue_published_did.feature rename to tests/integration-tests/src/test/resources/features/credentials/issue_jwt_with_published_did.feature index c44fa3c948..e54fd17c2d 100644 --- a/tests/integration-tests/src/test/resources/features/credentials/issue_published_did.feature +++ b/tests/integration-tests/src/test/resources/features/credentials/issue_jwt_with_published_did.feature @@ -1,11 +1,11 @@ @RFC0453 @AIP20 @credentials -Feature: Issue Credentials Protocol with published DID +Feature: Issue JWT Credentials with published DID Background: Given Issuer and Holder have an existing connection - And Issuer has a published DID + And Issuer has a published DID for JWT And Issuer has published STUDENT_SCHEMA schema - And Holder has an unpublished DID + And Holder has an unpublished DID for JWT Scenario: Issuing credential with published PRISM DID When Issuer offers a credential to Holder with "short" form DID diff --git a/tests/integration-tests/src/test/resources/features/credentials/issue_unpublished_did.feature b/tests/integration-tests/src/test/resources/features/credentials/issue_jwt_with_unpublished_did.feature similarity index 73% rename from tests/integration-tests/src/test/resources/features/credentials/issue_unpublished_did.feature rename to tests/integration-tests/src/test/resources/features/credentials/issue_jwt_with_unpublished_did.feature index e9ac6e44f0..a658a0c453 100644 --- a/tests/integration-tests/src/test/resources/features/credentials/issue_unpublished_did.feature +++ b/tests/integration-tests/src/test/resources/features/credentials/issue_jwt_with_unpublished_did.feature @@ -1,10 +1,10 @@ @RFC0453 @AIP20 @credentials -Feature: Issue Credentials Protocol with unpublished DID +Feature: Issue JWT Credentials with unpublished DID Background: Given Issuer and Holder have an existing connection - And Issuer has an unpublished DID - And Holder has an unpublished DID + And Issuer has an unpublished DID for JWT + And Holder has an unpublished DID for JWT Scenario: Issuing credential with unpublished PRISM DID And Issuer offers a credential to Holder with "long" form DID diff --git a/tests/integration-tests/src/test/resources/features/did/create_did.feature b/tests/integration-tests/src/test/resources/features/did/create_did.feature index 3879305102..55ebe81a21 100644 --- a/tests/integration-tests/src/test/resources/features/did/create_did.feature +++ b/tests/integration-tests/src/test/resources/features/did/create_did.feature @@ -1,28 +1,28 @@ @DLT @did @create Feature: Create and publish DID -Scenario Outline: Create PRISM DID - When Issuer creates PRISM DID with key having purpose - Then He sees PRISM DID was created successfully - And He sees PRISM DID data was stored correctly with and -Examples: - | curve | purpose | - | secp256k1 | authentication | - | secp256k1 | assertionMethod | - | Ed25519 | authentication | - | Ed25519 | assertionMethod | - | X25519 | keyAgreement | + Scenario Outline: Create PRISM DID + When Issuer creates PRISM DID with key having purpose + Then He sees PRISM DID was created successfully + And He sees PRISM DID data was stored correctly with and + Examples: + | curve | purpose | + | secp256k1 | authentication | + | secp256k1 | assertionMethod | + | Ed25519 | authentication | + | Ed25519 | assertionMethod | + | X25519 | keyAgreement | -Scenario Outline: Create PRISM DID with disallowed key purpose - When Issuer creates PRISM DID with key having purpose - Then He sees PRISM DID was not successfully created - Examples: - | curve | purpose | - | Ed25519 | keyAgreement | - | X25519 | authentication | - | X25519 | assertionMethod | + Scenario Outline: Create PRISM DID with disallowed key purpose + When Issuer creates PRISM DID with key having purpose + Then He sees PRISM DID was not successfully created + Examples: + | curve | purpose | + | Ed25519 | keyAgreement | + | X25519 | authentication | + | X25519 | assertionMethod | -Scenario: Successfully publish DID to ledger - When Issuer creates unpublished DID - And He publishes DID to ledger - Then He resolves DID document corresponds to W3C standard + Scenario: Successfully publish DID to ledger + Given Issuer creates unpublished DID + When He publishes DID to ledger + Then He resolves DID document corresponds to W3C standard diff --git a/tests/integration-tests/src/test/resources/features/did/update_did.feature b/tests/integration-tests/src/test/resources/features/did/update_did.feature index 67760dc1b6..9aeb2e122f 100644 --- a/tests/integration-tests/src/test/resources/features/did/update_did.feature +++ b/tests/integration-tests/src/test/resources/features/did/update_did.feature @@ -2,27 +2,27 @@ Feature: Update DID Background: Published DID is created - Given Issuer has a published DID + Given Issuer has a published DID for JWT - Scenario: Update PRISM DID by adding new services + Scenario: Update PRISM DID services When Issuer updates PRISM DID with new services - And He submits PRISM DID update operation - Then He sees PRISM DID was successfully updated with new services + Then He sees the PRISM DID should have been updated successfully + And He sees that PRISM DID should have the new service - Scenario: Update PRISM DID by removing services - When Issuer updates PRISM DID by removing services - And He submits PRISM DID update operation - Then He sees PRISM DID was successfully updated by removing services - - Scenario: Update PRISM DID by updating services When Issuer updates PRISM DID by updating services - And He submits PRISM DID update operation - Then He sees PRISM DID was successfully updated by updating services + Then He sees the PRISM DID should have been updated successfully + And He sees the PRISM DID should have the service updated - Scenario Outline: Update PRISM DID by adding new keys + When Issuer updates PRISM DID by removing services + Then He sees the PRISM DID should have been updated successfully + And He sees the PRISM DID should have the service removed + + Scenario Outline: Update PRISM DID keys When Issuer updates PRISM DID by adding new key with curve and purpose - And He submits PRISM DID update operation Then He sees PRISM DID was successfully updated with new keys of purpose + + When Issuer updates PRISM DID by removing keys + Then He sees PRISM DID was successfully updated and keys removed with purpose Examples: | curve | purpose | | secp256k1 | authentication | @@ -31,7 +31,11 @@ Feature: Update DID | Ed25519 | assertionMethod | | X25519 | keyAgreement | - Scenario: Update PRISM DID by removing keys - When Issuer updates PRISM DID by removing keys - And He submits PRISM DID update operation - Then He sees PRISM DID was successfully updated and keys removed + Scenario Outline: Update PRISM DID with disallowed key purpose + When Issuer updates PRISM DID by adding new key with curve and purpose + Then He sees the PRISM DID was not successfully updated + Examples: + | curve | purpose | + | Ed25519 | keyAgreement | + | X25519 | authentication | + | X25519 | assertionMethod | diff --git a/tests/integration-tests/src/test/resources/features/proofs/present_proof.feature b/tests/integration-tests/src/test/resources/features/proofs/present_proof.feature index f19b73090d..c2fa51b80d 100644 --- a/tests/integration-tests/src/test/resources/features/proofs/present_proof.feature +++ b/tests/integration-tests/src/test/resources/features/proofs/present_proof.feature @@ -3,24 +3,25 @@ Feature: Present Proof Protocol Scenario: Holder presents credential proof to verifier Given Verifier and Holder have an existing connection - And Holder has an issued credential from Issuer + And Holder has a jwt issued credential from Issuer When Verifier sends a request for proof presentation to Holder - And Holder receives the request - And Holder makes the presentation of the proof to Verifier + And Holder receives the presentation proof request + And Holder makes the presentation of the proof Then Verifier has the proof verified + Scenario: Holder presents proof to verifier which is the issuer itself + Given Issuer and Holder have an existing connection + And Holder has a jwt issued credential from Issuer + When Issuer sends a request for proof presentation to Holder + And Holder receives the presentation proof request + And Holder makes the presentation of the proof + Then Issuer has the proof verified + Scenario: Verifier rejects holder proof Given Verifier and Holder have an existing connection - And Holder has an issued credential from Issuer + And Holder has a jwt issued credential from Issuer When Verifier sends a request for proof presentation to Holder - And Holder receives the request + And Holder receives the presentation proof request And Holder rejects the proof Then Holder sees the proof is rejected - Scenario: Holder presents proof to verifier which is the issuer itself - Given Issuer and Holder have an existing connection - And Holder has an issued credential from Issuer - When Issuer sends a request for proof presentation to Holder - And Holder receives the request - And Holder makes the presentation of the proof to Issuer - Then Issuer has the proof verified diff --git a/tests/integration-tests/src/test/resources/features/proofs/present_proof_anoncred.feature b/tests/integration-tests/src/test/resources/features/proofs/present_proof_anoncred.feature index 102140b1fa..77c52dc572 100644 --- a/tests/integration-tests/src/test/resources/features/proofs/present_proof_anoncred.feature +++ b/tests/integration-tests/src/test/resources/features/proofs/present_proof_anoncred.feature @@ -4,8 +4,8 @@ Feature: Present Proof Protocol Scenario: Holder presents anoncreds credential proof to verifier Given Issuer and Holder have an existing connection And Verifier and Holder have an existing connection - And Issuer has a published DID - And Holder has an unpublished DID + And Issuer has a published DID for ANONCRED + And Holder has an unpublished DID for ANONCRED And Issuer has an anoncred schema definition And Issuer offers anoncred to Holder And Holder receives the credential offer diff --git a/tests/integration-tests/src/test/resources/features/revocation/revoke_jwt_credential.feature b/tests/integration-tests/src/test/resources/features/revocation/revoke_jwt_credential.feature index 2ac02f447b..b60e7d7bf9 100644 --- a/tests/integration-tests/src/test/resources/features/revocation/revoke_jwt_credential.feature +++ b/tests/integration-tests/src/test/resources/features/revocation/revoke_jwt_credential.feature @@ -2,20 +2,20 @@ Feature: Credential revocation - JWT Background: - Given Holder has an issued credential from Issuer + Given Holder has a jwt issued credential from Issuer Scenario: Revoke issued credential When Issuer revokes the credential issued to Holder Then Issuer should see the credential was revoked When Issuer sends a request for proof presentation to Holder - And Holder receives the request - And Holder makes the presentation of the proof to Issuer + And Holder receives the presentation proof request + And Holder makes the presentation of the proof Then Issuer sees the proof returned verification failed Scenario: Holder tries to revoke credential from issuer When Holder tries to revoke credential from Issuer And Issuer sends a request for proof presentation to Holder - And Holder receives the request - And Holder makes the presentation of the proof to Issuer + And Holder receives the presentation proof request + And Holder makes the presentation of the proof Then Issuer has the proof verified And Issuer should see the credential is not revoked diff --git a/tests/integration-tests/src/test/resources/features/schemas/credential_schemas.feature b/tests/integration-tests/src/test/resources/features/schemas/credential_schemas.feature index fb65e1682d..fdbbbc911f 100644 --- a/tests/integration-tests/src/test/resources/features/schemas/credential_schemas.feature +++ b/tests/integration-tests/src/test/resources/features/schemas/credential_schemas.feature @@ -2,7 +2,7 @@ Feature: Credential schemas Background: - When Issuer creates unpublished DID + Given Issuer creates unpublished DID Scenario: Successful schema creation When Issuer creates a new credential STUDENT_SCHEMA schema diff --git a/tests/integration-tests/src/test/resources/features/verification/vc_verification.feature b/tests/integration-tests/src/test/resources/features/verification/vc_verification.feature deleted file mode 100644 index c09163f1b8..0000000000 --- a/tests/integration-tests/src/test/resources/features/verification/vc_verification.feature +++ /dev/null @@ -1,6 +0,0 @@ -@verification @api -Feature: Vc Verification schemas - -Scenario: Successful Verifies VcVerificationRequest - Given Issuer and Holder have an existing connection - When Issuer verifies VcVerificationRequest diff --git a/tests/integration-tests/src/test/resources/features/verificationapi/vc_verification.feature b/tests/integration-tests/src/test/resources/features/verificationapi/vc_verification.feature new file mode 100644 index 0000000000..3db147f66d --- /dev/null +++ b/tests/integration-tests/src/test/resources/features/verificationapi/vc_verification.feature @@ -0,0 +1,54 @@ +@verification @api +Feature: Vc Verification schemas + + Scenario: Receive a jwt vc from cloud-agent and verify it + Given Holder has a jwt issued credential with STUDENT_SCHEMA schema from Issuer + And Holder uses that JWT VC issued from Issuer for Verification API + And Holder sends the JWT Credential to Issuer Verification API + | ALGORITHM_VERIFICATION | true | + | EXPIRATION_CHECK | true | + | ISSUER_IDENTIFICATION | true | + | NOT_BEFORE_CHECK | true | + | SCHEMA_CHECK | true | + | SIGNATURE_VERIFICATION | true | + | SEMANTIC_CHECK_OF_CLAIMS | true | + Then Holder should see that all checks have passed + + Scenario: Expected checks for generated JWT VC + Given Holder has a JWT VC for Verification API + When Holder sends the JWT Credential to Issuer Verification API + | ALGORITHM_VERIFICATION | true | + | AUDIENCE_CHECK | true | + | EXPIRATION_CHECK | true | + | ISSUER_IDENTIFICATION | true | + | NOT_BEFORE_CHECK | true | + | SIGNATURE_VERIFICATION | false | + | SEMANTIC_CHECK_OF_CLAIMS | true | + Then Holder should see that all checks have passed + + Scenario Outline: Expected failures + Given Holder has a problem in the Verifiable Credential + When Holder sends the JWT Credential to Issuer Verification API + | | false | + Then Holder should see that verification has failed with problem + Examples: + | problem | + | ALGORITHM_VERIFICATION | + | AUDIENCE_CHECK | + | EXPIRATION_CHECK | + | ISSUER_IDENTIFICATION | + | NOT_BEFORE_CHECK | + | SIGNATURE_VERIFICATION | + | SEMANTIC_CHECK_OF_CLAIMS | + + Scenario Outline: Unsupported verification check should fail + Given Holder has a JWT VC for Verification API + When Holder sends the JWT Credential to Issuer Verification API + | | false | + Then Holder should see the check has failed + Examples: + | verification | + | COMPLIANCE_WITH_STANDARDS | + | INTEGRITY_OF_CLAIMS | + | REVOCATION_CHECK | + | SUBJECT_VERIFICATION |