From 2fb740394a32ee8bbcdd9618e51a8292688af9f6 Mon Sep 17 00:00:00 2001 From: patlo-iog Date: Wed, 22 May 2024 17:16:51 +0700 Subject: [PATCH 1/4] docs: update new key types in DID tutorials [skip ci] (#1085) Signed-off-by: Pat Losoponkul --- .github/workflows/integration-tests.yml | 1 + .github/workflows/performance-tests.yml | 1 + .github/workflows/unit-tests.yml | 1 + docs/docusaurus/dids/create.md | 8 +++++--- docs/docusaurus/dids/update.md | 3 ++- 5 files changed, 10 insertions(+), 4 deletions(-) diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index 3a8202ed48..2467780df2 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -26,6 +26,7 @@ jobs: run-integration-tests: name: "Run integration tests" runs-on: ubuntu-latest + if: ${{ !contains(github.event.pull_request.title, '[skip ci]') }} env: REPORTS_DIR: "tests/integration-tests/target/site/serenity" steps: diff --git a/.github/workflows/performance-tests.yml b/.github/workflows/performance-tests.yml index 6a85eaaf81..c487e92863 100644 --- a/.github/workflows/performance-tests.yml +++ b/.github/workflows/performance-tests.yml @@ -21,6 +21,7 @@ jobs: run-e2e-tests: name: "Run performance tests" runs-on: ubuntu-latest + if: ${{ !contains(github.event.pull_request.title, '[skip ci]') }} steps: - name: Checkout uses: actions/checkout@v4 diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml index 065e5a5fcd..5a45f81988 100644 --- a/.github/workflows/unit-tests.yml +++ b/.github/workflows/unit-tests.yml @@ -16,6 +16,7 @@ jobs: build-and-unit-tests: name: "Build and unit tests" runs-on: self-hosted + if: ${{ !contains(github.event.pull_request.title, '[skip ci]') }} env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} container: diff --git a/docs/docusaurus/dids/create.md b/docs/docusaurus/dids/create.md index 2496a3a13e..42868604c7 100644 --- a/docs/docusaurus/dids/create.md +++ b/docs/docusaurus/dids/create.md @@ -54,8 +54,9 @@ The result should show an empty list, as no DIDs exist on this Cloud Agent insta ### 2. Create the Cloud Agent managed DID using DID registrar endpoint The DID controller can create a new DID by sending a [DID document](/docs/concepts/glossary#did-document) template to the Agent. -Since key pairs are generated and managed by the Cloud Agent, DID controller only has to specify the key `id` and its purpose (e.g., `authentication`, `assertionMethod`, etc.). -The current PRISM DID method supports a key with a single purpose, but it is extendible to support a key with multiple purposes in the future. +Since key pairs are generated and managed by the Cloud Agent, DID controller only has to specify the key `id`, +`purpose` (`authentication`, `assertionMethod`, etc.), and optional `curve` (`secp256k1`, `Ed25519`, `X25519`). +If the `curve` is omitted, the agent uses the `secp256k1` curve by default. ```bash curl --location --request POST 'http://localhost:8080/cloud-agent/did-registrar/dids' \ @@ -67,7 +68,8 @@ curl --location --request POST 'http://localhost:8080/cloud-agent/did-registrar/ "publicKeys": [ { "id": "auth-1", - "purpose": "authentication" + "purpose": "authentication", + "curve": "secp256k1" } ], "services": [] diff --git a/docs/docusaurus/dids/update.md b/docs/docusaurus/dids/update.md index b3e5e6760f..4a91c93107 100644 --- a/docs/docusaurus/dids/update.md +++ b/docs/docusaurus/dids/update.md @@ -120,7 +120,8 @@ curl --location --request POST 'http://localhost:8080/cloud-agent/did-registrar/ "actionType": "ADD_KEY", "addKey": { "id": "key-2", - "purpose": "authentication" + "purpose": "authentication", + "curve": "secp256k1" } } ] From fec905d9e8687ae3c5ee11842d14240892a1b956 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 22 May 2024 19:12:22 +0700 Subject: [PATCH 2/4] build: DAL dependency update (#1076) Signed-off-by: Hyperledger Bot Signed-off-by: Pat Losoponkul Co-authored-by: Hyperledger Bot Co-authored-by: patlo-iog --- build.sbt | 6 +++--- .../identus/pollux/anoncreds/PoCNewLib.scala | 7 ------- .../identus/shared/db/TransactorLayer.scala | 14 +++----------- 3 files changed, 6 insertions(+), 21 deletions(-) diff --git a/build.sbt b/build.sbt index ca3757024b..d46bd70175 100644 --- a/build.sbt +++ b/build.sbt @@ -72,10 +72,10 @@ lazy val V = new { val testContainersScala = "0.41.3" val testContainersJavaKeycloak = "3.2.0" // scala-steward:off - val doobie = "1.0.0-RC2" - val quill = "4.7.3" + val doobie = "1.0.0-RC5" + val quill = "4.8.4" val flyway = "9.22.3" - val postgresDriver = "42.2.29" + val postgresDriver = "42.7.3" val logback = "1.4.14" val slf4j = "2.0.13" diff --git a/pollux/anoncredsTest/src/test/scala/org/hyperledger/identus/pollux/anoncreds/PoCNewLib.scala b/pollux/anoncredsTest/src/test/scala/org/hyperledger/identus/pollux/anoncreds/PoCNewLib.scala index 6c74014750..b5825d3be6 100644 --- a/pollux/anoncredsTest/src/test/scala/org/hyperledger/identus/pollux/anoncreds/PoCNewLib.scala +++ b/pollux/anoncredsTest/src/test/scala/org/hyperledger/identus/pollux/anoncreds/PoCNewLib.scala @@ -3,13 +3,6 @@ package org.hyperledger.identus.pollux.anoncreds import org.scalatest.flatspec.AnyFlatSpec import scala.jdk.CollectionConverters.* -import org.hyperledger.identus.pollux.anoncreds.{ - AnoncredLinkSecretWithId, - AnoncredLinkSecret, - AnoncredPresentationRequest, - AnoncredLib, - AnoncredCredentialRequests -} /** polluxAnoncredsTest/Test/testOnly org.hyperledger.identus.pollux.anoncreds.PoCNewLib */ diff --git a/shared/core/src/main/scala/org/hyperledger/identus/shared/db/TransactorLayer.scala b/shared/core/src/main/scala/org/hyperledger/identus/shared/db/TransactorLayer.scala index 5bedd409c0..f4fa1e8380 100644 --- a/shared/core/src/main/scala/org/hyperledger/identus/shared/db/TransactorLayer.scala +++ b/shared/core/src/main/scala/org/hyperledger/identus/shared/db/TransactorLayer.scala @@ -5,7 +5,6 @@ import cats.effect.kernel.Resource import cats.effect.std.Dispatcher import com.zaxxer.hikari.HikariConfig import doobie.hikari.HikariTransactor -import doobie.util.ExecutionContexts import doobie.util.transactor.Transactor import zio.* import zio.interop.catz.* @@ -18,7 +17,7 @@ object TransactorLayer { // Here we use `Dispatcher.apply` // but at the agent level it is `Dispatcher.parallel` due to evicted version // Dispatcher.parallel[Task].allocated.map { case (dispatcher, _) => - Dispatcher[Task].allocated.map { case (dispatcher, _) => + Dispatcher.parallel[Task].allocated.map { case (dispatcher, _) => given Dispatcher[Task] = dispatcher TransactorLayer.hikari[Task](config) } @@ -34,7 +33,7 @@ object TransactorLayer { // Here we use `Dispatcher.apply` // but at the agent level it is `Dispatcher.parallel` due to evicted version // Dispatcher.parallel[ContextAwareTask].allocated.map { case (dispatcher, _) => - Dispatcher[ContextAwareTask].allocated.map { case (dispatcher, _) => + Dispatcher.parallel[ContextAwareTask].allocated.map { case (dispatcher, _) => given Dispatcher[ContextAwareTask] = dispatcher TransactorLayer.hikari[ContextAwareTask](config) } @@ -56,14 +55,7 @@ object TransactorLayer { hikariConfig } .map { hikariConfig => - val pool: Resource[A, Transactor[A]] = for { - // Resource yielding a transactor configured with a bounded connect EC and an unbounded - // transaction EC. Everything will be closed and shut down cleanly after use. - ec <- ExecutionContexts.fixedThreadPool[A](config.awaitConnectionThreads) // our connect EC - xa <- HikariTransactor.fromHikariConfig[A](hikariConfig, ec) - // xa <- HikariTransactor.fromHikariConfig[A](hikariConfig, Some(LogHandler.jdkLogHandler)) - } yield xa - + val pool: Resource[A, Transactor[A]] = HikariTransactor.fromHikariConfig[A](hikariConfig) pool.toManaged.toLayer[Transactor[A]] } From efc992d14933c2c9232cad0b037024d052946e66 Mon Sep 17 00:00:00 2001 From: Ezequiel Postan Date: Wed, 22 May 2024 16:46:16 -0300 Subject: [PATCH 3/4] docs: Add ADR for resources storage (#1087) Signed-off-by: EzequielPostan --- ...520-use-did-urls-to-reference-resources.md | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 docs/decisions/20240520-use-did-urls-to-reference-resources.md diff --git a/docs/decisions/20240520-use-did-urls-to-reference-resources.md b/docs/decisions/20240520-use-did-urls-to-reference-resources.md new file mode 100644 index 0000000000..62fb3d61d2 --- /dev/null +++ b/docs/decisions/20240520-use-did-urls-to-reference-resources.md @@ -0,0 +1,62 @@ +# Storage for SSI related resources + +- Status: accepted +- Deciders: Javi, Ben, Yurii +- Date: 2024-05-20 +- Tags: Verifiable Data Registry (VDR), decentralized storage + + +## Context and Problem Statement + +The main question to answer is: What is the most practical way to store resources related to VC verification and revocation? + +In the context of SSI, there are resources such as credential definitions, schemas, revocation lists, etc., that are referenced inside VCs. These resources need to be accessible to different parties in order to verifiy the credentials. In this ADR, we discuss the trade-offs of different storage alternatives. + +## Decision Drivers + +A desired solution should balance + +- Methods for data integrity and authenticity validation: For instance, if we are referring to a credential definition, the user retrieving the resource should be able to validate that the resource hasn't been altered since its creation. In the case of more dynamic resources, such as revocation lists, which are actually updated throughout time, the recipient party would need to validate that the resource was created by the issuer. +- Data availability: It is important for resources to be highly available. If a resource is missing, it can lead to an inability to validate a VC. +- Decentralization: There must be a consideration to avoid innecesary central points of failure +- Historical data requests: Some use cases may require support for querying historical data. For example, retreive a revocation list at certain point in the past. +- Write access control: Most generally issuers (as they create most of the resources), require to have control of the data they store in order to be able to update it when needed, and also to avoid third parties to make un-authorized changes. +- Latency, throughput, deployment costs: Any solution should provide a reasonable balance of non functional requirements, such as achieving enough throughput, or having low enough latency for the system to be practical. + +## Considered Options + +We considered the following alternatives, which contemplate the approaches currently discussed by the broad SSI ecosystem at the time of this writing. + +- URLs and traditional HTTP servers: with no surprises, in this approach, each resource is identified with a URL and stored in traditional servers. The URLs will encode hashes as query parameters to enforce integrity for static resources. Dynamic resources will be signed by the resource creator's key. +- DID URLs and traditional HTTP servers: in this variation, resources are still stored in servers. Resources are identified by DID URLs that dereference services of the associated DID document. The services will contain the final URL to retrieve the corresponding resources. Once again, hashes will be associated to static resources as DID URL query parameters, while dynamic resources will be signed adequately. +- IPFS: An IPFS approach would be useful for storing static resources using IPFS identifiers for them. Dynamic resources however become more challenges. Even though we recognize the existence of constructions like IPNS or other layers to manage dynamic resources, we find them less secure in terms of availability and consistency guarantees. +- Ledger based storage (Cardano in particular): In this approach, resources would be stored in transactions' metadata posted on-chain. The data availability and integrity can be inherited from the underlying ledger. +- A combination of previous methods and the use of a ledger: Similar as above, data references are posted on-chain, but the actual resources are stores in servers. The servers could be traditional HTTP servers or IPFS nodes. + +## Decision Outcome + +After a careful analysis we concluded the following points: +- There is an architectural need to develop a "proxy" component, a.k.a. VDR proxy, that would work as a first phase for resource resolution. Behind the VDR proxy, different storage implementations could be added as extensions +- With respect to specific implementations + + ledger based storage at this stage introduces latency, throughput bottlenecks, costs and other issues not suitable for most use cases. + + Hybrid solutions that make use of a ledger share similar drawbacks. + + Decentralized Hash Tables (such as IPFS) do not provide efficient handling for dynamic resources (such as revocation lists). + + We concluded that a reasonable first iteration could be delivered using DID URLs to identify resources while they would be, a priori, stored in traditional HTTP servers. + +### Positive Consequences + +- The implementation of a VDR proxy enables a transparent abstraction that allows to extend the possible methods to retrieve resources. +- DID URLs allow for a fair decentralization level at issuer's disposal to control the location of resources + +### Negative Consequences + +- There is a level of under-specification at W3C specifications with respect to DID URL dereferencing. This forces us to define the under-specificied behaviour or simply creata-our-own solution. + +## Links + +We leave a list of useful links for context + +- [AnonCreds Methods Registry](https://hyperledger.github.io/anoncreds-methods-registry/) +- [AnonCreds Specification](https://hyperledger.github.io/anoncreds-spec/) +- [W3C DID resolution algorithm](https://w3c-ccg.github.io/did-resolution/) + From 21f5f4f294d95912b75d90355309873823466aba Mon Sep 17 00:00:00 2001 From: Allain Magyar Date: Thu, 23 May 2024 00:38:17 -0300 Subject: [PATCH 4/4] test: add jwt revocation test scenario (#951) Signed-off-by: Allain Magyar Signed-off-by: Hyperledger Bot Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Hyperledger Bot --- tests/integration-tests/README.md | 4 +- .../test/kotlin/abilities/ListenToEvents.kt | 6 +- .../src/test/kotlin/common/Utils.kt | 28 ---- .../src/test/kotlin/models/Events.kt | 65 +++++++- .../src/test/kotlin/models/JwtCredential.kt | 20 +++ .../test/kotlin/steps/common/CommonSteps.kt | 35 ++--- .../steps/connection/ConnectionSteps.kt | 47 +++--- .../credentials/IssueCredentialsSteps.kt | 146 +++++------------ .../credentials/RevokeCredentialSteps.kt | 78 ++++++++++ .../kotlin/steps/did/DeactivateDidSteps.kt | 22 ++- .../test/kotlin/steps/did/PublishDidSteps.kt | 72 +++------ .../test/kotlin/steps/did/UpdateDidSteps.kt | 147 ++++++++---------- .../proofs/AnoncredsPresentProofSteps.kt | 31 ++-- .../kotlin/steps/proofs/PresentProofSteps.kt | 86 +++++----- .../schemas/AnoncredCredentialSchemaSteps.kt | 85 ++++++++++ .../credentials/issue_published_did.feature | 5 +- .../features/did/deactivate_did.feature | 3 +- .../resources/features/did/update_did.feature | 2 +- .../features/proofs/present_proof.feature | 1 + .../proofs/present_proof_anoncred.feature | 6 +- .../revocation/revoke_jwt_credential.feature | 21 +++ 21 files changed, 504 insertions(+), 406 deletions(-) delete mode 100644 tests/integration-tests/src/test/kotlin/common/Utils.kt create mode 100644 tests/integration-tests/src/test/kotlin/models/JwtCredential.kt create mode 100644 tests/integration-tests/src/test/kotlin/steps/credentials/RevokeCredentialSteps.kt create mode 100644 tests/integration-tests/src/test/kotlin/steps/schemas/AnoncredCredentialSchemaSteps.kt create mode 100644 tests/integration-tests/src/test/resources/features/revocation/revoke_jwt_credential.feature diff --git a/tests/integration-tests/README.md b/tests/integration-tests/README.md index 2725a970bc..91d175e678 100644 --- a/tests/integration-tests/README.md +++ b/tests/integration-tests/README.md @@ -46,9 +46,9 @@ The project structure is represented below: │ ├── abilities -> contains the abilities of the actors │ ├── common -> contains the common classes (test constants and helper functions) │ ├── config -> contains the configuration classes (Hoplite) - │ ├── features -> contains the features implementation steps │ ├── interactions -> contains the interactions of the actors - │ └── runners -> contains the test runners to execute the tests + │ ├── models -> contains the models + │ ├── steps -> contains the features implementation steps └── resources -> contains the test resources ├── configs -> contains the test configuration files ├── containers -> contains the Docker Compose files to start the test environment diff --git a/tests/integration-tests/src/test/kotlin/abilities/ListenToEvents.kt b/tests/integration-tests/src/test/kotlin/abilities/ListenToEvents.kt index 2ea3b69724..43c92230ed 100644 --- a/tests/integration-tests/src/test/kotlin/abilities/ListenToEvents.kt +++ b/tests/integration-tests/src/test/kotlin/abilities/ListenToEvents.kt @@ -39,9 +39,7 @@ open class ListenToEvents( TestConstants.EVENT_TYPE_CONNECTION_UPDATED -> connectionEvents.add(gson.fromJson(eventString, ConnectionEvent::class.java)) TestConstants.EVENT_TYPE_ISSUE_CREDENTIAL_RECORD_UPDATED -> credentialEvents.add(gson.fromJson(eventString, CredentialEvent::class.java)) TestConstants.EVENT_TYPE_PRESENTATION_UPDATED -> presentationEvents.add(gson.fromJson(eventString, PresentationEvent::class.java)) - TestConstants.EVENT_TYPE_DID_STATUS_UPDATED -> { - didEvents.add(gson.fromJson(eventString, DidEvent::class.java)) - } + TestConstants.EVENT_TYPE_DID_STATUS_UPDATED -> didEvents.add(gson.fromJson(eventString, DidEvent::class.java)) else -> { throw IllegalArgumentException("ERROR: unknown event type ${event.type}") } @@ -56,7 +54,7 @@ open class ListenToEvents( return ListenToEvents(url, webhookPort) } - fun `as`(actor: Actor): ListenToEvents { + fun with(actor: Actor): ListenToEvents { return actor.abilityTo(ListenToEvents::class.java) } } diff --git a/tests/integration-tests/src/test/kotlin/common/Utils.kt b/tests/integration-tests/src/test/kotlin/common/Utils.kt deleted file mode 100644 index 627dab4357..0000000000 --- a/tests/integration-tests/src/test/kotlin/common/Utils.kt +++ /dev/null @@ -1,28 +0,0 @@ -package common - -import org.awaitility.Awaitility -import org.awaitility.core.ConditionTimeoutException -import org.awaitility.pollinterval.FixedPollInterval -import java.time.Duration - -object Utils { - fun wait( - blockToWait: () -> Boolean, - errorMessage: String, - poolInterval: FixedPollInterval = FixedPollInterval(Duration.ofMillis(500L)), - timeout: Duration = Duration.ofSeconds(120L), - ) { - try { - Awaitility.await().with().pollInterval(poolInterval) - .pollInSameThread() - .atMost(timeout) - .until { - blockToWait() - } - } catch (err: ConditionTimeoutException) { - throw ConditionTimeoutException( - errorMessage, - ) - } - } -} diff --git a/tests/integration-tests/src/test/kotlin/models/Events.kt b/tests/integration-tests/src/test/kotlin/models/Events.kt index dcf27c3609..f4d1d76475 100644 --- a/tests/integration-tests/src/test/kotlin/models/Events.kt +++ b/tests/integration-tests/src/test/kotlin/models/Events.kt @@ -2,10 +2,7 @@ package models import com.google.gson.JsonElement import com.google.gson.annotations.SerializedName -import org.hyperledger.identus.client.models.Connection -import org.hyperledger.identus.client.models.IssueCredentialRecord -import org.hyperledger.identus.client.models.ManagedDID -import org.hyperledger.identus.client.models.PresentationStatus +import org.hyperledger.identus.client.models.* data class Event( @SerializedName("type") var type: String, @@ -35,10 +32,68 @@ data class PresentationEvent( @SerializedName("type") var type: String, @SerializedName("id") var id: String, @SerializedName("ts") var ts: String, - @SerializedName("data") var data: PresentationStatus, + @SerializedName("data") var data: PresentationStatusAdapter, // FIXME: rollback to PresentationStatus when Status is fixed @SerializedName("walletId") var walletId: String, ) +data class PresentationStatusAdapter( // FIXME: delete this class when PresentationStatus.Status is fixed + @SerializedName("presentationId") val presentationId: String, + @SerializedName("thid") val thid: String, + @SerializedName("role") val role: PresentationStatus.Role, + @SerializedName("status") val status: Status, + @SerializedName("metaRetries") val metaRetries: Int, + @SerializedName("proofs") val proofs: List? = null, + @SerializedName("data") val `data`: List? = null, + @SerializedName("connectionId") val connectionId: String? = null, +) { + enum class Status(val value: String) { + @SerializedName(value = "RequestPending") + REQUEST_PENDING("RequestPending"), + + @SerializedName(value = "RequestSent") + REQUEST_SENT("RequestSent"), + + @SerializedName(value = "RequestReceived") + REQUEST_RECEIVED("RequestReceived"), + + @SerializedName(value = "RequestRejected") + REQUEST_REJECTED("RequestRejected"), + + @SerializedName(value = "PresentationPending") + PRESENTATION_PENDING("PresentationPending"), + + @SerializedName(value = "PresentationGenerated") + PRESENTATION_GENERATED("PresentationGenerated"), + + @SerializedName(value = "PresentationSent") + PRESENTATION_SENT("PresentationSent"), + + @SerializedName(value = "PresentationReceived") + PRESENTATION_RECEIVED("PresentationReceived"), + + @SerializedName(value = "PresentationVerified") + PRESENTATION_VERIFIED("PresentationVerified"), + + @SerializedName(value = "PresentationAccepted") + PRESENTATION_ACCEPTED("PresentationAccepted"), + + @SerializedName(value = "PresentationRejected") + PRESENTATION_REJECTED("PresentationRejected"), + + @SerializedName(value = "ProblemReportPending") + PROBLEM_REPORT_PENDING("ProblemReportPending"), + + @SerializedName(value = "ProblemReportSent") + PROBLEM_REPORT_SENT("ProblemReportSent"), + + @SerializedName(value = "ProblemReportReceived") + PROBLEM_REPORT_RECEIVED("ProblemReportReceived"), + + @SerializedName(value = "PresentationVerificationFailed") + PRESENTATION_VERIFICATION_FAILED("PresentationVerificationFailed"), + } +} + data class DidEvent( @SerializedName("type") var type: String, @SerializedName("id") var id: String, diff --git a/tests/integration-tests/src/test/kotlin/models/JwtCredential.kt b/tests/integration-tests/src/test/kotlin/models/JwtCredential.kt new file mode 100644 index 0000000000..741f3f4c5f --- /dev/null +++ b/tests/integration-tests/src/test/kotlin/models/JwtCredential.kt @@ -0,0 +1,20 @@ +package models + +import com.jayway.jsonpath.DocumentContext +import com.jayway.jsonpath.JsonPath +import java.util.Base64 + +class JwtCredential(base64: String) { + private val payload: DocumentContext + + init { + val jwt = String(Base64.getDecoder().decode(base64)) + val parts = jwt.split(".") + payload = JsonPath.parse(String(Base64.getUrlDecoder().decode(parts[1]))) + } + + fun statusListId(): String { + val listUrl = payload.read("$.vc.credentialStatus.statusListCredential") + return listUrl.split("/credential-status/")[1] + } +} 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 1158745211..e9883293c9 100644 --- a/tests/integration-tests/src/test/kotlin/steps/common/CommonSteps.kt +++ b/tests/integration-tests/src/test/kotlin/steps/common/CommonSteps.kt @@ -33,31 +33,18 @@ class CommonSteps { @Given("{actor} has an issued credential from {actor}") fun holderHasIssuedCredentialFromIssuer(holder: Actor, issuer: Actor) { - holder.attemptsTo( - Get.resource("/issue-credentials/records"), - ) - holder.attemptsTo( - Ensure.thatTheLastResponse().statusCode().isEqualTo(HttpStatus.SC_OK), - ) - val receivedCredential = SerenityRest.lastResponse().get().contents!!.findLast { credential -> - credential.protocolState == IssueCredentialRecord.ProtocolState.CREDENTIAL_RECEIVED && - credential.credentialFormat == IssueCredentialRecord.CredentialFormat.JWT - } + actorsHaveExistingConnection(issuer, holder) - if (receivedCredential != null) { - holder.remember("issuedCredential", receivedCredential) - } else { - val publishDidSteps = PublishDidSteps() - val issueSteps = IssueCredentialsSteps() - actorsHaveExistingConnection(issuer, holder) - publishDidSteps.agentHasAnUnpublishedDID(holder) - publishDidSteps.agentHasAPublishedDID(issuer) - issueSteps.issuerOffersACredential(issuer, holder, "short") - issueSteps.holderReceivesCredentialOffer(holder) - issueSteps.holderAcceptsCredentialOfferForJwt(holder) - issueSteps.acmeIssuesTheCredential(issuer) - issueSteps.bobHasTheCredentialIssued(holder) - } + val publishDidSteps = PublishDidSteps() + publishDidSteps.createsUnpublishedDid(holder) + publishDidSteps.agentHasAPublishedDID(issuer) + + val issueSteps = IssueCredentialsSteps() + issueSteps.issuerOffersACredential(issuer, holder, "short") + issueSteps.holderReceivesCredentialOffer(holder) + issueSteps.holderAcceptsCredentialOfferForJwt(holder) + issueSteps.acmeIssuesTheCredential(issuer) + issueSteps.bobHasTheCredentialIssued(holder) } @Given("{actor} and {actor} have an existing connection") 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 38922a021c..99d0ed603e 100644 --- a/tests/integration-tests/src/test/kotlin/steps/connection/ConnectionSteps.kt +++ b/tests/integration-tests/src/test/kotlin/steps/connection/ConnectionSteps.kt @@ -1,21 +1,19 @@ package steps.connection import abilities.ListenToEvents -import common.Utils.wait import interactions.Get import interactions.Post import io.cucumber.java.en.Then import io.cucumber.java.en.When import io.iohk.atala.automation.extensions.get import io.iohk.atala.automation.serenity.ensure.Ensure +import io.iohk.atala.automation.utils.Wait import net.serenitybdd.rest.SerenityRest import net.serenitybdd.screenplay.Actor import org.apache.http.HttpStatus.SC_CREATED import org.apache.http.HttpStatus.SC_OK import org.assertj.core.api.Assertions.assertThat -import org.hyperledger.identus.client.models.AcceptConnectionInvitationRequest -import org.hyperledger.identus.client.models.Connection -import org.hyperledger.identus.client.models.CreateConnectionRequest +import org.hyperledger.identus.client.models.* class ConnectionSteps { @@ -66,7 +64,8 @@ class ConnectionSteps { Ensure.thatTheLastResponse().statusCode().isEqualTo(SC_OK), Ensure.that(inviteeConnection.invitation.from).isEqualTo(inviterConnection.invitation.from), Ensure.that(inviteeConnection.invitation.id).isEqualTo(inviterConnection.invitation.id), - Ensure.that(inviteeConnection.invitation.invitationUrl).isEqualTo(inviterConnection.invitation.invitationUrl), + Ensure.that(inviteeConnection.invitation.invitationUrl) + .isEqualTo(inviterConnection.invitation.invitationUrl), Ensure.that(inviteeConnection.invitation.type).isEqualTo(inviterConnection.invitation.type), Ensure.that(inviteeConnection.state).isEqualTo(Connection.State.CONNECTION_REQUEST_PENDING), Ensure.that(inviteeConnection.role).isEqualTo(Connection.Role.INVITEE), @@ -77,31 +76,27 @@ class ConnectionSteps { @When("{actor} receives the connection request and sends back the response") fun inviterReceivesTheConnectionRequest(inviter: Actor) { - wait( - { - val lastEvent = ListenToEvents.`as`(inviter).connectionEvents.lastOrNull { - it.data.thid == inviter.recall("connection").thid - } - lastEvent != null && - lastEvent.data.state == Connection.State.CONNECTION_RESPONSE_SENT - }, - "Inviter connection didn't reach ${Connection.State.CONNECTION_RESPONSE_SENT} state", - ) + Wait.until( + errorMessage = "Inviter connection didn't reach ${Connection.State.CONNECTION_RESPONSE_SENT} state", + ) { + val lastEvent = ListenToEvents.with(inviter).connectionEvents.lastOrNull { + it.data.thid == inviter.recall("connection").thid + } + lastEvent != null && lastEvent.data.state == Connection.State.CONNECTION_RESPONSE_SENT + } } @When("{actor} receives the connection response") fun inviteeReceivesTheConnectionResponse(invitee: Actor) { - // Bob (Holder) receives final connection response - wait( - { - val lastEvent = ListenToEvents.`as`(invitee).connectionEvents.lastOrNull { - it.data.thid == invitee.recall("connection").thid - } - lastEvent != null && - lastEvent.data.state == Connection.State.CONNECTION_RESPONSE_RECEIVED - }, - "Invitee connection didn't reach ${Connection.State.CONNECTION_RESPONSE_RECEIVED} state.", - ) + Wait.until( + errorMessage = "Invitee connection didn't reach ${Connection.State.CONNECTION_RESPONSE_RECEIVED} state.", + ) { + val lastEvent = ListenToEvents.with(invitee).connectionEvents.lastOrNull { + it.data.thid == invitee.recall("connection").thid + } + lastEvent != null && + lastEvent.data.state == Connection.State.CONNECTION_RESPONSE_RECEIVED + } } @Then("{actor} and {actor} have a connection") 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 578ce9419d..4c75524e0d 100644 --- a/tests/integration-tests/src/test/kotlin/steps/credentials/IssueCredentialsSteps.kt +++ b/tests/integration-tests/src/test/kotlin/steps/credentials/IssueCredentialsSteps.kt @@ -2,20 +2,17 @@ package steps.credentials import abilities.ListenToEvents import common.CredentialSchema -import common.Utils.wait import interactions.Post import io.cucumber.java.en.Then import io.cucumber.java.en.When import io.iohk.atala.automation.extensions.get import io.iohk.atala.automation.serenity.ensure.Ensure -import models.AnoncredsSchema +import io.iohk.atala.automation.utils.Wait import models.CredentialEvent import net.serenitybdd.rest.SerenityRest import net.serenitybdd.screenplay.Actor -import net.serenitybdd.screenplay.rest.abilities.CallAnApi import org.apache.http.HttpStatus.* import org.hyperledger.identus.client.models.* -import java.util.UUID class IssueCredentialsSteps { @@ -108,65 +105,7 @@ class IssueCredentialsSteps { "name" to "Name", "surname" to "Surname", ) - sendCredentialOffer(issuer, holder, "short", schemaGuid, claims) - } - - @When("{actor} creates anoncred schema") - fun acmeCreatesAnoncredSchema(issuer: Actor) { - issuer.attemptsTo( - Post.to("/schema-registry/schemas") - .with { - it.body( - CredentialSchemaInput( - author = issuer.recall("shortFormDid"), - name = UUID.randomUUID().toString(), - description = "Simple student credentials schema", - type = "AnoncredSchemaV1", - schema = AnoncredsSchema( - name = "StudentCredential", - version = "1.0", - issuerId = issuer.recall("shortFormDid"), - attrNames = listOf("name", "age", "sex"), - ), - tags = listOf("school", "students"), - version = "1.0.0", - ), - ) - }, - ) - issuer.attemptsTo( - Ensure.thatTheLastResponse().statusCode().isEqualTo(SC_CREATED), - ) - val schema = SerenityRest.lastResponse().get() - issuer.remember("anoncredsSchema", schema) - } - - @When("{actor} creates anoncred credential definition") - fun acmeCreatesAnoncredCredentialDefinition(issuer: Actor) { - val schemaRegistryUrl = issuer.usingAbilityTo(CallAnApi::class.java).resolve("/schema-registry/schemas") - .replace("localhost", "host.docker.internal") - issuer.attemptsTo( - Post.to("/credential-definition-registry/definitions") - .with { - it.body( - CredentialDefinitionInput( - name = "StudentCredential", - version = "1.0.0", - schemaId = "$schemaRegistryUrl/${issuer.recall("anoncredsSchema").guid}/schema", - description = "Simple student credentials definition", - author = issuer.recall("shortFormDid"), - signatureType = "CL", - tag = "student", - supportRevocation = false, - ), - ) - }, - ) - issuer.attemptsTo( - Ensure.thatTheLastResponse().statusCode().isEqualTo(SC_CREATED), - ) - val credentialDefinition = SerenityRest.lastResponse().get() - issuer.remember("anoncredsCredentialDefinition", credentialDefinition) + sendCredentialOffer(issuer, holder, format, schemaGuid, claims) } @When("{actor} offers anoncred to {actor}") @@ -204,19 +143,18 @@ class IssueCredentialsSteps { @When("{actor} receives the credential offer") fun holderReceivesCredentialOffer(holder: Actor) { - wait( - { - credentialEvent = ListenToEvents.`as`(holder).credentialEvents.lastOrNull { - it.data.thid == holder.recall("thid") - } - credentialEvent != null && - credentialEvent!!.data.protocolState == IssueCredentialRecord.ProtocolState.OFFER_RECEIVED - }, - "Holder was unable to receive the credential offer from Issuer! " + + Wait.until( + errorMessage = "Holder was unable to receive the credential offer from Issuer! " + "Protocol state did not achieve ${IssueCredentialRecord.ProtocolState.OFFER_RECEIVED} state.", - ) + ) { + credentialEvent = ListenToEvents.with(holder).credentialEvents.lastOrNull { + it.data.thid == holder.recall("thid") + } + credentialEvent != null && + credentialEvent!!.data.protocolState == IssueCredentialRecord.ProtocolState.OFFER_RECEIVED + } - val recordId = ListenToEvents.`as`(holder).credentialEvents.last().data.recordId + val recordId = ListenToEvents.with(holder).credentialEvents.last().data.recordId holder.remember("recordId", recordId) } @@ -252,16 +190,15 @@ class IssueCredentialsSteps { @When("{actor} issues the credential") fun acmeIssuesTheCredential(issuer: Actor) { - wait( - { - credentialEvent = ListenToEvents.`as`(issuer).credentialEvents.lastOrNull { - it.data.thid == issuer.recall("thid") - } - credentialEvent != null && - credentialEvent!!.data.protocolState == IssueCredentialRecord.ProtocolState.REQUEST_RECEIVED - }, - "Issuer was unable to receive the credential request from Holder! Protocol state did not achieve RequestReceived state.", - ) + Wait.until( + errorMessage = "Issuer was unable to receive the credential request from Holder! Protocol state did not achieve RequestReceived state.", + ) { + credentialEvent = ListenToEvents.with(issuer).credentialEvents.lastOrNull { + it.data.thid == issuer.recall("thid") + } + credentialEvent != null && + credentialEvent!!.data.protocolState == IssueCredentialRecord.ProtocolState.REQUEST_RECEIVED + } val recordId = credentialEvent!!.data.recordId issuer.attemptsTo( Post.to("/issue-credentials/records/$recordId/issue-credential"), @@ -270,33 +207,32 @@ class IssueCredentialsSteps { Ensure.thatTheLastResponse().statusCode().isEqualTo(SC_OK), ) - wait( - { - credentialEvent = ListenToEvents.`as`(issuer).credentialEvents.lastOrNull { - it.data.thid == issuer.recall("thid") - } - credentialEvent != null && - credentialEvent!!.data.protocolState == IssueCredentialRecord.ProtocolState.CREDENTIAL_SENT - }, - "Issuer was unable to issue the credential! " + + Wait.until( + errorMessage = "Issuer was unable to issue the credential! " + "Protocol state did not achieve ${IssueCredentialRecord.ProtocolState.CREDENTIAL_SENT} state.", - ) + ) { + credentialEvent = ListenToEvents.with(issuer).credentialEvents.lastOrNull { + it.data.thid == issuer.recall("thid") + } + issuer.remember("issuedCredential", credentialEvent!!.data) + credentialEvent != null && + credentialEvent!!.data.protocolState == IssueCredentialRecord.ProtocolState.CREDENTIAL_SENT + } } @Then("{actor} receives the issued credential") fun bobHasTheCredentialIssued(holder: Actor) { - wait( - { - credentialEvent = ListenToEvents.`as`(holder).credentialEvents.lastOrNull { - it.data.thid == holder.recall("thid") - } - credentialEvent != null && - credentialEvent!!.data.protocolState == IssueCredentialRecord.ProtocolState.CREDENTIAL_RECEIVED - }, - "Holder was unable to receive the credential from Issuer! " + + Wait.until( + errorMessage = "Holder was unable to receive the credential from Issuer! " + "Protocol state did not achieve ${IssueCredentialRecord.ProtocolState.CREDENTIAL_RECEIVED} state.", - ) - holder.remember("issuedCredential", ListenToEvents.`as`(holder).credentialEvents.last().data) + ) { + credentialEvent = ListenToEvents.with(holder).credentialEvents.lastOrNull { + it.data.thid == holder.recall("thid") + } + credentialEvent != null && + credentialEvent!!.data.protocolState == IssueCredentialRecord.ProtocolState.CREDENTIAL_RECEIVED + } + holder.remember("issuedCredential", ListenToEvents.with(holder).credentialEvents.last().data) } @Then("{actor} should see that credential issuance has failed") diff --git a/tests/integration-tests/src/test/kotlin/steps/credentials/RevokeCredentialSteps.kt b/tests/integration-tests/src/test/kotlin/steps/credentials/RevokeCredentialSteps.kt new file mode 100644 index 0000000000..4033fd924f --- /dev/null +++ b/tests/integration-tests/src/test/kotlin/steps/credentials/RevokeCredentialSteps.kt @@ -0,0 +1,78 @@ +package steps.credentials + +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.serenity.ensure.Ensure +import io.iohk.atala.automation.utils.Wait +import models.JwtCredential +import net.serenitybdd.rest.SerenityRest +import net.serenitybdd.screenplay.Actor +import org.apache.http.HttpStatus +import org.hyperledger.identus.client.models.IssueCredentialRecord +import kotlin.time.Duration.Companion.seconds + +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() + issuer.remember("statusListId", statusListId) + + issuer.attemptsTo( + Get.resource("/credential-status/$statusListId"), + ) + val encodedList = SerenityRest.lastResponse().get("credentialSubject.encodedList") + issuer.remember("encodedStatusList", encodedList) + + issuer.attemptsTo( + Patch.to("/credential-status/revoke-credential/${issuedCredential.recordId}"), + Ensure.thatTheLastResponse().statusCode().isEqualTo(HttpStatus.SC_OK), + ) + } + + @When("{actor} tries to revoke credential from {actor}") + fun holderTriesToRevokeCredentialFromIssuer(holder: Actor, issuer: Actor) { + val issuedCredential = issuer.recall("issuedCredential") + val receivedCredential = holder.recall("issuedCredential") + holder.attemptsTo( + Patch.to("/credential-status/revoke-credential/${issuedCredential.recordId}"), + Ensure.thatTheLastResponse().statusCode().isEqualTo(HttpStatus.SC_NOT_FOUND), + ) + holder.attemptsTo( + Patch.to("/credential-status/revoke-credential/${receivedCredential.recordId}"), + Ensure.thatTheLastResponse().statusCode().isEqualTo(HttpStatus.SC_NOT_FOUND), + ) + } + + @Then("{actor} should see the credential was revoked") + fun credentialShouldBeRevoked(issuer: Actor) { + Wait.until( + timeout = 60.seconds, + errorMessage = "Encoded Status List didn't change after revoking.", + ) { + val statusListId: String = issuer.recall("statusListId") + val encodedStatusList: String = issuer.recall("encodedStatusList") + issuer.attemptsTo( + Get.resource("/credential-status/$statusListId"), + ) + val actualEncodedList: String = SerenityRest.lastResponse().jsonPath().get("credentialSubject.encodedList") + println("actual encoded $actualEncodedList | before encoded $encodedStatusList") + actualEncodedList != encodedStatusList + } + } + + @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() + issuer.remember("statusListId", statusListId) + + issuer.attemptsTo( + Get.resource("/credential-status/$statusListId"), + ) + } +} diff --git a/tests/integration-tests/src/test/kotlin/steps/did/DeactivateDidSteps.kt b/tests/integration-tests/src/test/kotlin/steps/did/DeactivateDidSteps.kt index b95a9f3233..52a46869f5 100644 --- a/tests/integration-tests/src/test/kotlin/steps/did/DeactivateDidSteps.kt +++ b/tests/integration-tests/src/test/kotlin/steps/did/DeactivateDidSteps.kt @@ -1,13 +1,12 @@ package steps.did -import common.TestConstants -import common.Utils.wait import interactions.Get import interactions.Post import io.cucumber.java.en.Then import io.cucumber.java.en.When import io.iohk.atala.automation.extensions.get import io.iohk.atala.automation.serenity.ensure.Ensure +import io.iohk.atala.automation.utils.Wait import net.serenitybdd.rest.SerenityRest import net.serenitybdd.screenplay.Actor import org.apache.http.HttpStatus @@ -29,19 +28,18 @@ class DeactivateDidSteps { Ensure.that(didOperationResponse.scheduledOperation.didRef).isNotEmpty(), Ensure.that(didOperationResponse.scheduledOperation.id).isNotEmpty(), ) + actor.forget("hasPublishedDid") + val deactivatedDid = actor.forget("shortFormDid") + actor.forget("longFormDid") + actor.remember("deactivatedDid", deactivatedDid) } @Then("{actor} sees that PRISM DID is successfully deactivated") fun actorSeesThatPrismDidIsSuccessfullyDeactivated(actor: Actor) { - wait( - { - actor.attemptsTo( - Get.resource("/dids/${actor.recall("shortFormDid")}"), - ) - SerenityRest.lastResponse().get().didDocumentMetadata.deactivated!! - }, - "ERROR: DID deactivate operation did not succeed on the ledger!", - timeout = TestConstants.DID_UPDATE_PUBLISH_MAX_WAIT_5_MIN, - ) + val deactivatedDid = actor.recall("deactivatedDid") + Wait.until(errorMessage = "ERROR: DID deactivate operation did not succeed on the ledger!") { + actor.attemptsTo(Get.resource("/dids/$deactivatedDid")) + SerenityRest.lastResponse().get().didDocumentMetadata.deactivated!! + } } } 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 0793e8b2f0..0b766d65b4 100644 --- a/tests/integration-tests/src/test/kotlin/steps/did/PublishDidSteps.kt +++ b/tests/integration-tests/src/test/kotlin/steps/did/PublishDidSteps.kt @@ -2,49 +2,21 @@ package steps.did import abilities.ListenToEvents import common.TestConstants -import common.Utils.wait 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 io.cucumber.java.en.* import io.iohk.atala.automation.extensions.get import io.iohk.atala.automation.serenity.ensure.Ensure +import io.iohk.atala.automation.utils.Wait import net.serenitybdd.rest.SerenityRest import net.serenitybdd.screenplay.Actor import org.apache.http.HttpStatus import org.apache.http.HttpStatus.SC_CREATED import org.apache.http.HttpStatus.SC_OK import org.hyperledger.identus.client.models.* +import kotlin.time.Duration.Companion.seconds class PublishDidSteps { - - @Given("{actor} have published PRISM DID") - fun actorHavePublishedPrismDid(actor: Actor) { - actor.attemptsTo( - Get.resource("/did-registrar/dids"), - ) - actor.attemptsTo( - Ensure.thatTheLastResponse().statusCode().isEqualTo(SC_OK), - ) - val publishedDids = SerenityRest.lastResponse().get().contents!!.filter { - // TODO: fix openapi spec to have statuses as enum - it.status == "PUBLISHED" - } - val did = publishedDids.firstOrNull { - actor.attemptsTo( - Get.resource("/dids/${it.did}"), - ) - !SerenityRest.lastResponse().get().didDocumentMetadata.deactivated!! - } - if (did == null) { - createsUnpublishedDid(actor) - hePublishesDidToLedger(actor) - } else { - actor.remember("shortFormDid", did.did) - } - } - @Given("{actor} creates unpublished DID") fun createsUnpublishedDid(actor: Actor) { val createDidRequest = CreateManagedDidRequest( @@ -67,27 +39,21 @@ class PublishDidSteps { .with { it.body(createDidRequest) }, - ) - - val managedDid = SerenityRest.lastResponse().get() - actor.attemptsTo( Ensure.thatTheLastResponse().statusCode().isEqualTo(SC_CREATED), - Ensure.that(managedDid.longFormDid!!).isNotEmpty(), ) - actor.remember("longFormDid", managedDid.longFormDid) + val managedDid = SerenityRest.lastResponse().get() actor.attemptsTo( + Ensure.that(managedDid.longFormDid!!).isNotEmpty(), Get.resource("/did-registrar/dids/${managedDid.longFormDid}"), - ) - actor.attemptsTo( Ensure.thatTheLastResponse().statusCode().isEqualTo(SC_OK), ) + val did = SerenityRest.lastResponse().get() - actor.remember( - "shortFormDid", - did.did, - ) + + actor.remember("longFormDid", managedDid.longFormDid) + actor.remember("shortFormDid", did.did) actor.forget("hasPublishedDid") } @@ -130,17 +96,15 @@ class PublishDidSteps { Ensure.that(didOperationResponse.scheduledOperation.id).isNotEmpty(), ) - wait( - { - val didEvent = - ListenToEvents.`as`(actor).didEvents.lastOrNull { - it.data.did == actor.recall("shortFormDid") - } - didEvent != null && didEvent.data.status == "PUBLISHED" - }, - "ERROR: DID was not published to ledger!", - timeout = TestConstants.DID_UPDATE_PUBLISH_MAX_WAIT_5_MIN, - ) + Wait.until( + timeout = 30.seconds, + errorMessage = "ERROR: DID was not published to ledger!", + ) { + val didEvent = ListenToEvents.with(actor).didEvents.lastOrNull { + it.data.did == actor.recall("shortFormDid") + } + didEvent != null && didEvent.data.status == "PUBLISHED" + } actor.attemptsTo( Get.resource("/dids/${actor.recall("shortFormDid")}"), ) 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 9ff4b5e207..ea2c0ec435 100644 --- a/tests/integration-tests/src/test/kotlin/steps/did/UpdateDidSteps.kt +++ b/tests/integration-tests/src/test/kotlin/steps/did/UpdateDidSteps.kt @@ -1,24 +1,17 @@ package steps.did import common.TestConstants -import common.Utils.wait import interactions.Get import interactions.Post import io.cucumber.java.en.Then import io.cucumber.java.en.When import io.iohk.atala.automation.extensions.get import io.iohk.atala.automation.serenity.ensure.Ensure +import io.iohk.atala.automation.utils.Wait import net.serenitybdd.rest.SerenityRest import net.serenitybdd.screenplay.Actor import org.apache.http.HttpStatus -import org.hyperledger.identus.client.models.ActionType -import org.hyperledger.identus.client.models.DIDOperationResponse -import org.hyperledger.identus.client.models.DIDResolutionResult -import org.hyperledger.identus.client.models.Json -import org.hyperledger.identus.client.models.RemoveEntryById -import org.hyperledger.identus.client.models.UpdateManagedDIDRequest -import org.hyperledger.identus.client.models.UpdateManagedDIDRequestAction -import org.hyperledger.identus.client.models.UpdateManagedDIDServiceAction +import org.hyperledger.identus.client.models.* class UpdateDidSteps { @@ -92,94 +85,84 @@ class UpdateDidSteps { @Then("{actor} sees PRISM DID was successfully updated with new keys") fun actorSeesDidSuccessfullyUpdatedWithNewKeys(actor: Actor) { - wait( - { - 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.any { - it == "${actor.recall("shortFormDid")}#${TestConstants.PRISM_DID_UPDATE_NEW_AUTH_KEY.id}" - } && verificationMethods.any { - it == "${actor.recall("shortFormDid")}#${TestConstants.PRISM_DID_AUTH_KEY.id}" - } - }, - "ERROR: DID UPDATE operation did not succeed on the ledger!", - timeout = TestConstants.DID_UPDATE_PUBLISH_MAX_WAIT_5_MIN, - ) + 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.any { + it == "${actor.recall("shortFormDid")}#${TestConstants.PRISM_DID_UPDATE_NEW_AUTH_KEY.id}" + } && verificationMethods.any { + it == "${actor.recall("shortFormDid")}#${TestConstants.PRISM_DID_AUTH_KEY.id}" + } + } } @Then("{actor} sees PRISM DID was successfully updated and keys removed") fun actorSeesDidSuccessfullyUpdatedAndKeysRemoved(actor: Actor) { - wait( - { - 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}" - } - }, - "ERROR: DID UPDATE operation did not succeed on the ledger!", - timeout = TestConstants.DID_UPDATE_PUBLISH_MAX_WAIT_5_MIN, - ) + 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}" + } + } } @Then("{actor} sees PRISM DID was successfully updated with new services") fun actorSeesDidSuccessfullyUpdatedWithNewServices(actor: Actor) { - wait( - { - actor.attemptsTo( - Get.resource("/dids/${actor.recall("shortFormDid")}"), - ) - val serviceIds = - SerenityRest.lastResponse().get().didDocument!!.service!!.map { it.id } - serviceIds.any { - it == "${actor.recall("shortFormDid")}#${TestConstants.PRISM_DID_UPDATE_NEW_SERVICE.id}" - } - }, - "ERROR: DID UPDATE operation did not succeed on the ledger!", - timeout = TestConstants.DID_UPDATE_PUBLISH_MAX_WAIT_5_MIN, - ) + Wait.until( + errorMessage = "ERROR: DID UPDATE operation did not succeed on the ledger!", + ) { + actor.attemptsTo( + Get.resource("/dids/${actor.recall("shortFormDid")}"), + ) + val serviceIds = + SerenityRest.lastResponse().get().didDocument!!.service!!.map { it.id } + serviceIds.any { + it == "${actor.recall("shortFormDid")}#${TestConstants.PRISM_DID_UPDATE_NEW_SERVICE.id}" + } + } } @Then("{actor} sees PRISM DID was successfully updated by removing services") fun actorSeesDidSuccessfullyUpdatedByRemovingServices(actor: Actor) { - wait( - { - actor.attemptsTo( - Get.resource("/dids/${actor.recall("shortFormDid")}"), - ) - val serviceIds = - SerenityRest.lastResponse().get().didDocument!!.service!!.map { it.id } - serviceIds.none { - it == "${actor.recall("shortFormDid")}#${TestConstants.PRISM_DID_UPDATE_NEW_SERVICE.id}" - } - }, - "ERROR: DID UPDATE operation did not succeed on the ledger!", - timeout = TestConstants.DID_UPDATE_PUBLISH_MAX_WAIT_5_MIN, - ) + Wait.until( + errorMessage = "ERROR: DID UPDATE operation did not succeed on the ledger!", + ) { + actor.attemptsTo( + Get.resource("/dids/${actor.recall("shortFormDid")}"), + ) + val serviceIds = + SerenityRest.lastResponse().get().didDocument!!.service!!.map { it.id } + serviceIds.none { + it == "${actor.recall("shortFormDid")}#${TestConstants.PRISM_DID_UPDATE_NEW_SERVICE.id}" + } + } } @Then("{actor} sees PRISM DID was successfully updated by updating services") fun actorSeesDidSuccessfullyUpdatedByUpdatingServices(actor: Actor) { - wait( - { - actor.attemptsTo( - 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) } - }, - "ERROR: DID UPDATE operation did not succeed on the ledger!", - timeout = TestConstants.DID_UPDATE_PUBLISH_MAX_WAIT_5_MIN, - ) + Wait.until( + errorMessage = "ERROR: DID UPDATE operation did not succeed on the ledger!", + ) { + actor.attemptsTo( + 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) } + } } } diff --git a/tests/integration-tests/src/test/kotlin/steps/proofs/AnoncredsPresentProofSteps.kt b/tests/integration-tests/src/test/kotlin/steps/proofs/AnoncredsPresentProofSteps.kt index 566d2f1864..ace6fc890e 100644 --- a/tests/integration-tests/src/test/kotlin/steps/proofs/AnoncredsPresentProofSteps.kt +++ b/tests/integration-tests/src/test/kotlin/steps/proofs/AnoncredsPresentProofSteps.kt @@ -1,13 +1,14 @@ package steps.proofs import abilities.ListenToEvents -import common.Utils.wait import interactions.Patch import interactions.Post import io.cucumber.java.en.When import io.iohk.atala.automation.extensions.get import io.iohk.atala.automation.serenity.ensure.Ensure +import io.iohk.atala.automation.utils.Wait import models.PresentationEvent +import models.PresentationStatusAdapter import net.serenitybdd.rest.SerenityRest import net.serenitybdd.screenplay.Actor import net.serenitybdd.screenplay.rest.abilities.CallAnApi @@ -67,7 +68,7 @@ class AnoncredsPresentProofSteps { }, ) faber.attemptsTo( - Ensure.thatTheLastResponse().apply { println(this.contentType()) }.statusCode().isEqualTo(SC_CREATED), + Ensure.thatTheLastResponse().statusCode().isEqualTo(SC_CREATED), ) val presentationStatus = SerenityRest.lastResponse().get() faber.remember("thid", presentationStatus.thid) @@ -76,21 +77,20 @@ class AnoncredsPresentProofSteps { @When("{actor} receives the anoncreds request") fun bobReceivesTheAnoncredsRequest(bob: Actor) { - wait( - { - proofEvent = ListenToEvents.`as`(bob).presentationEvents.lastOrNull { - it.data.thid == bob.recall("thid") - } - proofEvent != null && - proofEvent!!.data.status == PresentationStatus.Status.REQUEST_RECEIVED - }, - "ERROR: Bob did not achieve any presentation request!", - ) + Wait.until( + errorMessage = "ERROR: Bob did not achieve any presentation request!", + ) { + proofEvent = ListenToEvents.with(bob).presentationEvents.lastOrNull { + it.data.thid == bob.recall("thid") + } + proofEvent != null && + proofEvent!!.data.status == PresentationStatusAdapter.Status.REQUEST_RECEIVED + } bob.remember("presentationId", proofEvent!!.data.presentationId) } - @When("{actor} accepts the anoncreds presentation request from {actor}") - fun bobAcceptsTheAnoncredsPresentationWithProof(bob: Actor, faber: Actor) { + @When("{actor} accepts the anoncreds presentation request") + fun bobAcceptsTheAnoncredsPresentationWithProof(bob: Actor) { val requestPresentationAction = RequestPresentationAction( anoncredPresentationRequest = AnoncredCredentialProofsV1( @@ -105,8 +105,9 @@ class AnoncredsPresentProofSteps { action = RequestPresentationAction.Action.REQUEST_MINUS_ACCEPT, ) + val presentationId = bob.recall("presentationId") bob.attemptsTo( - Patch.to("/present-proof/presentations/${bob.recall("presentationId")}").with { + Patch.to("/present-proof/presentations/$presentationId").with { it.body( requestPresentationAction, ) 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 741ef0e9b6..9d2d09665d 100644 --- a/tests/integration-tests/src/test/kotlin/steps/proofs/PresentProofSteps.kt +++ b/tests/integration-tests/src/test/kotlin/steps/proofs/PresentProofSteps.kt @@ -1,23 +1,22 @@ package steps.proofs import abilities.ListenToEvents -import common.Utils.wait import interactions.Patch import interactions.Post import io.cucumber.java.en.Then import io.cucumber.java.en.When import io.iohk.atala.automation.extensions.get import io.iohk.atala.automation.serenity.ensure.Ensure -import models.PresentationEvent +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.hyperledger.identus.client.models.* +import kotlin.time.Duration.Companion.seconds class PresentProofSteps { - private var proofEvent: PresentationEvent? = null - @When("{actor} sends a request for proof presentation to {actor}") fun faberSendsARequestForProofPresentationToBob(faber: Actor, bob: Actor) { val presentationRequest = RequestPresentationInput( @@ -51,17 +50,16 @@ class PresentProofSteps { @When("{actor} receives the request") fun bobReceivesTheRequest(bob: Actor) { - wait( - { - proofEvent = ListenToEvents.`as`(bob).presentationEvents.lastOrNull { - it.data.thid == bob.recall("thid") - } - proofEvent != null && - proofEvent!!.data.status == PresentationStatus.Status.REQUEST_RECEIVED - }, - "ERROR: Bob did not achieve any presentation request!", - ) - bob.remember("presentationId", proofEvent!!.data.presentationId) + 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") + } + bob.remember("presentationId", proofEvent?.data?.presentationId) + proofEvent?.data?.status == PresentationStatusAdapter.Status.REQUEST_RECEIVED + } } @When("{actor} makes the presentation of the proof to {actor}") @@ -73,9 +71,7 @@ class PresentProofSteps { bob.attemptsTo( Patch.to("/present-proof/presentations/${bob.recall("presentationId")}").with { - it.body( - requestPresentationAction, - ) + it.body(requestPresentationAction) }, ) } @@ -85,9 +81,7 @@ class PresentProofSteps { bob.attemptsTo( Patch.to("/present-proof/presentations/${bob.recall("presentationId")}").with { it.body( - RequestPresentationAction( - action = RequestPresentationAction.Action.REQUEST_MINUS_REJECT, - ), + RequestPresentationAction(action = RequestPresentationAction.Action.REQUEST_MINUS_REJECT), ) }, ) @@ -95,30 +89,40 @@ class PresentProofSteps { @Then("{actor} sees the proof is rejected") fun bobSeesProofIsRejected(bob: Actor) { - wait( - { - proofEvent = ListenToEvents.`as`(bob).presentationEvents.lastOrNull { - it.data.thid == bob.recall("thid") - } - proofEvent != null && - proofEvent!!.data.status == PresentationStatus.Status.REQUEST_REJECTED - }, - "ERROR: Faber did not receive presentation from Bob!", - ) + Wait.until( + timeout = 30.seconds, + errorMessage = "ERROR: Faber did not receive presentation from Bob!", + ) { + val proofEvent = ListenToEvents.with(bob).presentationEvents.lastOrNull { + it.data.thid == bob.recall("thid") + } + proofEvent?.data?.status == PresentationStatusAdapter.Status.REQUEST_REJECTED + } } @Then("{actor} has the proof verified") fun faberHasTheProofVerified(faber: Actor) { - wait( - { - proofEvent = ListenToEvents.`as`(faber).presentationEvents.lastOrNull { - it.data.thid == faber.recall("thid") - } + Wait.until( + timeout = 30.seconds, + errorMessage = "Presentation did not achieve PresentationVerified state!", + ) { + val proofEvent = ListenToEvents.with(faber).presentationEvents.lastOrNull { + it.data.thid == faber.recall("thid") + } + proofEvent?.data?.status == PresentationStatusAdapter.Status.PRESENTATION_VERIFIED + } + } - proofEvent != null && - proofEvent!!.data.status == PresentationStatus.Status.PRESENTATION_VERIFIED - }, - "ERROR: presentation did not achieve PresentationVerified state!", - ) + @Then("{actor} sees the proof returned verification failed") + fun verifierSeesTheProofReturnedVerificationFailed(verifier: Actor) { + Wait.until( + timeout = 60.seconds, + errorMessage = "Presentation did not achieve PresentationVerificationFailed state!", + ) { + val proofEvent = ListenToEvents.with(verifier).presentationEvents.lastOrNull { + it.data.thid == verifier.recall("thid") + } + proofEvent?.data?.status == PresentationStatusAdapter.Status.PRESENTATION_VERIFICATION_FAILED + } } } diff --git a/tests/integration-tests/src/test/kotlin/steps/schemas/AnoncredCredentialSchemaSteps.kt b/tests/integration-tests/src/test/kotlin/steps/schemas/AnoncredCredentialSchemaSteps.kt new file mode 100644 index 0000000000..3279777b88 --- /dev/null +++ b/tests/integration-tests/src/test/kotlin/steps/schemas/AnoncredCredentialSchemaSteps.kt @@ -0,0 +1,85 @@ +package steps.schemas + +import interactions.Post +import io.cucumber.java.en.Given +import io.cucumber.java.en.When +import io.iohk.atala.automation.extensions.get +import io.iohk.atala.automation.serenity.ensure.Ensure +import models.AnoncredsSchema +import net.serenitybdd.rest.SerenityRest +import net.serenitybdd.screenplay.Actor +import net.serenitybdd.screenplay.rest.abilities.CallAnApi +import org.apache.http.HttpStatus.SC_CREATED +import org.hyperledger.identus.client.models.* +import java.util.UUID + +class AnoncredCredentialSchemaSteps { + @Given("{actor} has an anoncred schema definition") + fun issuerHasAnAnoncredSchemaDefinition(issuer: Actor) { + if (issuer.recallAll().containsKey("anoncredsCredentialDefinition")) { + return + } + if (!issuer.recallAll().containsKey("anoncredsSchema")) { + issuerCreatesAnoncredSchema(issuer) + } + issuerCreatesAnoncredCredentialDefinition(issuer) + } + + @When("{actor} creates anoncred schema") + fun issuerCreatesAnoncredSchema(issuer: Actor) { + issuer.attemptsTo( + Post.to("/schema-registry/schemas") + .with { + it.body( + CredentialSchemaInput( + author = issuer.recall("shortFormDid"), + name = UUID.randomUUID().toString(), + description = "Simple student credentials schema", + type = "AnoncredSchemaV1", + schema = AnoncredsSchema( + name = "StudentCredential", + version = "1.0", + issuerId = issuer.recall("shortFormDid"), + attrNames = listOf("name", "age", "sex"), + ), + tags = listOf("school", "students"), + version = "1.0.0", + ), + ) + }, + ) + issuer.attemptsTo( + Ensure.thatTheLastResponse().statusCode().isEqualTo(SC_CREATED), + ) + val schema = SerenityRest.lastResponse().get() + issuer.remember("anoncredsSchema", schema) + } + + @When("{actor} creates anoncred credential definition") + fun issuerCreatesAnoncredCredentialDefinition(issuer: Actor) { + val schemaRegistryUrl = issuer.usingAbilityTo(CallAnApi::class.java).resolve("/schema-registry/schemas") + .replace("localhost", "host.docker.internal") + issuer.attemptsTo( + Post.to("/credential-definition-registry/definitions") + .with { + it.body( + CredentialDefinitionInput( + name = "StudentCredential", + version = "1.0.0", + schemaId = "$schemaRegistryUrl/${issuer.recall("anoncredsSchema").guid}/schema", + description = "Simple student credentials definition", + author = issuer.recall("shortFormDid"), + signatureType = "CL", + tag = "student", + supportRevocation = false, + ), + ) + }, + ) + issuer.attemptsTo( + Ensure.thatTheLastResponse().statusCode().isEqualTo(SC_CREATED), + ) + val credentialDefinition = SerenityRest.lastResponse().get() + issuer.remember("anoncredsCredentialDefinition", credentialDefinition) + } +} 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_published_did.feature index d33c7a021d..c44fa3c948 100644 --- a/tests/integration-tests/src/test/resources/features/credentials/issue_published_did.feature +++ b/tests/integration-tests/src/test/resources/features/credentials/issue_published_did.feature @@ -15,9 +15,8 @@ Feature: Issue Credentials Protocol with published DID Then Holder receives the issued credential Scenario: Issuing anoncred with published PRISM DID - When Issuer creates anoncred schema - And Issuer creates anoncred credential definition - And Issuer offers anoncred to Holder + 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 diff --git a/tests/integration-tests/src/test/resources/features/did/deactivate_did.feature b/tests/integration-tests/src/test/resources/features/did/deactivate_did.feature index bed84078a0..89fc7358f5 100644 --- a/tests/integration-tests/src/test/resources/features/did/deactivate_did.feature +++ b/tests/integration-tests/src/test/resources/features/did/deactivate_did.feature @@ -2,6 +2,7 @@ Feature: Deactivate DID Scenario: Deactivate DID - Given Issuer have published PRISM DID + Given Issuer creates unpublished DID + And Issuer publishes DID to ledger When Issuer deactivates PRISM DID Then He sees that PRISM DID is successfully deactivated 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 ae474c65fa..ae2496ccb1 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,7 +2,7 @@ Feature: Update DID Background: Published DID is created - Given Issuer have published PRISM DID + Given Issuer has a published DID Scenario: Update PRISM DID by adding new services When Issuer updates PRISM DID with new services 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 cbf44843f6..f439ce3a09 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 @@ -1,3 +1,4 @@ +@proof @jwt Feature: Present Proof Protocol Scenario: Holder presents credential proof to verifier 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 9161c46bb4..cee6e19472 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 @@ -1,3 +1,4 @@ +@proof @anoncreds Feature: Present Proof Protocol Scenario: Holder presents anoncreds credential proof to verifier @@ -5,8 +6,7 @@ Scenario: Holder presents anoncreds credential proof to verifier And Verifier and Holder have an existing connection And Issuer has a published DID And Holder has an unpublished DID - And Issuer creates anoncred schema - And Issuer creates anoncred credential definition + And Issuer has an anoncred schema definition And Issuer offers anoncred to Holder And Holder receives the credential offer And Holder accepts credential offer for anoncred @@ -14,5 +14,5 @@ Scenario: Holder presents anoncreds credential proof to verifier And Holder receives the issued credential When Verifier sends a anoncreds request for proof presentation to Holder using credential definition issued by Issuer And Holder receives the anoncreds request - And Holder accepts the anoncreds presentation request from Verifier + And Holder accepts the anoncreds presentation request Then Verifier has the proof verified 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 new file mode 100644 index 0000000000..2ac02f447b --- /dev/null +++ b/tests/integration-tests/src/test/resources/features/revocation/revoke_jwt_credential.feature @@ -0,0 +1,21 @@ +@revocation @jwt +Feature: Credential revocation - JWT + + Background: + Given Holder has an 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 + 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 + Then Issuer has the proof verified + And Issuer should see the credential is not revoked