Skip to content

Commit

Permalink
test: add jwt integration scenario
Browse files Browse the repository at this point in the history
Signed-off-by: Allain Magyar <allain.magyar@iohk.io>
  • Loading branch information
amagyar-iohk committed May 7, 2024
1 parent d674f31 commit ab8ffcb
Show file tree
Hide file tree
Showing 13 changed files with 259 additions and 88 deletions.
4 changes: 2 additions & 2 deletions tests/integration-tests/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,9 @@ The project structure is represented below:
│ ├── abilities -> contains the abilities of the actors
│ ├── common -> contains the common classes (test constants and helper functions)
│ ├── config -> contains the configuration classes (Hoplite)
│ ├── features -> contains the features implementation steps
│ ├── interactions -> contains the interactions of the actors
│ └── runners -> contains the test runners to execute the tests
│ ├── models -> contains the models
│ ├── steps -> contains the features implementation steps
└── resources -> contains the test resources
├── configs -> contains the test configuration files
├── containers -> contains the Docker Compose files to start the test environment
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,7 @@ open class ListenToEvents(
TestConstants.EVENT_TYPE_CONNECTION_UPDATED -> connectionEvents.add(gson.fromJson(eventString, ConnectionEvent::class.java))
TestConstants.EVENT_TYPE_ISSUE_CREDENTIAL_RECORD_UPDATED -> credentialEvents.add(gson.fromJson(eventString, CredentialEvent::class.java))
TestConstants.EVENT_TYPE_PRESENTATION_UPDATED -> presentationEvents.add(gson.fromJson(eventString, PresentationEvent::class.java))
TestConstants.EVENT_TYPE_DID_STATUS_UPDATED -> {
didEvents.add(gson.fromJson(eventString, DidEvent::class.java))
}
TestConstants.EVENT_TYPE_DID_STATUS_UPDATED -> didEvents.add(gson.fromJson(eventString, DidEvent::class.java))
else -> {
throw IllegalArgumentException("ERROR: unknown event type ${event.type}")
}
Expand All @@ -56,7 +54,7 @@ open class ListenToEvents(
return ListenToEvents(url, webhookPort)
}

fun `as`(actor: Actor): ListenToEvents {
fun with(actor: Actor): ListenToEvents {
return actor.abilityTo(ListenToEvents::class.java)
}
}
Expand Down
36 changes: 31 additions & 5 deletions tests/integration-tests/src/test/kotlin/models/Events.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,7 @@ package models

import com.google.gson.JsonElement
import com.google.gson.annotations.SerializedName
import org.hyperledger.identus.client.models.Connection
import org.hyperledger.identus.client.models.IssueCredentialRecord
import org.hyperledger.identus.client.models.ManagedDID
import org.hyperledger.identus.client.models.PresentationStatus
import org.hyperledger.identus.client.models.*

data class Event(
@SerializedName("type") var type: String,
Expand Down Expand Up @@ -35,10 +32,39 @@ data class PresentationEvent(
@SerializedName("type") var type: String,
@SerializedName("id") var id: String,
@SerializedName("ts") var ts: String,
@SerializedName("data") var data: PresentationStatus,
@SerializedName("data") var data: PresentationStatusAdapter, // FIXME: rollback to PresentationStatus when Status is fixed
@SerializedName("walletId") var walletId: String,
)

data class PresentationStatusAdapter( // FIXME: delete this class when PresentationStatus.Status is fixed
@SerializedName("presentationId") val presentationId: String,
@SerializedName("thid") val thid: String,
@SerializedName("role") val role: PresentationStatus.Role,
@SerializedName("status") val status: Status,
@SerializedName("metaRetries") val metaRetries: Int,
@SerializedName("proofs") val proofs: List<ProofRequestAux>? = null,
@SerializedName("data") val `data`: List<String>? = null,
@SerializedName("connectionId") val connectionId: String? = null
) {
enum class Status(val value: String) {
@SerializedName(value = "RequestPending") REQUEST_PENDING("RequestPending"),
@SerializedName(value = "RequestSent") REQUEST_SENT("RequestSent"),
@SerializedName(value = "RequestReceived") REQUEST_RECEIVED("RequestReceived"),
@SerializedName(value = "RequestRejected") REQUEST_REJECTED("RequestRejected"),
@SerializedName(value = "PresentationPending") PRESENTATION_PENDING("PresentationPending"),
@SerializedName(value = "PresentationGenerated") PRESENTATION_GENERATED("PresentationGenerated"),
@SerializedName(value = "PresentationSent") PRESENTATION_SENT("PresentationSent"),
@SerializedName(value = "PresentationReceived") PRESENTATION_RECEIVED("PresentationReceived"),
@SerializedName(value = "PresentationVerified") PRESENTATION_VERIFIED("PresentationVerified"),
@SerializedName(value = "PresentationAccepted") PRESENTATION_ACCEPTED("PresentationAccepted"),
@SerializedName(value = "PresentationRejected") PRESENTATION_REJECTED("PresentationRejected"),
@SerializedName(value = "ProblemReportPending") PROBLEM_REPORT_PENDING("ProblemReportPending"),
@SerializedName(value = "ProblemReportSent") PROBLEM_REPORT_SENT("ProblemReportSent"),
@SerializedName(value = "ProblemReportReceived") PROBLEM_REPORT_RECEIVED("ProblemReportReceived"),
@SerializedName(value = "PresentationVerificationFailed") PRESENTATION_VERIFICATION_FAILED("PresentationVerificationFailed");
}
}

data class DidEvent(
@SerializedName("type") var type: String,
@SerializedName("id") var id: String,
Expand Down
22 changes: 22 additions & 0 deletions tests/integration-tests/src/test/kotlin/models/JwtCredential.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package models

import com.jayway.jsonpath.DocumentContext
import com.jayway.jsonpath.JsonPath
import java.util.Base64


class JwtCredential(base64: String) {
private val payload: DocumentContext

init {
val jwt = String(Base64.getDecoder().decode(base64))
val parts = jwt.split(".")
payload = JsonPath.parse(String(Base64.getUrlDecoder().decode(parts[1])))
}

fun statusListId(): String {
val listUrl = payload.read<String>("$.vc.credentialStatus.statusListCredential")
return listUrl.split("/credential-status/")[1]
}

}
35 changes: 11 additions & 24 deletions tests/integration-tests/src/test/kotlin/steps/common/CommonSteps.kt
Original file line number Diff line number Diff line change
Expand Up @@ -25,31 +25,18 @@ class CommonSteps {

@Given("{actor} has an issued credential from {actor}")
fun holderHasIssuedCredentialFromIssuer(holder: Actor, issuer: Actor) {
holder.attemptsTo(
Get.resource("/issue-credentials/records"),
)
holder.attemptsTo(
Ensure.thatTheLastResponse().statusCode().isEqualTo(HttpStatus.SC_OK),
)
val receivedCredential = SerenityRest.lastResponse().get<IssueCredentialRecordPage>().contents!!.findLast { credential ->
credential.protocolState == IssueCredentialRecord.ProtocolState.CREDENTIAL_RECEIVED &&
credential.credentialFormat == IssueCredentialRecord.CredentialFormat.JWT
}
actorsHaveExistingConnection(issuer, holder)

if (receivedCredential != null) {
holder.remember("issuedCredential", receivedCredential)
} else {
val publishDidSteps = PublishDidSteps()
val issueSteps = IssueCredentialsSteps()
actorsHaveExistingConnection(issuer, holder)
publishDidSteps.agentHasAnUnpublishedDID(holder)
publishDidSteps.agentHasAPublishedDID(issuer)
issueSteps.issuerOffersACredential(issuer, holder, "short")
issueSteps.holderReceivesCredentialOffer(holder)
issueSteps.holderAcceptsCredentialOfferForJwt(holder)
issueSteps.acmeIssuesTheCredential(issuer)
issueSteps.bobHasTheCredentialIssued(holder)
}
val publishDidSteps = PublishDidSteps()
publishDidSteps.createsUnpublishedDid(holder)
publishDidSteps.actorHavePublishedPrismDid(issuer)

val issueSteps = IssueCredentialsSteps()
issueSteps.issuerOffersACredential(issuer, holder, "short")
issueSteps.holderReceivesCredentialOffer(holder)
issueSteps.holderAcceptsCredentialOfferForJwt(holder)
issueSteps.acmeIssuesTheCredential(issuer)
issueSteps.bobHasTheCredentialIssued(holder)
}

@Given("{actor} and {actor} have an existing connection")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ class ConnectionSteps {
fun inviterReceivesTheConnectionRequest(inviter: Actor) {
wait(
{
val lastEvent = ListenToEvents.`as`(inviter).connectionEvents.lastOrNull {
val lastEvent = ListenToEvents.with(inviter).connectionEvents.lastOrNull {
it.data.thid == inviter.recall<Connection>("connection").thid
}
lastEvent != null &&
Expand All @@ -94,7 +94,7 @@ class ConnectionSteps {
// Bob (Holder) receives final connection response
wait(
{
val lastEvent = ListenToEvents.`as`(invitee).connectionEvents.lastOrNull {
val lastEvent = ListenToEvents.with(invitee).connectionEvents.lastOrNull {
it.data.thid == invitee.recall<Connection>("connection").thid
}
lastEvent != null &&
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,7 @@ class IssueCredentialsSteps {
fun holderReceivesCredentialOffer(holder: Actor) {
wait(
{
credentialEvent = ListenToEvents.`as`(holder).credentialEvents.lastOrNull {
credentialEvent = ListenToEvents.with(holder).credentialEvents.lastOrNull {
it.data.thid == holder.recall<String>("thid")
}
credentialEvent != null &&
Expand All @@ -216,7 +216,7 @@ class IssueCredentialsSteps {
"Protocol state did not achieve ${IssueCredentialRecord.ProtocolState.OFFER_RECEIVED} state.",
)

val recordId = ListenToEvents.`as`(holder).credentialEvents.last().data.recordId
val recordId = ListenToEvents.with(holder).credentialEvents.last().data.recordId
holder.remember("recordId", recordId)
}

Expand Down Expand Up @@ -254,7 +254,7 @@ class IssueCredentialsSteps {
fun acmeIssuesTheCredential(issuer: Actor) {
wait(
{
credentialEvent = ListenToEvents.`as`(issuer).credentialEvents.lastOrNull {
credentialEvent = ListenToEvents.with(issuer).credentialEvents.lastOrNull {
it.data.thid == issuer.recall<String>("thid")
}
credentialEvent != null &&
Expand All @@ -272,9 +272,10 @@ class IssueCredentialsSteps {

wait(
{
credentialEvent = ListenToEvents.`as`(issuer).credentialEvents.lastOrNull {
credentialEvent = ListenToEvents.with(issuer).credentialEvents.lastOrNull {
it.data.thid == issuer.recall<String>("thid")
}
issuer.remember("issuedCredential", credentialEvent!!.data)
credentialEvent != null &&
credentialEvent!!.data.protocolState == IssueCredentialRecord.ProtocolState.CREDENTIAL_SENT
},
Expand All @@ -287,7 +288,7 @@ class IssueCredentialsSteps {
fun bobHasTheCredentialIssued(holder: Actor) {
wait(
{
credentialEvent = ListenToEvents.`as`(holder).credentialEvents.lastOrNull {
credentialEvent = ListenToEvents.with(holder).credentialEvents.lastOrNull {
it.data.thid == holder.recall<String>("thid")
}
credentialEvent != null &&
Expand All @@ -296,7 +297,7 @@ class IssueCredentialsSteps {
"Holder was unable to receive the credential from Issuer! " +
"Protocol state did not achieve ${IssueCredentialRecord.ProtocolState.CREDENTIAL_RECEIVED} state.",
)
holder.remember("issuedCredential", ListenToEvents.`as`(holder).credentialEvents.last().data)
holder.remember("issuedCredential", ListenToEvents.with(holder).credentialEvents.last().data)
}

@Then("{actor} should see that credential issuance has failed")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package steps.credentials

import interactions.*
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.utils.Wait
import models.JwtCredential
import net.serenitybdd.rest.SerenityRest
import net.serenitybdd.screenplay.Actor
import org.apache.http.HttpStatus
import org.hyperledger.identus.client.models.IssueCredentialRecord
import kotlin.time.Duration.Companion.seconds

class RevokeCredentialSteps {
@When("{actor} revokes the credential issued to {actor}")
fun issuerRevokesCredentialsIssuedToHolder(issuer: Actor, holder: Actor) {
val issuedCredential = issuer.recall<IssueCredentialRecord>("issuedCredential")
val jwtCred = JwtCredential(issuedCredential.credential!!)
val statusListId = jwtCred.statusListId()
issuer.remember("statusListId", statusListId)

issuer.attemptsTo(
Get.resource("/credential-status/${statusListId}")
)
val encodedList = SerenityRest.lastResponse().jsonPath().get<String>("credentialSubject.encodedList")
issuer.remember("encodedStatusList", encodedList)

issuer.attemptsTo(
Patch.to("/credential-status/revoke-credential/${issuedCredential.recordId}"),
Ensure.thatTheLastResponse().statusCode().isEqualTo(HttpStatus.SC_OK)
)
}

@When("{actor} tries to revoke credential from {actor}")
fun holderTriesToRevokeCredentialFromIssuer(holder: Actor, issuer: Actor) {
val issuedCredential = issuer.recall<IssueCredentialRecord>("issuedCredential")
val receivedCredential = holder.recall<IssueCredentialRecord>("issuedCredential")
holder.attemptsTo(
Patch.to("/credential-status/revoke-credential/${issuedCredential.recordId}"),
Ensure.thatTheLastResponse().statusCode().isEqualTo(HttpStatus.SC_NOT_FOUND)
)
holder.attemptsTo(
Patch.to("/credential-status/revoke-credential/${receivedCredential.recordId}"),
Ensure.thatTheLastResponse().statusCode().isEqualTo(HttpStatus.SC_NOT_FOUND)
)
}

@Then("{actor} should see the credential was revoked")
fun credentialShouldBeRevoked(issuer: Actor) {
Wait.until(
timeout = 30.seconds,
errorMessage = "Encoded Status List didn't change after revoking."
) {
val statusListId = issuer.recall<String>("statusListId")
val encodedList = issuer.recall<String>("statusEncodedList")
issuer.attemptsTo(
Get.resource("/credential-status/$statusListId")
)
val actualEncodedList = SerenityRest.lastResponse().jsonPath().get<String>("credentialSubject.encodedList")
actualEncodedList != encodedList
}
}

@Then("{actor} should see the credential is not revoked")
fun issuerShouldSeeTheCredentialIsNotRevoked(issuer: Actor) {
val issuedCredential = issuer.recall<IssueCredentialRecord>("issuedCredential")
val jwtCred = JwtCredential(issuedCredential.credential!!)
val statusListId = jwtCred.statusListId()
issuer.remember("statusListId", statusListId)

issuer.attemptsTo(
Get.resource("/credential-status/${statusListId}")
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ class PublishDidSteps {
wait(
{
val didEvent =
ListenToEvents.`as`(actor).didEvents.lastOrNull {
ListenToEvents.with(actor).didEvents.lastOrNull {
it.data.did == actor.recall<String>("shortFormDid")
}
didEvent != null && didEvent.data.status == "PUBLISHED"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import io.cucumber.java.en.When
import io.iohk.atala.automation.extensions.get
import io.iohk.atala.automation.serenity.ensure.Ensure
import models.PresentationEvent
import models.PresentationStatusAdapter
import net.serenitybdd.rest.SerenityRest
import net.serenitybdd.screenplay.Actor
import net.serenitybdd.screenplay.rest.abilities.CallAnApi
Expand Down Expand Up @@ -67,7 +68,7 @@ class AnoncredsPresentProofSteps {
},
)
faber.attemptsTo(
Ensure.thatTheLastResponse().apply { println(this.contentType()) }.statusCode().isEqualTo(SC_CREATED),
Ensure.thatTheLastResponse().statusCode().isEqualTo(SC_CREATED)
)
val presentationStatus = SerenityRest.lastResponse().get<PresentationStatus>()
faber.remember("thid", presentationStatus.thid)
Expand All @@ -78,11 +79,11 @@ class AnoncredsPresentProofSteps {
fun bobReceivesTheAnoncredsRequest(bob: Actor) {
wait(
{
proofEvent = ListenToEvents.`as`(bob).presentationEvents.lastOrNull {
proofEvent = ListenToEvents.with(bob).presentationEvents.lastOrNull {
it.data.thid == bob.recall<String>("thid")
}
proofEvent != null &&
proofEvent!!.data.status == PresentationStatus.Status.REQUEST_RECEIVED
proofEvent!!.data.status == PresentationStatusAdapter.Status.REQUEST_RECEIVED
},
"ERROR: Bob did not achieve any presentation request!",
)
Expand Down

0 comments on commit ab8ffcb

Please sign in to comment.