From d10507bc14ca68f44dc3a0b73f96ad3186b24b1c Mon Sep 17 00:00:00 2001 From: Allain Magyar Date: Mon, 15 Jul 2024 08:52:16 -0300 Subject: [PATCH] test: add sd-jwt integration test (#1260) 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 Signed-off-by: Shota Jolbordi --- .../test/kotlin/abilities/ListenToEvents.kt | 39 +++ .../src/test/kotlin/common/DidPurpose.kt | 25 +- .../src/test/kotlin/models/JwtCredential.kt | 3 +- .../src/test/kotlin/models/SdJwtClaim.kt | 7 + .../test/kotlin/steps/common/CommonSteps.kt | 67 +++-- .../steps/connection/ConnectionSteps.kt | 38 ++- .../kotlin/steps/credentials/AnoncredSteps.kt | 50 ++++ .../steps/credentials/CredentialSteps.kt | 79 ++++++ .../credentials/IssueCredentialsSteps.kt | 242 ------------------ .../steps/credentials/JwtCredentialSteps.kt | 110 ++++++++ .../credentials/RevokeCredentialSteps.kt | 34 ++- .../steps/credentials/SdJwtCredentialSteps.kt | 137 ++++++++++ .../kotlin/steps/did/DeactivateDidSteps.kt | 16 +- .../test/kotlin/steps/did/PublishDidSteps.kt | 37 +-- .../test/kotlin/steps/did/UpdateDidSteps.kt | 154 ++++++----- ...entProofSteps.kt => AnoncredProofSteps.kt} | 58 +---- .../kotlin/steps/proofs/HolderProofSteps.kt | 35 +++ .../test/kotlin/steps/proofs/JwtProofSteps.kt | 52 ++++ .../kotlin/steps/proofs/PresentProofSteps.kt | 123 --------- .../kotlin/steps/proofs/SdJwtProofSteps.kt | 99 +++++++ .../kotlin/steps/proofs/VerifierProofSteps.kt | 32 +++ .../anoncred/issuance.feature} | 6 +- .../anoncred/present_proof.feature} | 6 +- .../features/credential/jwt/issuance.feature | 41 +++ .../jwt}/present_proof.feature | 16 +- .../jwt/revocation.feature} | 16 +- .../credential/sdjwt/issuance.feature | 37 +++ .../credential/sdjwt/present_proof.feature | 35 +++ .../issue_jwt_with_published_did.feature | 34 --- .../issue_jwt_with_unpublished_did.feature | 14 - 30 files changed, 1014 insertions(+), 628 deletions(-) create mode 100644 tests/integration-tests/src/test/kotlin/models/SdJwtClaim.kt create mode 100644 tests/integration-tests/src/test/kotlin/steps/credentials/AnoncredSteps.kt create mode 100644 tests/integration-tests/src/test/kotlin/steps/credentials/CredentialSteps.kt delete mode 100644 tests/integration-tests/src/test/kotlin/steps/credentials/IssueCredentialsSteps.kt create mode 100644 tests/integration-tests/src/test/kotlin/steps/credentials/JwtCredentialSteps.kt create mode 100644 tests/integration-tests/src/test/kotlin/steps/credentials/SdJwtCredentialSteps.kt rename tests/integration-tests/src/test/kotlin/steps/proofs/{AnoncredsPresentProofSteps.kt => AnoncredProofSteps.kt} (61%) create mode 100644 tests/integration-tests/src/test/kotlin/steps/proofs/HolderProofSteps.kt create mode 100644 tests/integration-tests/src/test/kotlin/steps/proofs/JwtProofSteps.kt delete mode 100644 tests/integration-tests/src/test/kotlin/steps/proofs/PresentProofSteps.kt create mode 100644 tests/integration-tests/src/test/kotlin/steps/proofs/SdJwtProofSteps.kt create mode 100644 tests/integration-tests/src/test/kotlin/steps/proofs/VerifierProofSteps.kt rename tests/integration-tests/src/test/resources/features/{credentials/issue_anoncred_with_published_did.feature => credential/anoncred/issuance.feature} (78%) rename tests/integration-tests/src/test/resources/features/{proofs/present_proof_anoncred.feature => credential/anoncred/present_proof.feature} (86%) create mode 100644 tests/integration-tests/src/test/resources/features/credential/jwt/issuance.feature rename tests/integration-tests/src/test/resources/features/{proofs => credential/jwt}/present_proof.feature (60%) rename tests/integration-tests/src/test/resources/features/{revocation/revoke_jwt_credential.feature => credential/jwt/revocation.feature} (55%) create mode 100644 tests/integration-tests/src/test/resources/features/credential/sdjwt/issuance.feature create mode 100644 tests/integration-tests/src/test/resources/features/credential/sdjwt/present_proof.feature delete mode 100644 tests/integration-tests/src/test/resources/features/credentials/issue_jwt_with_published_did.feature delete mode 100644 tests/integration-tests/src/test/resources/features/credentials/issue_jwt_with_unpublished_did.feature diff --git a/tests/integration-tests/src/test/kotlin/abilities/ListenToEvents.kt b/tests/integration-tests/src/test/kotlin/abilities/ListenToEvents.kt index 8c721cce04..4ea1481a90 100644 --- a/tests/integration-tests/src/test/kotlin/abilities/ListenToEvents.kt +++ b/tests/integration-tests/src/test/kotlin/abilities/ListenToEvents.kt @@ -14,6 +14,9 @@ import models.* import net.serenitybdd.screenplay.Ability import net.serenitybdd.screenplay.Actor import net.serenitybdd.screenplay.HasTeardown +import net.serenitybdd.screenplay.Question +import org.hyperledger.identus.client.models.Connection +import org.hyperledger.identus.client.models.IssueCredentialRecord import java.net.URL import java.time.OffsetDateTime @@ -64,6 +67,42 @@ open class ListenToEvents( fun with(actor: Actor): ListenToEvents { return actor.abilityTo(ListenToEvents::class.java) } + + fun presentationProofStatus(actor: Actor): Question { + return Question.about("presentation status").answeredBy { + val proofEvent = with(actor).presentationEvents.lastOrNull { + it.data.thid == actor.recall("thid") + } + proofEvent?.data?.status + } + } + + fun connectionState(actor: Actor): Question { + return Question.about("connection state").answeredBy { + val lastEvent = with(actor).connectionEvents.lastOrNull { + it.data.thid == actor.recall("connection").thid + } + lastEvent?.data?.state + } + } + + fun credentialState(actor: Actor): Question { + return Question.about("credential state").answeredBy { + val credentialEvent = ListenToEvents.with(actor).credentialEvents.lastOrNull { + it.data.thid == actor.recall("thid") + } + credentialEvent?.data?.protocolState + } + } + + fun didStatus(actor: Actor): Question { + return Question.about("did status").answeredBy { + val didEvent = ListenToEvents.with(actor).didEvents.lastOrNull { + it.data.did == actor.recall("shortFormDid") + } + didEvent?.data?.status + } + } } init { diff --git a/tests/integration-tests/src/test/kotlin/common/DidPurpose.kt b/tests/integration-tests/src/test/kotlin/common/DidPurpose.kt index dee1243f98..7dcc6ec6e3 100644 --- a/tests/integration-tests/src/test/kotlin/common/DidPurpose.kt +++ b/tests/integration-tests/src/test/kotlin/common/DidPurpose.kt @@ -3,23 +3,30 @@ package common import org.hyperledger.identus.client.models.* enum class DidPurpose { - EMPTY { - override val publicKeys = emptyList() - override val services = emptyList() + CUSTOM { + override val publicKeys = mutableListOf() + override val services = mutableListOf() + }, + SD_JWT { + override val publicKeys = mutableListOf( + ManagedDIDKeyTemplate("auth-1", Purpose.AUTHENTICATION, Curve.ED25519), + ManagedDIDKeyTemplate("assertion-1", Purpose.ASSERTION_METHOD, Curve.ED25519), + ) + override val services = mutableListOf() }, JWT { - override val publicKeys = listOf( + override val publicKeys = mutableListOf( 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() + override val services = mutableListOf() }, ANONCRED { - override val publicKeys = emptyList() - override val services = emptyList() + override val publicKeys = mutableListOf() + override val services = mutableListOf() }, ; - abstract val publicKeys: List - abstract val services: List + abstract val publicKeys: MutableList + abstract val services: MutableList } diff --git a/tests/integration-tests/src/test/kotlin/models/JwtCredential.kt b/tests/integration-tests/src/test/kotlin/models/JwtCredential.kt index e4395e27e0..f1d3e71695 100644 --- a/tests/integration-tests/src/test/kotlin/models/JwtCredential.kt +++ b/tests/integration-tests/src/test/kotlin/models/JwtCredential.kt @@ -16,7 +16,6 @@ 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 @@ -146,7 +145,7 @@ class JwtCredential { } fun parseBase64(base64: String): JwtCredential { - val jwt = String(Base64.getDecoder().decode(base64)) + val jwt = Base64URL.from(base64).decodeToString() return parseJwt(jwt) } diff --git a/tests/integration-tests/src/test/kotlin/models/SdJwtClaim.kt b/tests/integration-tests/src/test/kotlin/models/SdJwtClaim.kt new file mode 100644 index 0000000000..0ce288c800 --- /dev/null +++ b/tests/integration-tests/src/test/kotlin/models/SdJwtClaim.kt @@ -0,0 +1,7 @@ +package models + +data class SdJwtClaim( + val salt: String, + val key: String, + val value: String, +) 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 bc885b15ee..d5c8428b13 100644 --- a/tests/integration-tests/src/test/kotlin/steps/common/CommonSteps.kt +++ b/tests/integration-tests/src/test/kotlin/steps/common/CommonSteps.kt @@ -12,29 +12,31 @@ import org.apache.http.HttpStatus import org.hyperledger.identus.client.models.Connection import org.hyperledger.identus.client.models.ConnectionsPage import steps.connection.ConnectionSteps -import steps.credentials.IssueCredentialsSteps +import steps.credentials.* import steps.did.PublishDidSteps import steps.schemas.CredentialSchemasSteps class CommonSteps { @Given("{actor} has a jwt issued credential from {actor}") - fun holderHasIssuedCredentialFromIssuer(holder: Actor, issuer: Actor) { + fun holderHasIssuedJwtCredentialFromIssuer(holder: Actor, issuer: Actor) { actorsHaveExistingConnection(issuer, holder) val publishDidSteps = PublishDidSteps() publishDidSteps.agentHasAnUnpublishedDID(holder, DidPurpose.JWT) publishDidSteps.agentHasAPublishedDID(issuer, DidPurpose.JWT) - val issueSteps = IssueCredentialsSteps() - issueSteps.issuerOffersACredential(issuer, holder, "short") - issueSteps.holderReceivesCredentialOffer(holder) - issueSteps.holderAcceptsCredentialOfferForJwt(holder) - issueSteps.acmeIssuesTheCredential(issuer) - issueSteps.bobHasTheCredentialIssued(holder) + val jwtCredentialSteps = JwtCredentialSteps() + val credentialSteps = CredentialSteps() + + jwtCredentialSteps.issuerOffersAJwtCredential(issuer, holder, "short") + credentialSteps.holderReceivesCredentialOffer(holder) + jwtCredentialSteps.holderAcceptsJwtCredentialOfferForJwt(holder) + credentialSteps.issuerIssuesTheCredential(issuer) + credentialSteps.holderReceivesTheIssuedCredential(holder) } @Given("{actor} has a jwt issued credential with {} schema from {actor}") - fun holderHasIssuedCredentialFromIssuerWithSchema( + fun holderHasIssuedJwtCredentialFromIssuerWithSchema( holder: Actor, schema: CredentialSchema, issuer: Actor, @@ -48,12 +50,47 @@ class CommonSteps { val schemaSteps = CredentialSchemasSteps() schemaSteps.agentHasAPublishedSchema(issuer, schema) - val issueSteps = IssueCredentialsSteps() - issueSteps.issuerOffersCredentialToHolderUsingSchema(issuer, holder, "short", schema) - issueSteps.holderReceivesCredentialOffer(holder) - issueSteps.holderAcceptsCredentialOfferForJwt(holder) - issueSteps.acmeIssuesTheCredential(issuer) - issueSteps.bobHasTheCredentialIssued(holder) + val jwtCredentialSteps = JwtCredentialSteps() + val credentialSteps = CredentialSteps() + jwtCredentialSteps.issuerOffersJwtCredentialToHolderUsingSchema(issuer, holder, "short", schema) + credentialSteps.holderReceivesCredentialOffer(holder) + jwtCredentialSteps.holderAcceptsJwtCredentialOfferForJwt(holder) + credentialSteps.issuerIssuesTheCredential(issuer) + credentialSteps.holderReceivesTheIssuedCredential(holder) + } + + @Given("{actor} has a sd-jwt issued credential from {actor}") + fun holderHasIssuedSdJwtCredentialFromIssuer(holder: Actor, issuer: Actor) { + actorsHaveExistingConnection(issuer, holder) + + val publishDidSteps = PublishDidSteps() + publishDidSteps.agentHasAnUnpublishedDID(holder, DidPurpose.SD_JWT) + publishDidSteps.agentHasAPublishedDID(issuer, DidPurpose.SD_JWT) + + val sdJwtCredentialSteps = SdJwtCredentialSteps() + val credentialSteps = CredentialSteps() + sdJwtCredentialSteps.issuerOffersSdJwtCredentialToHolder(issuer, holder) + credentialSteps.holderReceivesCredentialOffer(holder) + sdJwtCredentialSteps.holderAcceptsSdJwtCredentialOffer(holder) + credentialSteps.issuerIssuesTheCredential(issuer) + credentialSteps.holderReceivesTheIssuedCredential(holder) + } + + @Given("{actor} has a bound sd-jwt issued credential from {actor}") + fun holderHasIssuedSdJwtCredentialFromIssuerWithKeyBind(holder: Actor, issuer: Actor) { + actorsHaveExistingConnection(issuer, holder) + + val publishDidSteps = PublishDidSteps() + publishDidSteps.agentHasAnUnpublishedDID(holder, DidPurpose.SD_JWT) + publishDidSteps.agentHasAPublishedDID(issuer, DidPurpose.SD_JWT) + + val sdJwtCredentialSteps = SdJwtCredentialSteps() + val credentialSteps = CredentialSteps() + sdJwtCredentialSteps.issuerOffersSdJwtCredentialToHolder(issuer, holder) + credentialSteps.holderReceivesCredentialOffer(holder) + sdJwtCredentialSteps.holderAcceptsSdJwtCredentialOfferWithKeyBinding(holder, "auth-1") + credentialSteps.issuerIssuesTheCredential(issuer) + credentialSteps.holderReceivesTheIssuedCredential(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 886bb64b86..17df7e4bf3 100644 --- a/tests/integration-tests/src/test/kotlin/steps/connection/ConnectionSteps.kt +++ b/tests/integration-tests/src/test/kotlin/steps/connection/ConnectionSteps.kt @@ -8,13 +8,16 @@ 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 io.iohk.atala.automation.serenity.interactions.PollingWait 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.hamcrest.CoreMatchers import org.hyperledger.identus.client.models.* +import org.hyperledger.identus.client.models.Connection.State.CONNECTION_RESPONSE_RECEIVED +import org.hyperledger.identus.client.models.Connection.State.CONNECTION_RESPONSE_SENT class ConnectionSteps { @@ -73,27 +76,22 @@ class ConnectionSteps { @When("{actor} receives the connection request and sends back the response") fun inviterReceivesTheConnectionRequest(inviter: Actor) { - 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 - } + inviter.attemptsTo( + PollingWait.until( + ListenToEvents.connectionState(inviter), + CoreMatchers.equalTo(CONNECTION_RESPONSE_SENT), + ), + ) } @When("{actor} receives the connection response") fun inviteeReceivesTheConnectionResponse(invitee: Actor) { - 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 - } + invitee.attemptsTo( + PollingWait.until( + ListenToEvents.connectionState(invitee), + CoreMatchers.equalTo(CONNECTION_RESPONSE_RECEIVED), + ), + ) } @Then("{actor} and {actor} have a connection") @@ -120,8 +118,8 @@ class ConnectionSteps { assertThat(inviter.recall("connection-with-${invitee.name}").theirDid) .isEqualTo(invitee.recall("connection-with-${inviter.name}").myDid) assertThat(inviter.recall("connection-with-${invitee.name}").state) - .isEqualTo(Connection.State.CONNECTION_RESPONSE_SENT) + .isEqualTo(CONNECTION_RESPONSE_SENT) assertThat(invitee.recall("connection-with-${inviter.name}").state) - .isEqualTo(Connection.State.CONNECTION_RESPONSE_RECEIVED) + .isEqualTo(CONNECTION_RESPONSE_RECEIVED) } } diff --git a/tests/integration-tests/src/test/kotlin/steps/credentials/AnoncredSteps.kt b/tests/integration-tests/src/test/kotlin/steps/credentials/AnoncredSteps.kt new file mode 100644 index 0000000000..2cddddb2b6 --- /dev/null +++ b/tests/integration-tests/src/test/kotlin/steps/credentials/AnoncredSteps.kt @@ -0,0 +1,50 @@ +package steps.credentials + +import interactions.Post +import interactions.body +import io.cucumber.java.en.When +import io.iohk.atala.automation.extensions.get +import io.iohk.atala.automation.serenity.ensure.Ensure +import net.serenitybdd.rest.SerenityRest +import net.serenitybdd.screenplay.Actor +import org.apache.http.HttpStatus.SC_CREATED +import org.apache.http.HttpStatus.SC_OK +import org.hyperledger.identus.client.models.* + +class AnoncredSteps { + + @When("{actor} accepts anoncred credential offer") + fun holderAcceptsCredentialOfferForAnoncred(holder: Actor) { + val recordId = holder.recall("recordId") + holder.attemptsTo( + Post.to("/issue-credentials/records/$recordId/accept-offer").body("{}"), + Ensure.thatTheLastResponse().statusCode().isEqualTo(SC_OK), + ) + } + + @When("{actor} offers anoncred to {actor}") + fun acmeOffersAnoncredToBob(issuer: Actor, holder: Actor) { + val credentialOfferRequest = CreateIssueCredentialRecordRequest( + credentialDefinitionId = issuer.recall("anoncredsCredentialDefinition").guid, + claims = linkedMapOf( + "name" to "Bob", + "age" to "21", + "sex" to "M", + ), + issuingDID = issuer.recall("shortFormDid"), + connectionId = issuer.recall("connection-with-${holder.name}").connectionId, + validityPeriod = 3600.0, + credentialFormat = "AnonCreds", + automaticIssuance = false, + ) + + issuer.attemptsTo( + Post.to("/issue-credentials/credential-offers").body(credentialOfferRequest), + Ensure.thatTheLastResponse().statusCode().isEqualTo(SC_CREATED), + ) + + val credentialRecord = SerenityRest.lastResponse().get() + issuer.remember("thid", credentialRecord.thid) + holder.remember("thid", credentialRecord.thid) + } +} diff --git a/tests/integration-tests/src/test/kotlin/steps/credentials/CredentialSteps.kt b/tests/integration-tests/src/test/kotlin/steps/credentials/CredentialSteps.kt new file mode 100644 index 0000000000..7c2251ec83 --- /dev/null +++ b/tests/integration-tests/src/test/kotlin/steps/credentials/CredentialSteps.kt @@ -0,0 +1,79 @@ +package steps.credentials + +import abilities.ListenToEvents +import interactions.Post +import io.cucumber.java.en.Then +import io.cucumber.java.en.When +import io.iohk.atala.automation.serenity.ensure.Ensure +import io.iohk.atala.automation.serenity.interactions.PollingWait +import net.serenitybdd.screenplay.Actor +import org.apache.http.HttpStatus.* +import org.hamcrest.CoreMatchers.equalTo +import org.hyperledger.identus.client.models.* +import org.hyperledger.identus.client.models.IssueCredentialRecord.ProtocolState.* + +class CredentialSteps { + + @When("{actor} receives the credential offer") + fun holderReceivesCredentialOffer(holder: Actor) { + holder.attemptsTo( + PollingWait.until( + ListenToEvents.credentialState(holder), + equalTo(OFFER_RECEIVED), + ), + ) + val recordId = ListenToEvents.with(holder).credentialEvents.last().data.recordId + holder.remember("recordId", recordId) + } + + @When("{actor} tries to issue the credential") + fun issuerTriesToIssueTheCredential(issuer: Actor) { + issuer.attemptsTo( + PollingWait.until( + ListenToEvents.credentialState(issuer), + equalTo(REQUEST_RECEIVED), + ), + ) + + val recordId = ListenToEvents.with(issuer).credentialEvents.last().data.recordId + + issuer.attemptsTo( + Post.to("/issue-credentials/records/$recordId/issue-credential"), + ) + } + + @When("{actor} issues the credential") + fun issuerIssuesTheCredential(issuer: Actor) { + issuerTriesToIssueTheCredential(issuer) + + issuer.attemptsTo( + Ensure.thatTheLastResponse().statusCode().isEqualTo(SC_OK), + ) + + issuer.attemptsTo( + PollingWait.until( + ListenToEvents.credentialState(issuer), + equalTo(CREDENTIAL_SENT), + ), + ) + issuer.remember("issuedCredential", ListenToEvents.with(issuer).credentialEvents.last().data) + } + + @Then("{actor} receives the issued credential") + fun holderReceivesTheIssuedCredential(holder: Actor) { + holder.attemptsTo( + PollingWait.until( + ListenToEvents.credentialState(holder), + equalTo(CREDENTIAL_RECEIVED), + ), + ) + holder.remember("issuedCredential", ListenToEvents.with(holder).credentialEvents.last().data) + } + + @Then("{actor} should see that credential issuance has failed") + fun issuerShouldSeeThatCredentialIssuanceHasFailed(issuer: Actor) { + issuer.attemptsTo( + Ensure.thatTheLastResponse().statusCode().isEqualTo(SC_UNPROCESSABLE_ENTITY), + ) + } +} diff --git a/tests/integration-tests/src/test/kotlin/steps/credentials/IssueCredentialsSteps.kt b/tests/integration-tests/src/test/kotlin/steps/credentials/IssueCredentialsSteps.kt deleted file mode 100644 index 1688e3a585..0000000000 --- a/tests/integration-tests/src/test/kotlin/steps/credentials/IssueCredentialsSteps.kt +++ /dev/null @@ -1,242 +0,0 @@ -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 -import io.iohk.atala.automation.serenity.ensure.Ensure -import io.iohk.atala.automation.utils.Wait -import models.CredentialEvent -import net.serenitybdd.rest.SerenityRest -import net.serenitybdd.screenplay.Actor -import org.apache.http.HttpStatus.* -import org.hyperledger.identus.client.models.* -import kotlin.time.Duration.Companion.seconds - -class IssueCredentialsSteps { - - private var credentialEvent: CredentialEvent? = null - - private fun sendCredentialOffer( - issuer: Actor, - holder: Actor, - didForm: String, - schemaGuid: String?, - claims: Map, - ) { - val did: String = if (didForm == "short") { - issuer.recall("shortFormDid") - } else { - issuer.recall("longFormDid") - } - - val schemaId: String? = if (schemaGuid != null) { - val baseUrl = issuer.recall("baseUrl") - "$baseUrl/schema-registry/schemas/$schemaGuid" - } else { - null - } - - val credentialOfferRequest = CreateIssueCredentialRecordRequest( - schemaId = schemaId, - claims = claims, - issuingDID = did, - connectionId = issuer.recall("connection-with-${holder.name}").connectionId, - validityPeriod = 3600.0, - credentialFormat = "JWT", - automaticIssuance = false, - ) - - issuer.attemptsTo( - Post.to("/issue-credentials/credential-offers").body(credentialOfferRequest), - ) - } - - private fun saveCredentialOffer(issuer: Actor, holder: Actor) { - val credentialRecord = SerenityRest.lastResponse().get() - - issuer.attemptsTo( - Ensure.thatTheLastResponse().statusCode().isEqualTo(SC_CREATED), - ) - - issuer.remember("thid", credentialRecord.thid) - holder.remember("thid", credentialRecord.thid) - } - - @When("{actor} offers a credential to {actor} with {string} form DID") - fun issuerOffersACredential(issuer: Actor, holder: Actor, format: String) { - val claims = linkedMapOf( - "firstName" to "FirstName", - "lastName" to "LastName", - ) - sendCredentialOffer(issuer, holder, format, null, claims) - saveCredentialOffer(issuer, holder) - } - - @When("{actor} offers a credential to {actor} with {} form using {} schema") - fun issuerOffersCredentialToHolderUsingSchema( - issuer: Actor, - holder: Actor, - format: String, - schema: CredentialSchema, - ) { - val schemaGuid = issuer.recall(schema.name) - val claims = schema.claims - sendCredentialOffer(issuer, holder, format, schemaGuid, claims) - saveCredentialOffer(issuer, holder) - } - - @When("{actor} offers a credential to {actor} with {} form DID with wrong claims structure using {} schema") - fun issuerOffersCredentialToHolderWithWrongClaimStructure( - issuer: Actor, - holder: Actor, - format: String, - schema: CredentialSchema, - ) { - val schemaGuid = issuer.recall(schema.name)!! - val claims = linkedMapOf( - "name" to "Name", - "surname" to "Surname", - ) - sendCredentialOffer(issuer, holder, format, schemaGuid, claims) - } - - @When("{actor} offers anoncred to {actor}") - fun acmeOffersAnoncredToBob(issuer: Actor, holder: Actor) { - val credentialOfferRequest = CreateIssueCredentialRecordRequest( - credentialDefinitionId = issuer.recall("anoncredsCredentialDefinition").guid, - claims = linkedMapOf( - "name" to "Bob", - "age" to "21", - "sex" to "M", - ), - issuingDID = issuer.recall("shortFormDid"), - connectionId = issuer.recall("connection-with-${holder.name}").connectionId, - validityPeriod = 3600.0, - credentialFormat = "AnonCreds", - automaticIssuance = false, - ) - - issuer.attemptsTo( - Post.to("/issue-credentials/credential-offers") - .with { - it.body(credentialOfferRequest) - }, - ) - - val credentialRecord = SerenityRest.lastResponse().get() - - issuer.attemptsTo( - Ensure.thatTheLastResponse().statusCode().isEqualTo(SC_CREATED), - ) - - issuer.remember("thid", credentialRecord.thid) - holder.remember("thid", credentialRecord.thid) - } - - @When("{actor} receives the credential offer") - fun holderReceivesCredentialOffer(holder: Actor) { - 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.with(holder).credentialEvents.last().data.recordId - holder.remember("recordId", recordId) - } - - @When("{actor} accepts credential offer for JWT") - fun holderAcceptsCredentialOfferForJwt(holder: Actor) { - holder.attemptsTo( - Post.to("/issue-credentials/records/${holder.recall("recordId")}/accept-offer") - .with { - it.body( - AcceptCredentialOfferRequest(holder.recall("longFormDid")), - ) - }, - ) - holder.attemptsTo( - Ensure.thatTheLastResponse().statusCode().isEqualTo(SC_OK), - ) - } - - @When("{actor} accepts credential offer for anoncred") - fun holderAcceptsCredentialOfferForAnoncred(holder: Actor) { - holder.attemptsTo( - Post.to("/issue-credentials/records/${holder.recall("recordId")}/accept-offer") - .with { - it.body( - "{}", - ) - }, - ) - holder.attemptsTo( - Ensure.thatTheLastResponse().statusCode().isEqualTo(SC_OK), - ) - } - - @When("{actor} issues the credential") - fun acmeIssuesTheCredential(issuer: Actor) { - 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"), - ) - issuer.attemptsTo( - Ensure.thatTheLastResponse().statusCode().isEqualTo(SC_OK), - ) - - Wait.until( - 10.seconds, - 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.until( - errorMessage = "Holder was unable to receive the credential from Issuer! " + - "Protocol state did not achieve ${IssueCredentialRecord.ProtocolState.CREDENTIAL_RECEIVED} state.", - ) { - 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") - fun issuerShouldSeeThatCredentialIssuanceHasFailed(issuer: Actor) { - issuer.attemptsTo( - Ensure.thatTheLastResponse().statusCode().isEqualTo(SC_UNPROCESSABLE_ENTITY), - ) - } -} diff --git a/tests/integration-tests/src/test/kotlin/steps/credentials/JwtCredentialSteps.kt b/tests/integration-tests/src/test/kotlin/steps/credentials/JwtCredentialSteps.kt new file mode 100644 index 0000000000..b5026a7163 --- /dev/null +++ b/tests/integration-tests/src/test/kotlin/steps/credentials/JwtCredentialSteps.kt @@ -0,0 +1,110 @@ +package steps.credentials + +import common.CredentialSchema +import interactions.Post +import interactions.body +import io.cucumber.java.en.When +import io.iohk.atala.automation.extensions.get +import io.iohk.atala.automation.serenity.ensure.Ensure +import net.serenitybdd.rest.SerenityRest +import net.serenitybdd.screenplay.Actor +import org.apache.http.HttpStatus.SC_CREATED +import org.apache.http.HttpStatus.SC_OK +import org.hyperledger.identus.client.models.* + +class JwtCredentialSteps { + + private fun sendCredentialOffer( + issuer: Actor, + holder: Actor, + didForm: String, + schemaGuid: String?, + claims: Map, + ) { + val did: String = if (didForm == "short") { + issuer.recall("shortFormDid") + } else { + issuer.recall("longFormDid") + } + + val schemaId: String? = if (schemaGuid != null) { + val baseUrl = issuer.recall("baseUrl") + "$baseUrl/schema-registry/schemas/$schemaGuid" + } else { + null + } + + val credentialOfferRequest = CreateIssueCredentialRecordRequest( + schemaId = schemaId, + claims = claims, + issuingDID = did, + connectionId = issuer.recall("connection-with-${holder.name}").connectionId, + validityPeriod = 3600.0, + credentialFormat = "JWT", + automaticIssuance = false, + ) + + issuer.attemptsTo( + Post.to("/issue-credentials/credential-offers").body(credentialOfferRequest), + ) + } + + private fun saveCredentialOffer(issuer: Actor, holder: Actor) { + val credentialRecord = SerenityRest.lastResponse().get() + + issuer.attemptsTo( + Ensure.thatTheLastResponse().statusCode().isEqualTo(SC_CREATED), + ) + + issuer.remember("thid", credentialRecord.thid) + holder.remember("thid", credentialRecord.thid) + } + + @When("{actor} offers a jwt credential to {actor} with {string} form DID") + fun issuerOffersAJwtCredential(issuer: Actor, holder: Actor, format: String) { + val claims = linkedMapOf( + "firstName" to "FirstName", + "lastName" to "LastName", + ) + sendCredentialOffer(issuer, holder, format, null, claims) + saveCredentialOffer(issuer, holder) + } + + @When("{actor} offers a jwt credential to {actor} with {} form using {} schema") + fun issuerOffersJwtCredentialToHolderUsingSchema( + issuer: Actor, + holder: Actor, + format: String, + schema: CredentialSchema, + ) { + val schemaGuid = issuer.recall(schema.name) + val claims = schema.claims + sendCredentialOffer(issuer, holder, format, schemaGuid, claims) + saveCredentialOffer(issuer, holder) + } + + @When("{actor} offers a jwt credential to {actor} with {} form DID with wrong claims structure using {} schema") + fun issuerOffersJwtCredentialToHolderWithWrongClaimStructure( + issuer: Actor, + holder: Actor, + format: String, + schema: CredentialSchema, + ) { + val schemaGuid = issuer.recall(schema.name)!! + val claims = linkedMapOf( + "name" to "Name", + "surname" to "Surname", + ) + sendCredentialOffer(issuer, holder, format, schemaGuid, claims) + } + + @When("{actor} accepts jwt credential offer") + fun holderAcceptsJwtCredentialOfferForJwt(holder: Actor) { + val recordId = holder.recall("recordId") + holder.attemptsTo( + Post.to("/issue-credentials/records/$recordId/accept-offer") + .body(AcceptCredentialOfferRequest(holder.recall("longFormDid"))), + Ensure.thatTheLastResponse().statusCode().isEqualTo(SC_OK), + ) + } +} 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 ab8d16d289..3e3d539346 100644 --- a/tests/integration-tests/src/test/kotlin/steps/credentials/RevokeCredentialSteps.kt +++ b/tests/integration-tests/src/test/kotlin/steps/credentials/RevokeCredentialSteps.kt @@ -6,15 +6,19 @@ 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 io.iohk.atala.automation.serenity.interactions.PollingWait import models.JwtCredential import net.serenitybdd.rest.SerenityRest import net.serenitybdd.screenplay.Actor +import net.serenitybdd.screenplay.Question import org.apache.http.HttpStatus +import org.hamcrest.CoreMatchers.equalTo import org.hyperledger.identus.client.models.IssueCredentialRecord -import kotlin.time.Duration.Companion.seconds +import kotlin.time.Duration.Companion.milliseconds +import kotlin.time.Duration.Companion.minutes class RevokeCredentialSteps { + @When("{actor} revokes the credential issued to {actor}") fun issuerRevokesCredentialsIssuedToHolder(issuer: Actor, holder: Actor) { val issuedCredential = issuer.recall("issuedCredential") @@ -50,18 +54,20 @@ class RevokeCredentialSteps { @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") - actualEncodedList != encodedStatusList - } + issuer.attemptsTo( + PollingWait.with(1.minutes, 500.milliseconds).until( + Question.about("revocation status list").answeredBy { + 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") + actualEncodedList != encodedStatusList + }, + equalTo(true), + ), + ) } @Then("{actor} should see the credential is not revoked") diff --git a/tests/integration-tests/src/test/kotlin/steps/credentials/SdJwtCredentialSteps.kt b/tests/integration-tests/src/test/kotlin/steps/credentials/SdJwtCredentialSteps.kt new file mode 100644 index 0000000000..5bdf8bcf48 --- /dev/null +++ b/tests/integration-tests/src/test/kotlin/steps/credentials/SdJwtCredentialSteps.kt @@ -0,0 +1,137 @@ +package steps.credentials + +import com.google.gson.Gson +import com.nimbusds.jose.util.Base64URL +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 +import io.iohk.atala.automation.serenity.ensure.Ensure +import models.JwtCredential +import models.SdJwtClaim +import net.serenitybdd.rest.SerenityRest +import net.serenitybdd.screenplay.Actor +import org.apache.http.HttpStatus.* +import org.hyperledger.identus.client.models.* + +class SdJwtCredentialSteps { + + private val claims = linkedMapOf( + "firstName" to "Automation", + "lastName" to "Execution", + ) + + @When("{actor} offers a sd-jwt credential to {actor}") + fun issuerOffersSdJwtCredentialToHolder(issuer: Actor, holder: Actor) { + val connectionId = issuer.recall("connection-with-${holder.name}").connectionId + val did = issuer.recall("shortFormDid") + + val credentialOfferRequest = CreateIssueCredentialRecordRequest( + claims = claims, + issuingDID = did, + connectionId = connectionId, + validityPeriod = 3600.0, + credentialFormat = "SDJWT", + automaticIssuance = false, + ) + + issuer.attemptsTo( + Post.to("/issue-credentials/credential-offers").body(credentialOfferRequest), + Ensure.thatTheLastResponse().statusCode().isEqualTo(SC_CREATED), + ) + + val credentialRecord = SerenityRest.lastResponse().get() + issuer.remember("thid", credentialRecord.thid) + holder.remember("thid", credentialRecord.thid) + } + + @When("{actor} accepts credential offer for sd-jwt") + fun holderAcceptsSdJwtCredentialOffer(holder: Actor) { + holderAcceptsSdJwtCredentialOfferWithKeyBinding(holder, null) + } + + @When("{actor} accepts credential offer for sd-jwt with '{}' key binding") + fun holderAcceptsSdJwtCredentialOfferWithKeyBinding(holder: Actor, key: String?) { + val recordId = holder.recall("recordId") + val did = holder.recall("longFormDid") + val request = AcceptCredentialOfferRequest(subjectId = did, keyId = key) + holder.attemptsTo( + Post.to("/issue-credentials/records/$recordId/accept-offer").body(request), + Ensure.thatTheLastResponse().statusCode().isEqualTo(SC_OK), + ) + } + + @When("{actor} tries to offer a sd-jwt credential to {actor}") + fun issuerTriesToOfferSdJwtCredentialToHolder(issuer: Actor, holder: Actor) { + val connectionId = issuer.recall("connection-with-${holder.name}").connectionId + val did = issuer.recall("shortFormDid") + + val credentialOfferRequest = CreateIssueCredentialRecordRequest( + claims = claims, + issuingDID = did, + connectionId = connectionId, + validityPeriod = 3600.0, + credentialFormat = "SDJWT", + automaticIssuance = false, + ) + + issuer.attemptsTo( + Post.to("/issue-credentials/credential-offers").body(credentialOfferRequest), + ) + } + + @Then("{actor} checks the sd-jwt credential contents") + fun holderChecksTheSdJwtCredentialContents(holder: Actor) { + commonValidation(holder) { payload, _ -> + holder.attemptsTo( + Ensure.that(payload.containsKey("cnf")).isFalse(), + ) + } + } + + @Then("{actor} checks the sd-jwt credential contents with holder binding") + fun holderChecksTheSdJwtCredentialContentsWithHolderBinding(holder: Actor) { + commonValidation(holder) { payload, _ -> + holder.attemptsTo( + Ensure.that(payload.containsKey("cnf")).isTrue(), + ) + } + } + + @Then("{actor} should see the issuance has failed") + fun issuerShouldSeeTheIssuanceHasFailed(issuer: Actor) { + issuer.attemptsTo( + Ensure.thatTheLastResponse().statusCode().isEqualTo(SC_EXPECTATION_FAILED), + ) + } + + private fun commonValidation( + holder: Actor, + additionalChecks: (payload: Map, disclosedClaims: Map) -> Unit, + ) { + val issuedCredential = holder.recall("issuedCredential") + val jwtCredential = JwtCredential.parseBase64(issuedCredential.credential!!) + + val payload = jwtCredential.payload!!.toJSONObject() + + val disclosedClaims = jwtCredential.signature!!.toString().split("~") + .drop(1) + .dropLastWhile { it.isBlank() } + .map { Base64URL.from(it).decodeToString() } + .map { Gson().fromJson(it, Array::class.java) } + .associate { it[1] to SdJwtClaim(salt = it[0], key = it[1], value = it[2]) } + + holder.attemptsTo( + Ensure.that(payload.containsKey("_sd")).isTrue(), + Ensure.that(payload.containsKey("_sd_alg")).isTrue(), + Ensure.that(payload.containsKey("iss")).isTrue(), + Ensure.that(payload.containsKey("iat")).isTrue(), + Ensure.that(payload.containsKey("exp")).isTrue(), + Ensure.that(disclosedClaims["firstName"]!!.value).isEqualTo(claims["firstName"]!!), + Ensure.that(disclosedClaims["lastName"]!!.value).isEqualTo(claims["lastName"]!!), + ) + + additionalChecks(payload, disclosedClaims) + } +} 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 52a46869f5..5a4ab0c385 100644 --- a/tests/integration-tests/src/test/kotlin/steps/did/DeactivateDidSteps.kt +++ b/tests/integration-tests/src/test/kotlin/steps/did/DeactivateDidSteps.kt @@ -1,17 +1,17 @@ package steps.did -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.matchers.RestAssuredJsonProperty import io.iohk.atala.automation.serenity.ensure.Ensure -import io.iohk.atala.automation.utils.Wait +import io.iohk.atala.automation.serenity.interactions.PollingWait +import io.iohk.atala.automation.serenity.questions.HttpRequest import net.serenitybdd.rest.SerenityRest import net.serenitybdd.screenplay.Actor import org.apache.http.HttpStatus import org.hyperledger.identus.client.models.DIDOperationResponse -import org.hyperledger.identus.client.models.DIDResolutionResult class DeactivateDidSteps { @@ -37,9 +37,11 @@ class DeactivateDidSteps { @Then("{actor} sees that PRISM DID is successfully deactivated") fun actorSeesThatPrismDidIsSuccessfullyDeactivated(actor: Actor) { 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!! - } + actor.attemptsTo( + PollingWait.until( + HttpRequest.get("/dids/$deactivatedDid"), + RestAssuredJsonProperty.toBe("didDocumentMetadata.deactivated", "true"), + ), + ) } } 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 cd4302d194..d7c3addafb 100644 --- a/tests/integration-tests/src/test/kotlin/steps/did/PublishDidSteps.kt +++ b/tests/integration-tests/src/test/kotlin/steps/did/PublishDidSteps.kt @@ -8,14 +8,14 @@ import interactions.body 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 io.iohk.atala.automation.serenity.interactions.PollingWait 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.hamcrest.CoreMatchers.equalTo import org.hyperledger.identus.client.models.* -import kotlin.time.Duration.Companion.seconds class PublishDidSteps { @@ -46,7 +46,7 @@ class PublishDidSteps { @Given("{actor} creates unpublished DID") fun agentCreatesEmptyUnpublishedDid(actor: Actor) { - agentCreatesUnpublishedDid(actor, DidPurpose.EMPTY) + agentCreatesUnpublishedDid(actor, DidPurpose.CUSTOM) } @Given("{actor} creates unpublished DID for {}") @@ -75,6 +75,24 @@ class PublishDidSteps { actor.forget("hasPublishedDid") } + @When("{actor} prepares a custom PRISM DID") + fun actorPreparesCustomDid(actor: Actor) { + val customDid = DidPurpose.CUSTOM + actor.remember("customDid", customDid) + } + + @When("{actor} adds a '{curve}' key for '{purpose}' purpose with '{}' name to the custom PRISM DID") + fun actorAddsKeyToCustomDid(actor: Actor, curve: Curve, purpose: Purpose, name: String) { + val customDid = actor.recall("customDid") + customDid.publicKeys.add(ManagedDIDKeyTemplate(name, purpose, curve)) + } + + @When("{actor} creates the custom PRISM DID") + fun actorCreatesTheCustomPrismDid(actor: Actor) { + val customDid = actor.recall("customDid") + agentCreatesUnpublishedDid(actor, customDid) + } + @When("{actor} publishes DID to ledger") fun hePublishesDidToLedger(actor: Actor) { val shortFormDid = actor.recall("shortFormDid") @@ -87,18 +105,7 @@ class PublishDidSteps { Ensure.thatTheLastResponse().statusCode().isEqualTo(HttpStatus.SC_ACCEPTED), Ensure.that(didOperationResponse.scheduledOperation.didRef).isNotEmpty(), Ensure.that(didOperationResponse.scheduledOperation.id).isNotEmpty(), - ) - - 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( + PollingWait.until(ListenToEvents.didStatus(actor), equalTo("PUBLISHED")), 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 b2a73bf072..e9e590d319 100644 --- a/tests/integration-tests/src/test/kotlin/steps/did/UpdateDidSteps.kt +++ b/tests/integration-tests/src/test/kotlin/steps/did/UpdateDidSteps.kt @@ -7,10 +7,12 @@ 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 io.iohk.atala.automation.serenity.interactions.PollingWait import net.serenitybdd.rest.SerenityRest import net.serenitybdd.screenplay.Actor +import net.serenitybdd.screenplay.Question import org.apache.http.HttpStatus +import org.hamcrest.CoreMatchers.equalTo import org.hyperledger.identus.client.models.* import java.util.UUID @@ -87,91 +89,111 @@ class UpdateDidSteps { fun actorSeesDidSuccessfullyUpdatedWithNewKeys(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 didKey = "${actor.recall("shortFormDid")}#$newDidKeyId" - val didDocument = SerenityRest.lastResponse().get().didDocument!! - val foundVerificationMethod = didDocument.verificationMethod!!.map { it.id }.any { it == didKey } - - foundVerificationMethod && when (purpose) { - Purpose.ASSERTION_METHOD -> didDocument.assertionMethod!!.any { it == didKey } - Purpose.AUTHENTICATION -> didDocument.authentication!!.any { it == didKey } - Purpose.CAPABILITY_DELEGATION -> didDocument.capabilityDelegation!!.any { it == didKey } - Purpose.CAPABILITY_INVOCATION -> didDocument.capabilityInvocation!!.any { it == didKey } - Purpose.KEY_AGREEMENT -> didDocument.keyAgreement!!.any { it == didKey } - } - } + actor.attemptsTo( + PollingWait.until( + Question.about("did update").answeredBy { + actor.attemptsTo( + Get.resource("/dids/${actor.recall("shortFormDid")}"), + ) + val didKey = "${actor.recall("shortFormDid")}#$newDidKeyId" + val didDocument = SerenityRest.lastResponse().get().didDocument!! + val foundVerificationMethod = didDocument.verificationMethod!!.map { it.id }.any { it == didKey } + + foundVerificationMethod && when (purpose) { + Purpose.ASSERTION_METHOD -> didDocument.assertionMethod!!.any { it == didKey } + Purpose.AUTHENTICATION -> didDocument.authentication!!.any { it == didKey } + Purpose.CAPABILITY_DELEGATION -> didDocument.capabilityDelegation!!.any { it == didKey } + Purpose.CAPABILITY_INVOCATION -> didDocument.capabilityInvocation!!.any { it == didKey } + Purpose.KEY_AGREEMENT -> didDocument.keyAgreement!!.any { it == didKey } + } + }, + equalTo(true), + ), + ) } @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 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 } - } - } + + actor.attemptsTo( + PollingWait.until( + Question.about("did update").answeredBy { + actor.attemptsTo( + Get.resource("/dids/${actor.recall("shortFormDid")}"), + ) + 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 } + } + }, + equalTo(true), + ), + ) } @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!", - ) { - actor.attemptsTo( - Get.resource("/dids/${actor.recall("shortFormDid")}"), - ) - val serviceIds = - SerenityRest.lastResponse().get().didDocument!!.service!!.map { it.id } - serviceIds.any { - it == "${actor.recall("shortFormDid")}#$serviceId" - } - } + actor.attemptsTo( + PollingWait.until( + Question.about("did update").answeredBy { + actor.attemptsTo( + Get.resource("/dids/${actor.recall("shortFormDid")}"), + ) + val serviceIds = + SerenityRest.lastResponse().get().didDocument!!.service!!.map { it.id } + serviceIds.any { + it == "${actor.recall("shortFormDid")}#$serviceId" + } + }, + equalTo(true), + ), + ) } @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!", - ) { - actor.attemptsTo( - Get.resource("/dids/${actor.recall("shortFormDid")}"), - ) - val serviceIds = - SerenityRest.lastResponse().get().didDocument!!.service!!.map { it.id } - serviceIds.none { - it == "${actor.recall("shortFormDid")}#$serviceId" - } - } + actor.attemptsTo( + PollingWait.until( + Question.about("did update").answeredBy { + actor.attemptsTo( + Get.resource("/dids/${actor.recall("shortFormDid")}"), + ) + val serviceIds = + SerenityRest.lastResponse().get().didDocument!!.service!!.map { it.id } + serviceIds.none { + it == "${actor.recall("shortFormDid")}#$serviceId" + } + }, + equalTo(true), + ), + ) } @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!", - ) { - actor.attemptsTo( - Get.resource("/dids/${actor.recall("shortFormDid")}"), - ) - val service = SerenityRest.lastResponse().get().didDocument!!.service!! - service.any { it.serviceEndpoint.value.contains(serviceUrl) } - } + actor.attemptsTo( + PollingWait.until( + Question.about("did update").answeredBy { + actor.attemptsTo( + Get.resource("/dids/${actor.recall("shortFormDid")}"), + ) + val service = SerenityRest.lastResponse().get().didDocument!!.service!! + service.any { it.serviceEndpoint.value.contains(serviceUrl) } + }, + equalTo(true), + ), + ) } private fun actorSubmitsPrismDidUpdateOperation(actor: Actor, updatePrismDidAction: UpdateManagedDIDRequestAction) { diff --git a/tests/integration-tests/src/test/kotlin/steps/proofs/AnoncredsPresentProofSteps.kt b/tests/integration-tests/src/test/kotlin/steps/proofs/AnoncredProofSteps.kt similarity index 61% rename from tests/integration-tests/src/test/kotlin/steps/proofs/AnoncredsPresentProofSteps.kt rename to tests/integration-tests/src/test/kotlin/steps/proofs/AnoncredProofSteps.kt index ace6fc890e..e41618037c 100644 --- a/tests/integration-tests/src/test/kotlin/steps/proofs/AnoncredsPresentProofSteps.kt +++ b/tests/integration-tests/src/test/kotlin/steps/proofs/AnoncredProofSteps.kt @@ -1,26 +1,19 @@ package steps.proofs -import abilities.ListenToEvents -import interactions.Patch -import interactions.Post +import interactions.* 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 import org.apache.http.HttpStatus.SC_CREATED import org.hyperledger.identus.client.models.* -class AnoncredsPresentProofSteps { - - private var proofEvent: PresentationEvent? = null +class AnoncredProofSteps { @When("{actor} sends a anoncreds request for proof presentation to {actor} using credential definition issued by {actor}") - fun faberSendsAnAnoncredsRequestForProofPresentationToBob(faber: Actor, bob: Actor, issuer: Actor) { + fun verifierSendsAnAnoncredRequestForProofPresentationToHolder(verifier: Actor, holder: Actor, issuer: Actor) { val credentialDefinitionRegistryUrl = issuer.usingAbilityTo(CallAnApi::class.java) .resolve("/credential-definition-registry/definitions") @@ -54,49 +47,28 @@ class AnoncredsPresentProofSteps { version = "0.1", ) val presentationRequest = RequestPresentationInput( - connectionId = faber.recall("connection-with-${bob.name}").connectionId, + connectionId = verifier.recall("connection-with-${holder.name}").connectionId, credentialFormat = "AnonCreds", anoncredPresentationRequest = anoncredsPresentationRequestV1, proofs = emptyList(), ) - 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) - } - - @When("{actor} receives the anoncreds request") - fun bobReceivesTheAnoncredsRequest(bob: Actor) { - 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) + verifier.remember("thid", presentationStatus.thid) + holder.remember("thid", presentationStatus.thid) } @When("{actor} accepts the anoncreds presentation request") - fun bobAcceptsTheAnoncredsPresentationWithProof(bob: Actor) { + fun holderAcceptsTheAnoncredsPresentationWithProof(holder: Actor) { val requestPresentationAction = RequestPresentationAction( anoncredPresentationRequest = AnoncredCredentialProofsV1( listOf( AnoncredCredentialProofV1( - bob.recall("issuedCredential").recordId, + holder.recall("issuedCredential").recordId, listOf("sex"), listOf("age"), ), @@ -105,13 +77,9 @@ class AnoncredsPresentProofSteps { action = RequestPresentationAction.Action.REQUEST_MINUS_ACCEPT, ) - val presentationId = bob.recall("presentationId") - bob.attemptsTo( - Patch.to("/present-proof/presentations/$presentationId").with { - it.body( - requestPresentationAction, - ) - }, + val presentationId = holder.recall("presentationId") + holder.attemptsTo( + Patch.to("/present-proof/presentations/$presentationId").body(requestPresentationAction), ) } } diff --git a/tests/integration-tests/src/test/kotlin/steps/proofs/HolderProofSteps.kt b/tests/integration-tests/src/test/kotlin/steps/proofs/HolderProofSteps.kt new file mode 100644 index 0000000000..bdf3d078f7 --- /dev/null +++ b/tests/integration-tests/src/test/kotlin/steps/proofs/HolderProofSteps.kt @@ -0,0 +1,35 @@ +package steps.proofs + +import abilities.ListenToEvents +import interactions.* +import io.cucumber.java.en.When +import io.iohk.atala.automation.serenity.interactions.PollingWait +import models.PresentationStatusAdapter.Status.REQUEST_RECEIVED +import net.serenitybdd.screenplay.Actor +import org.hamcrest.CoreMatchers.equalTo +import org.hyperledger.identus.client.models.RequestPresentationAction + +class HolderProofSteps { + + @When("{actor} rejects the proof") + fun holderRejectsProof(holder: Actor) { + val presentationId: String = holder.recall("presentationId") + holder.attemptsTo( + Patch.to("/present-proof/presentations/$presentationId").body( + RequestPresentationAction(action = RequestPresentationAction.Action.REQUEST_MINUS_REJECT), + ), + ) + } + + @When("{actor} receives the presentation proof request") + fun holderReceivesTheRequest(holder: Actor) { + holder.attemptsTo( + PollingWait.until( + ListenToEvents.presentationProofStatus(holder), + equalTo(REQUEST_RECEIVED), + ), + ) + val presentationId = ListenToEvents.with(holder).presentationEvents.last().data.presentationId + holder.remember("presentationId", presentationId) + } +} diff --git a/tests/integration-tests/src/test/kotlin/steps/proofs/JwtProofSteps.kt b/tests/integration-tests/src/test/kotlin/steps/proofs/JwtProofSteps.kt new file mode 100644 index 0000000000..78985e62b0 --- /dev/null +++ b/tests/integration-tests/src/test/kotlin/steps/proofs/JwtProofSteps.kt @@ -0,0 +1,52 @@ +package steps.proofs + +import interactions.* +import io.cucumber.java.en.When +import io.iohk.atala.automation.extensions.get +import io.iohk.atala.automation.serenity.ensure.Ensure +import net.serenitybdd.rest.SerenityRest +import net.serenitybdd.screenplay.Actor +import org.apache.http.HttpStatus.SC_CREATED +import org.apache.http.HttpStatus.SC_OK +import org.hyperledger.identus.client.models.* + +class JwtProofSteps { + + @When("{actor} sends a request for jwt proof presentation to {actor}") + fun verifierSendsARequestForJwtProofPresentationToHolder(verifier: Actor, holder: Actor) { + val verifierConnectionToHolder = verifier.recall("connection-with-${holder.name}").connectionId + val presentationRequest = RequestPresentationInput( + connectionId = verifierConnectionToHolder, + options = Options( + challenge = "11c91493-01b3-4c4d-ac36-b336bab5bddf", + domain = "https://example-verifier.com", + ), + proofs = listOf( + ProofRequestAux( + schemaId = "https://schema.org/Person", + trustIssuers = listOf("did:web:atalaprism.io/users/testUser"), + ), + ), + ) + verifier.attemptsTo( + Post.to("/present-proof/presentations").body(presentationRequest), + Ensure.thatTheLastResponse().statusCode().isEqualTo(SC_CREATED), + ) + val presentationStatus = SerenityRest.lastResponse().get() + verifier.remember("thid", presentationStatus.thid) + holder.remember("thid", presentationStatus.thid) + } + + @When("{actor} makes the jwt presentation of the proof") + fun holderMakesThePresentationOfTheProofToVerifier(holder: Actor) { + val requestPresentationAction = RequestPresentationAction( + proofId = listOf(holder.recall("issuedCredential").recordId), + action = RequestPresentationAction.Action.REQUEST_MINUS_ACCEPT, + ) + val presentationId: String = holder.recall("presentationId") + holder.attemptsTo( + Patch.to("/present-proof/presentations/$presentationId").body(requestPresentationAction), + Ensure.thatTheLastResponse().statusCode().isEqualTo(SC_OK), + ) + } +} diff --git a/tests/integration-tests/src/test/kotlin/steps/proofs/PresentProofSteps.kt b/tests/integration-tests/src/test/kotlin/steps/proofs/PresentProofSteps.kt deleted file mode 100644 index b0599b7627..0000000000 --- a/tests/integration-tests/src/test/kotlin/steps/proofs/PresentProofSteps.kt +++ /dev/null @@ -1,123 +0,0 @@ -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 -import io.iohk.atala.automation.serenity.ensure.Ensure -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.* -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 verifierSendsARequestForProofPresentationToHolder(verifier: Actor, holder: Actor) { - val verifierConnectionToHolder = verifier.recall("connection-with-${holder.name}").connectionId - val presentationRequest = RequestPresentationInput( - connectionId = verifierConnectionToHolder, - options = Options( - challenge = "11c91493-01b3-4c4d-ac36-b336bab5bddf", - domain = "https://example-verifier.com", - ), - proofs = listOf( - ProofRequestAux( - schemaId = "https://schema.org/Person", - trustIssuers = listOf("did:web:atalaprism.io/users/testUser"), - ), - ), - ) - verifier.attemptsTo( - Post.to("/present-proof/presentations").body(presentationRequest), - Ensure.thatTheLastResponse().statusCode().isEqualTo(SC_CREATED), - ) - val presentationStatus = SerenityRest.lastResponse().get() - verifier.remember("thid", presentationStatus.thid) - holder.remember("thid", presentationStatus.thid) - } - - @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(holder).presentationEvents.lastOrNull { - it.data.thid == holder.recall("thid") - } - holder.remember("presentationId", proofEvent?.data?.presentationId) - proofEvent?.data?.status == PresentationStatusAdapter.Status.REQUEST_RECEIVED - } - } - - @When("{actor} makes the presentation of the proof") - fun holderMakesThePresentationOfTheProofToVerifier(holder: Actor) { - val requestPresentationAction = RequestPresentationAction( - proofId = listOf(holder.recall("issuedCredential").recordId), - action = RequestPresentationAction.Action.REQUEST_MINUS_ACCEPT, - ) - 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 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), - ) - }, - ) - } - - @Then("{actor} sees the proof is rejected") - fun bobSeesProofIsRejected(bob: Actor) { - 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.until( - timeout = 60.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 - } - } - - @Then("{actor} sees the proof returned verification failed") - fun verifierSeesTheProofReturnedVerificationFailed(verifier: Actor) { - Wait.until( - timeout = 120.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/proofs/SdJwtProofSteps.kt b/tests/integration-tests/src/test/kotlin/steps/proofs/SdJwtProofSteps.kt new file mode 100644 index 0000000000..41eee396b0 --- /dev/null +++ b/tests/integration-tests/src/test/kotlin/steps/proofs/SdJwtProofSteps.kt @@ -0,0 +1,99 @@ +package steps.proofs + +import abilities.ListenToEvents +import com.google.gson.Gson +import com.google.gson.JsonObject +import com.nimbusds.jose.util.Base64URL +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 models.JwtCredential +import models.SdJwtClaim +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.hyperledger.identus.client.models.* + +class SdJwtProofSteps { + + @When("{actor} sends a request for sd-jwt proof presentation to {actor} requesting [{}] claims") + fun verifierSendsARequestForSdJwtProofPresentationToHolder(verifier: Actor, holder: Actor, keys: String) { + val claims = JsonObject() + for (key in keys.split(",")) { + claims.addProperty(key, "{}") + } + val verifierConnectionToHolder = verifier.recall("connection-with-${holder.name}").connectionId + val presentationRequest = RequestPresentationInput( + connectionId = verifierConnectionToHolder, + options = Options( + challenge = "11c91493-01b3-4c4d-ac36-b336bab5bddf", + domain = "https://example-verifier.com", + ), + proofs = listOf(), + credentialFormat = "SDJWT", + claims = claims, + ) + verifier.attemptsTo( + Post.to("/present-proof/presentations").body(presentationRequest), + Ensure.thatTheLastResponse().statusCode().isEqualTo(SC_CREATED), + ) + val presentationStatus = SerenityRest.lastResponse().get() + verifier.remember("thid", presentationStatus.thid) + holder.remember("thid", presentationStatus.thid) + } + + @When("{actor} makes the sd-jwt presentation of the proof disclosing [{}] claims") + fun holderMakesThePresentationOfTheProofToVerifier(holder: Actor, keys: String) { + val claims = JsonObject() + for (key in keys.split(",")) { + claims.addProperty(key, "{}") + } + + val requestPresentationAction = RequestPresentationAction( + proofId = listOf(holder.recall("issuedCredential").recordId), + action = RequestPresentationAction.Action.REQUEST_MINUS_ACCEPT, + claims = claims, + credentialFormat = "SDJWT", + ) + + val presentationId: String = holder.recall("presentationId") + holder.attemptsTo( + Patch.to("/present-proof/presentations/$presentationId").body(requestPresentationAction), + Ensure.thatTheLastResponse().statusCode().isEqualTo(SC_OK), + ) + } + + @Then("{actor} has the sd-jwt proof verified") + fun verifierHasTheSdJwtProofVerified(verifier: Actor) { + VerifierProofSteps().verifierHasTheProofVerified(verifier) + val proofEvent = ListenToEvents.with(verifier).presentationEvents.last().data + val sdjwt = proofEvent.data!!.first() + val isBound = !sdjwt.endsWith("~") // if it ends with a ~ there's no binding + + val jwt = JwtCredential.parseJwt(sdjwt.split("~").first()) + val claims = sdjwt.split("~") + .drop(1) + .dropLast(if (isBound) 1 else 0) + .dropLastWhile { it.isBlank() } + .map { Base64URL.from(it).decodeToString() } + .map { Gson().fromJson(it, Array::class.java) } + .associate { it[1] to SdJwtClaim(salt = it[0], key = it[1], value = it[2]) } + + verifier.attemptsTo( + Ensure.that(claims.containsKey("firstName")).isTrue(), + Ensure.that(claims.containsKey("lastName")).isFalse(), + ) + + if (isBound) { + val bindingJwt = JwtCredential.parseJwt(sdjwt.split("~").last()) + val payload = Gson().toJsonTree(bindingJwt.payload!!.toJSONObject()).asJsonObject + + verifier.attemptsTo( + Ensure.that(payload.get("aud").asString).isEqualTo("https://example-verifier.com"), + ) + } + } +} diff --git a/tests/integration-tests/src/test/kotlin/steps/proofs/VerifierProofSteps.kt b/tests/integration-tests/src/test/kotlin/steps/proofs/VerifierProofSteps.kt new file mode 100644 index 0000000000..e99c103c83 --- /dev/null +++ b/tests/integration-tests/src/test/kotlin/steps/proofs/VerifierProofSteps.kt @@ -0,0 +1,32 @@ +package steps.proofs + +import abilities.ListenToEvents.Companion.presentationProofStatus +import io.cucumber.java.en.Then +import io.iohk.atala.automation.serenity.interactions.PollingWait +import models.PresentationStatusAdapter.Status.* +import net.serenitybdd.screenplay.Actor +import org.hamcrest.CoreMatchers.equalTo + +class VerifierProofSteps { + + @Then("{actor} sees the proof returned verification failed") + fun verifierSeesTheProofReturnedVerificationFailed(verifier: Actor) { + verifier.attemptsTo( + PollingWait.until(presentationProofStatus(verifier), equalTo(PRESENTATION_VERIFICATION_FAILED)), + ) + } + + @Then("{actor} sees the proof is rejected") + fun verifierSeesProofIsRejected(verifier: Actor) { + verifier.attemptsTo( + PollingWait.until(presentationProofStatus(verifier), equalTo(REQUEST_REJECTED)), + ) + } + + @Then("{actor} has the proof verified") + fun verifierHasTheProofVerified(verifier: Actor) { + verifier.attemptsTo( + PollingWait.until(presentationProofStatus(verifier), equalTo(PRESENTATION_VERIFIED)), + ) + } +} 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/credential/anoncred/issuance.feature similarity index 78% rename from tests/integration-tests/src/test/resources/features/credentials/issue_anoncred_with_published_did.feature rename to tests/integration-tests/src/test/resources/features/credential/anoncred/issuance.feature index 051e8df412..712d6d7db6 100644 --- a/tests/integration-tests/src/test/resources/features/credentials/issue_anoncred_with_published_did.feature +++ b/tests/integration-tests/src/test/resources/features/credential/anoncred/issuance.feature @@ -1,5 +1,5 @@ -@RFC0453 @AIP20 @credentials -Feature: Issue Anoncred with published DID +@anoncred @issuance +Feature: Issue Anoncred credential Background: Given Issuer and Holder have an existing connection @@ -10,6 +10,6 @@ Feature: Issue Anoncred with published 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 Holder accepts anoncred credential offer And Issuer issues the credential Then Holder receives the issued credential diff --git a/tests/integration-tests/src/test/resources/features/proofs/present_proof_anoncred.feature b/tests/integration-tests/src/test/resources/features/credential/anoncred/present_proof.feature similarity index 86% rename from tests/integration-tests/src/test/resources/features/proofs/present_proof_anoncred.feature rename to tests/integration-tests/src/test/resources/features/credential/anoncred/present_proof.feature index 77c52dc572..8bc05395c0 100644 --- a/tests/integration-tests/src/test/resources/features/proofs/present_proof_anoncred.feature +++ b/tests/integration-tests/src/test/resources/features/credential/anoncred/present_proof.feature @@ -1,4 +1,4 @@ -@proof @anoncreds +@anoncred @proof Feature: Present Proof Protocol Scenario: Holder presents anoncreds credential proof to verifier @@ -9,10 +9,10 @@ Scenario: Holder presents anoncreds credential proof to verifier 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 + And Holder accepts anoncred credential offer And Issuer issues the credential 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 receives the presentation proof request And Holder accepts the anoncreds presentation request # Then Verifier has the proof verified FIXME diff --git a/tests/integration-tests/src/test/resources/features/credential/jwt/issuance.feature b/tests/integration-tests/src/test/resources/features/credential/jwt/issuance.feature new file mode 100644 index 0000000000..2e9689f772 --- /dev/null +++ b/tests/integration-tests/src/test/resources/features/credential/jwt/issuance.feature @@ -0,0 +1,41 @@ +@jwt @issuance +Feature: Issue JWT credential + + Scenario: Issuing jwt credential with published PRISM DID + Given Issuer and Holder have an existing connection + And Issuer has a published DID for JWT + And Holder has an unpublished DID for JWT + When Issuer offers a jwt credential to Holder with "short" form DID + And Holder receives the credential offer + And Holder accepts jwt credential offer + And Issuer issues the credential + Then Holder receives the issued credential + + Scenario: Issuing jwt credential with a schema + Given Issuer and Holder have an existing connection + And Issuer has a published DID for JWT + And Issuer has published STUDENT_SCHEMA schema + And Holder has an unpublished DID for JWT + When Issuer offers a jwt credential to Holder with "short" form using STUDENT_SCHEMA schema + And Holder receives the credential offer + And Holder accepts jwt credential offer + And Issuer issues the credential + Then Holder receives the issued credential + + Scenario: Issuing jwt credential with wrong claim structure for schema + Given Issuer and Holder have an existing connection + And Issuer has a published DID for JWT + And Issuer has published STUDENT_SCHEMA schema + And Holder has an unpublished DID for JWT + When Issuer offers a jwt credential to Holder with "short" form DID with wrong claims structure using STUDENT_SCHEMA schema + Then Issuer should see that credential issuance has failed + + Scenario: Issuing jwt credential with unpublished PRISM DID + Given Issuer and Holder have an existing connection + And Issuer has an unpublished DID for JWT + And Holder has an unpublished DID for JWT + And Issuer offers a jwt credential to Holder with "long" form DID + And Holder receives the credential offer + And Holder accepts jwt credential offer + And Issuer issues the credential + Then Holder receives the issued credential diff --git a/tests/integration-tests/src/test/resources/features/proofs/present_proof.feature b/tests/integration-tests/src/test/resources/features/credential/jwt/present_proof.feature similarity index 60% rename from tests/integration-tests/src/test/resources/features/proofs/present_proof.feature rename to tests/integration-tests/src/test/resources/features/credential/jwt/present_proof.feature index c2fa51b80d..5b0f46564c 100644 --- a/tests/integration-tests/src/test/resources/features/proofs/present_proof.feature +++ b/tests/integration-tests/src/test/resources/features/credential/jwt/present_proof.feature @@ -1,26 +1,26 @@ -@proof @jwt +@jwt @proof Feature: Present Proof Protocol - Scenario: Holder presents credential proof to verifier + Scenario: Holder presents jwt credential proof to verifier Given Verifier and Holder have an existing connection And Holder has a jwt issued credential from Issuer - When Verifier sends a request for proof presentation to Holder + When Verifier sends a request for jwt proof presentation to Holder And Holder receives the presentation proof request - And Holder makes the presentation of the proof + And Holder makes the jwt presentation of the proof Then Verifier has the proof verified - Scenario: Holder presents proof to verifier which is the issuer itself + Scenario: Holder presents jwt 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 + When Issuer sends a request for jwt proof presentation to Holder And Holder receives the presentation proof request - And Holder makes the presentation of the proof + And Holder makes the jwt 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 a jwt issued credential from Issuer - When Verifier sends a request for proof presentation to Holder + When Verifier sends a request for jwt proof presentation to Holder And Holder receives the presentation proof request And Holder rejects the proof Then Holder sees the proof is rejected diff --git a/tests/integration-tests/src/test/resources/features/revocation/revoke_jwt_credential.feature b/tests/integration-tests/src/test/resources/features/credential/jwt/revocation.feature similarity index 55% rename from tests/integration-tests/src/test/resources/features/revocation/revoke_jwt_credential.feature rename to tests/integration-tests/src/test/resources/features/credential/jwt/revocation.feature index b60e7d7bf9..508380402d 100644 --- a/tests/integration-tests/src/test/resources/features/revocation/revoke_jwt_credential.feature +++ b/tests/integration-tests/src/test/resources/features/credential/jwt/revocation.feature @@ -1,21 +1,21 @@ -@revocation @jwt -Feature: Credential revocation - JWT +@jwt @revocation +Feature: JWT Credential revocation Background: Given Holder has a jwt issued credential from Issuer - Scenario: Revoke issued credential + Scenario: Revoke jwt 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 + When Issuer sends a request for jwt proof presentation to Holder And Holder receives the presentation proof request - And Holder makes the presentation of the proof + And Holder makes the jwt presentation of the proof Then Issuer sees the proof returned verification failed - Scenario: Holder tries to revoke credential from issuer + Scenario: Holder tries to revoke jwt credential from issuer When Holder tries to revoke credential from Issuer - And Issuer sends a request for proof presentation to Holder + And Issuer sends a request for jwt proof presentation to Holder And Holder receives the presentation proof request - And Holder makes the presentation of the proof + And Holder makes the jwt 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/credential/sdjwt/issuance.feature b/tests/integration-tests/src/test/resources/features/credential/sdjwt/issuance.feature new file mode 100644 index 0000000000..29af552150 --- /dev/null +++ b/tests/integration-tests/src/test/resources/features/credential/sdjwt/issuance.feature @@ -0,0 +1,37 @@ +@sdjwt @issuance +Feature: Issue SD-JWT credential + + Scenario: Issuing sd-jwt credential + Given Issuer and Holder have an existing connection + And Issuer has a published DID for SD_JWT + And Holder has an unpublished DID for SD_JWT + When Issuer offers a sd-jwt credential to Holder + And Holder receives the credential offer + And Holder accepts credential offer for sd-jwt + And Issuer issues the credential + Then Holder receives the issued credential + And Holder checks the sd-jwt credential contents + + Scenario: Issuing sd-jwt credential with holder binding + Given Issuer and Holder have an existing connection + And Issuer has a published DID for SD_JWT + And Holder has an unpublished DID for SD_JWT + When Issuer offers a sd-jwt credential to Holder + And Holder receives the credential offer + And Holder accepts credential offer for sd-jwt with 'auth-1' key binding + And Issuer issues the credential + Then Holder receives the issued credential + Then Holder checks the sd-jwt credential contents with holder binding + +# Scenario: Issuing sd-jwt with wrong algorithm +# Given Issuer and Holder have an existing connection +# When Issuer prepares a custom PRISM DID +# And Issuer adds a 'secp256k1' key for 'assertionMethod' purpose with 'assert-1' name to the custom PRISM DID +# And Issuer adds a 'secp256k1' key for 'authentication' purpose with 'auth-1' name to the custom PRISM DID +# And Issuer creates the custom PRISM DID +# And Holder has an unpublished DID for SD_JWT +# And Issuer offers a sd-jwt credential to Holder +# And Holder receives the credential offer +# And Holder accepts credential offer for sd-jwt +# And Issuer tries to issue the credential +# Then Issuer should see that credential issuance has failed diff --git a/tests/integration-tests/src/test/resources/features/credential/sdjwt/present_proof.feature b/tests/integration-tests/src/test/resources/features/credential/sdjwt/present_proof.feature new file mode 100644 index 0000000000..e5d273bf37 --- /dev/null +++ b/tests/integration-tests/src/test/resources/features/credential/sdjwt/present_proof.feature @@ -0,0 +1,35 @@ +@sdjwt @proof +Feature: Present SD-JWT Proof Protocol + + Scenario Outline: Holder presents sd-jwt proof to + Given and Holder have an existing connection + And Holder has a sd-jwt issued credential from Issuer + When sends a request for sd-jwt proof presentation to Holder requesting [firstName] claims + And Holder receives the presentation proof request + And Holder makes the sd-jwt presentation of the proof disclosing [firstName] claims + Then has the proof verified + Examples: + | verifier | + | Verifier | + | Issuer | + + Scenario Outline: Holder presents a bound sd-jwt proof to + Given and Holder have an existing connection + And Holder has a bound sd-jwt issued credential from Issuer + When sends a request for sd-jwt proof presentation to Holder requesting [firstName] claims + And Holder receives the presentation proof request + And Holder makes the sd-jwt presentation of the proof disclosing [firstName] claims + Then has the sd-jwt proof verified + Examples: + | verifier | + | Verifier | + | Issuer | + +# Scenario: Holder presents sd-jwt proof with different claims from requested +# Given Verifier and Holder have an existing connection +# And Holder has a bound sd-jwt issued credential from Issuer +# When Verifier sends a request for sd-jwt proof presentation to Holder requesting [firstName] claims +# And Holder receives the presentation proof request +# And Holder makes the sd-jwt presentation of the proof disclosing [lastName] claims +# Then Verifier sees the proof returned verification failed + diff --git a/tests/integration-tests/src/test/resources/features/credentials/issue_jwt_with_published_did.feature b/tests/integration-tests/src/test/resources/features/credentials/issue_jwt_with_published_did.feature deleted file mode 100644 index e54fd17c2d..0000000000 --- a/tests/integration-tests/src/test/resources/features/credentials/issue_jwt_with_published_did.feature +++ /dev/null @@ -1,34 +0,0 @@ -@RFC0453 @AIP20 @credentials -Feature: Issue JWT Credentials with published DID - - Background: - Given Issuer and Holder have an existing connection - And Issuer has a published DID for JWT - And Issuer has published STUDENT_SCHEMA schema - 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 - And Holder receives the credential offer - And Holder accepts credential offer for JWT - And Issuer issues the credential - Then Holder receives the issued credential - - 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 - - Scenario: Issuing credential with an existing schema - When Issuer offers a credential to Holder with "short" form using STUDENT_SCHEMA schema - And Holder receives the credential offer - And Holder accepts credential offer for JWT - And Issuer issues the credential - Then Holder receives the issued credential - - Scenario: Issuing credential with wrong claim structure for schema - When Issuer offers a credential to Holder with "short" form DID with wrong claims structure using STUDENT_SCHEMA schema - Then Issuer should see that credential issuance has failed diff --git a/tests/integration-tests/src/test/resources/features/credentials/issue_jwt_with_unpublished_did.feature b/tests/integration-tests/src/test/resources/features/credentials/issue_jwt_with_unpublished_did.feature deleted file mode 100644 index a658a0c453..0000000000 --- a/tests/integration-tests/src/test/resources/features/credentials/issue_jwt_with_unpublished_did.feature +++ /dev/null @@ -1,14 +0,0 @@ -@RFC0453 @AIP20 @credentials -Feature: Issue JWT Credentials with unpublished DID - - Background: - Given Issuer and Holder have an existing connection - 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 - And Holder receives the credential offer - And Holder accepts credential offer for JWT - And Issuer issues the credential - Then Holder receives the issued credential