Skip to content

Commit

Permalink
test: add sd-jwt integration test (#1260)
Browse files Browse the repository at this point in the history
Signed-off-by: Allain Magyar <[email protected]>
Signed-off-by: Hyperledger Bot <[email protected]>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Hyperledger Bot <[email protected]>
  • Loading branch information
3 people authored Jul 15, 2024
1 parent c30e310 commit fafeee6
Show file tree
Hide file tree
Showing 30 changed files with 1,014 additions and 628 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -64,6 +67,42 @@ open class ListenToEvents(
fun with(actor: Actor): ListenToEvents {
return actor.abilityTo(ListenToEvents::class.java)
}

fun presentationProofStatus(actor: Actor): Question<PresentationStatusAdapter.Status?> {
return Question.about("presentation status").answeredBy {
val proofEvent = with(actor).presentationEvents.lastOrNull {
it.data.thid == actor.recall<String>("thid")
}
proofEvent?.data?.status
}
}

fun connectionState(actor: Actor): Question<Connection.State?> {
return Question.about("connection state").answeredBy {
val lastEvent = with(actor).connectionEvents.lastOrNull {
it.data.thid == actor.recall<Connection>("connection").thid
}
lastEvent?.data?.state
}
}

fun credentialState(actor: Actor): Question<IssueCredentialRecord.ProtocolState?> {
return Question.about("credential state").answeredBy {
val credentialEvent = ListenToEvents.with(actor).credentialEvents.lastOrNull {
it.data.thid == actor.recall<String>("thid")
}
credentialEvent?.data?.protocolState
}
}

fun didStatus(actor: Actor): Question<String> {
return Question.about("did status").answeredBy {
val didEvent = ListenToEvents.with(actor).didEvents.lastOrNull {
it.data.did == actor.recall<String>("shortFormDid")
}
didEvent?.data?.status
}
}
}

init {
Expand Down
25 changes: 16 additions & 9 deletions tests/integration-tests/src/test/kotlin/common/DidPurpose.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,30 @@ package common
import org.hyperledger.identus.client.models.*

enum class DidPurpose {
EMPTY {
override val publicKeys = emptyList<ManagedDIDKeyTemplate>()
override val services = emptyList<Service>()
CUSTOM {
override val publicKeys = mutableListOf<ManagedDIDKeyTemplate>()
override val services = mutableListOf<Service>()
},
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<Service>()
},
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<Service>()
override val services = mutableListOf<Service>()
},
ANONCRED {
override val publicKeys = emptyList<ManagedDIDKeyTemplate>()
override val services = emptyList<Service>()
override val publicKeys = mutableListOf<ManagedDIDKeyTemplate>()
override val services = mutableListOf<Service>()
}, ;

abstract val publicKeys: List<ManagedDIDKeyTemplate>
abstract val services: List<Service>
abstract val publicKeys: MutableList<ManagedDIDKeyTemplate>
abstract val services: MutableList<Service>
}
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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)
}

Expand Down
7 changes: 7 additions & 0 deletions tests/integration-tests/src/test/kotlin/models/SdJwtClaim.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package models

data class SdJwtClaim(
val salt: String,
val key: String,
val value: String,
)
67 changes: 52 additions & 15 deletions tests/integration-tests/src/test/kotlin/steps/common/CommonSteps.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Expand Down Expand Up @@ -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>("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>("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")
Expand All @@ -120,8 +118,8 @@ class ConnectionSteps {
assertThat(inviter.recall<Connection>("connection-with-${invitee.name}").theirDid)
.isEqualTo(invitee.recall<Connection>("connection-with-${inviter.name}").myDid)
assertThat(inviter.recall<Connection>("connection-with-${invitee.name}").state)
.isEqualTo(Connection.State.CONNECTION_RESPONSE_SENT)
.isEqualTo(CONNECTION_RESPONSE_SENT)
assertThat(invitee.recall<Connection>("connection-with-${inviter.name}").state)
.isEqualTo(Connection.State.CONNECTION_RESPONSE_RECEIVED)
.isEqualTo(CONNECTION_RESPONSE_RECEIVED)
}
}
Original file line number Diff line number Diff line change
@@ -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<String>("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<CredentialDefinitionResponse>("anoncredsCredentialDefinition").guid,
claims = linkedMapOf(
"name" to "Bob",
"age" to "21",
"sex" to "M",
),
issuingDID = issuer.recall("shortFormDid"),
connectionId = issuer.recall<Connection>("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<IssueCredentialRecord>()
issuer.remember("thid", credentialRecord.thid)
holder.remember("thid", credentialRecord.thid)
}
}
Loading

0 comments on commit fafeee6

Please sign in to comment.