Repository: TBD54566975/ssi-service
Pull request #487 (2023-05-25 18:03:25)

Fixing Cred Suspension Bug
Fixed the suspension issue not reflecting back to the user.

Updated unit tests
Created suspension integration test
Added UnRevoke and UnSuspend integration test

<img width="765" alt="image" src="https://github.com/TBD54566975/ssi-service/assets/5314059/30bf338d-7d03-4f48-bdad-746bf9fd1964">

<img width="896" alt="image" src="https://github.com/TBD54566975/ssi-service/assets/5314059/ddc01954-3e3e-4cbd-af8b-aa6ee78812cc">


nitro-neal: Fixes - https://github.com/TBD54566975/ssi-service/issues/473


Patch:
diff --git a/integration/common.go b/integration/common.go
index 4fded02..7fa8a53 100644
--- a/integration/common.go
+++ b/integration/common.go
@@ -107,24 +107,30 @@ func CreateKYCSchema() (string, error) {
 }
 
 type credInputParams struct {
-	IssuerID  string
-	IssuerKID string
-	SchemaID  string
-	SubjectID string
+	IssuerID    string
+	IssuerKID   string
+	SchemaID    string
+	SubjectID   string
+	Revocable   bool
+	Suspendable bool
 }
 
-func CreateVerifiableCredential(credentialInput credInputParams, revocable bool) (string, error) {
+func CreateVerifiableCredential(credentialInput credInputParams) (string, error) {
 	logrus.Println("\n\nCreate a verifiable credential")
 
 	if credentialInput.SubjectID == "" {
 		credentialInput.SubjectID = credentialInput.IssuerID
 	}
 
-	fileName := "credential-input.json"
-	if revocable {
-		fileName = "credential-revocable-input.json"
+	if credentialInput.Revocable {
+		credentialInput.Revocable = true
 	}
-	credentialJSON, err := resolveTemplate(credentialInput, fileName)
+
+	if credentialInput.Suspendable {
+		credentialInput.Suspendable = true
+	}
+
+	credentialJSON, err := resolveTemplate(credentialInput, "credential-input.json")
 	if err != nil {
 		return "", err
 	}
diff --git a/pkg/service/credential/service.go b/pkg/service/credential/service.go
index 931c044..504cc3b 100644
--- a/pkg/service/credential/service.go
+++ b/pkg/service/credential/service.go
@@ -426,6 +426,8 @@ func (s Service) GetCredential(ctx context.Context, request GetCredentialRequest
 			ID:            gotCred.CredentialID,
 			Credential:    gotCred.Credential,
 			CredentialJWT: gotCred.CredentialJWT,
+			Revoked:       gotCred.Revoked,
+			Suspended:     gotCred.Suspended,
 		},
 	}
 	return &response, nil
@@ -445,6 +447,8 @@ func (s Service) ListCredentialsByIssuer(ctx context.Context, request ListCreden
 			ID:            cred.CredentialID,
 			Credential:    cred.Credential,
 			CredentialJWT: cred.CredentialJWT,
+			Revoked:       cred.Revoked,
+			Suspended:     cred.Suspended,
 		}
 		creds = append(creds, container)
 	}
@@ -467,6 +471,8 @@ func (s Service) ListCredentialsBySubject(ctx context.Context, request ListCrede
 			ID:            cred.CredentialID,
 			Credential:    cred.Credential,
 			CredentialJWT: cred.CredentialJWT,
+			Revoked:       cred.Revoked,
+			Suspended:     cred.Suspended,
 		}
 		creds = append(creds, container)
 	}
@@ -488,6 +494,8 @@ func (s Service) ListCredentialsBySchema(ctx context.Context, request ListCreden
 			ID:            cred.CredentialID,
 			Credential:    cred.Credential,
 			CredentialJWT: cred.CredentialJWT,
+			Revoked:       cred.Revoked,
+			Suspended:     cred.Suspended,
 		}
 		creds = append(creds, container)
 	}
@@ -506,7 +514,8 @@ func (s Service) GetCredentialStatus(ctx context.Context, request GetCredentialS
 		return nil, sdkutil.LoggingNewErrorf("credential returned is not valid: %s", request.ID)
 	}
 	response := GetCredentialStatusResponse{
-		Revoked: gotCred.Revoked,
+		Revoked:   gotCred.Revoked,
+		Suspended: gotCred.Suspended,
 	}
 	return &response, nil
 }
@@ -526,6 +535,8 @@ func (s Service) GetCredentialStatusList(ctx context.Context, request GetCredent
 			ID:            gotCred.CredentialID,
 			Credential:    gotCred.Credential,
 			CredentialJWT: gotCred.CredentialJWT,
+			Revoked:       false, // Credential Status List cannot be revoked
+			Suspended:     false, // Credential Status List cannot be suspended
 		},
 	}
 	return &response, nil
@@ -582,7 +593,7 @@ func (s Service) updateCredentialStatusFunc(request UpdateCredentialStatusReques
 }
 
 func (s Service) updateCredentialStatusBusinessLogic(ctx context.Context, tx storage.Tx, request UpdateCredentialStatusRequest, slcMetadata StatusListCredentialMetadata) (*UpdateCredentialStatusResponse, error) {
-	logrus.Debugf("updating credential status: %s to Revoked: %v", request.ID, request.Revoked)
+	logrus.Debugf("updating credential status: %s to Revoked: %v, Suspended: %v", request.ID, request.Revoked, request.Suspended)
 
 	if request.Suspended && request.Revoked {
 		return nil, sdkutil.LoggingNewErrorf("cannot update both suspended and revoked status")


Test patch:
diff --git a/integration/credential_manifest_integration_test.go b/integration/credential_manifest_integration_test.go
index 32d0de4..a324c3d 100644
--- a/integration/credential_manifest_integration_test.go
+++ b/integration/credential_manifest_integration_test.go
@@ -92,7 +92,7 @@ func TestCreateVerifiableCredentialIntegration(t *testing.T) {
 		IssuerKID: issuerKID.(string),
 		SchemaID:  schemaID.(string),
 		SubjectID: issuerDID.(string),
-	}, false)
+	})
 	assert.NoError(t, err)
 	assert.NotEmpty(t, vcOutput)
 
diff --git a/integration/credential_revocation_concurrency_integration_test.go b/integration/credential_revocation_concurrency_integration_test.go
index 6181702..31973b8 100644
--- a/integration/credential_revocation_concurrency_integration_test.go
+++ b/integration/credential_revocation_concurrency_integration_test.go
@@ -30,7 +30,7 @@ func TestCreateRevocationVerifiableCredentialIntegration(t *testing.T) {
 	assert.NoError(t, err)
 	assert.NotEmpty(t, schemaID)
 
-	vcOutput, err := CreateVerifiableCredential(credInputParams{IssuerID: issuerDID, IssuerKID: issuerKID, SchemaID: schemaID, SubjectID: issuerDID}, true)
+	vcOutput, err := CreateVerifiableCredential(credInputParams{IssuerID: issuerDID, IssuerKID: issuerKID, SchemaID: schemaID, SubjectID: issuerDID, Revocable: true})
 	assert.NoError(t, err)
 	assert.NotEmpty(t, vcOutput)
 
@@ -68,7 +68,7 @@ func TestCreateRevocationVerifiableCredentialShareStatusListIntegration(t *testi
 	assert.NoError(t, err)
 	assert.NotEmpty(t, schemaID)
 
-	vcOutput, err := CreateVerifiableCredential(credInputParams{IssuerID: issuerDID, IssuerKID: issuerKID, SchemaID: schemaID, SubjectID: issuerDID}, true)
+	vcOutput, err := CreateVerifiableCredential(credInputParams{IssuerID: issuerDID, IssuerKID: issuerKID, SchemaID: schemaID, SubjectID: issuerDID, Revocable: true})
 	assert.NoError(t, err)
 	assert.NotEmpty(t, vcOutput)
 
@@ -82,7 +82,7 @@ func TestCreateRevocationVerifiableCredentialShareStatusListIntegration(t *testi
 	assert.NotEmpty(t, credStatusListURL)
 	assert.Contains(t, credStatusListURL, "http")
 
-	vcOutputTwo, err := CreateVerifiableCredential(credInputParams{IssuerID: issuerDID, IssuerKID: issuerKID, SchemaID: schemaID, SubjectID: issuerDID}, true)
+	vcOutputTwo, err := CreateVerifiableCredential(credInputParams{IssuerID: issuerDID, IssuerKID: issuerKID, SchemaID: schemaID, SubjectID: issuerDID, Revocable: true})
 	assert.NoError(t, err)
 	assert.NotEmpty(t, vcOutputTwo)
 
@@ -135,7 +135,7 @@ func TestConcurrencyRevocationVerifiableCredentialIntegration(t *testing.T) {
 		go func() {
 			defer wg.Done()
 
-			vcOutput, err := CreateVerifiableCredential(credInputParams{IssuerID: issuerDID, IssuerKID: issuerKID, SchemaID: schemaID, SubjectID: issuerDID}, true)
+			vcOutput, err := CreateVerifiableCredential(credInputParams{IssuerID: issuerDID, IssuerKID: issuerKID, SchemaID: schemaID, SubjectID: issuerDID, Revocable: true})
 
 			// We're hammering the DB, so some calls might fail due to internal timeouts or similar. Upon failure, we
 			// shouldn't check any assertions, since we know they'll fail.
diff --git a/integration/credential_revocation_integration_test.go b/integration/credential_revocation_integration_test.go
index 49461ab..03360a0 100644
--- a/integration/credential_revocation_integration_test.go
+++ b/integration/credential_revocation_integration_test.go
@@ -66,7 +66,8 @@ func TestRevocationCreateVerifiableCredentialIntegration(t *testing.T) {
 		IssuerKID: issuerKID.(string),
 		SchemaID:  schemaID.(string),
 		SubjectID: issuerDID.(string),
-	}, true)
+		Revocable: true,
+	})
 	assert.NoError(t, err)
 	assert.NotEmpty(t, vcOutput)
 
@@ -195,3 +196,36 @@ func TestRevocationValidateCredentialInStatusListIntegration(t *testing.T) {
 	assert.NoError(t, err)
 	assert.True(t, valid)
 }
+
+func TestRevocationUnRevokeCredential(t *testing.T) {
+	if testing.Short() {
+		t.Skip("skipping integration test")
+	}
+
+	credStatusURL, err := GetValue(credentialRevocationContext, "credStatusURL")
+	assert.NoError(t, err)
+	assert.NotEmpty(t, credStatusURL)
+
+	credStatusOutput, err := get(credStatusURL.(string))
+	assert.NoError(t, err)
+	assert.NotEmpty(t, credStatusOutput)
+
+	revoked, err := getJSONElement(credStatusOutput, "$.revoked")
+	assert.NoError(t, err)
+	assert.Equal(t, "true", revoked)
+
+	revokedOutput, err := put(credStatusURL.(string), `{"revoked":false}`)
+	assert.NoError(t, err)
+
+	revoked, err = getJSONElement(revokedOutput, "$.revoked")
+	assert.NoError(t, err)
+	assert.Equal(t, "false", revoked)
+
+	credStatusOutput, err = get(credStatusURL.(string))
+	assert.NoError(t, err)
+	assert.NotEmpty(t, credStatusOutput)
+
+	revoked, err = getJSONElement(credStatusOutput, "$.revoked")
+	assert.NoError(t, err)
+	assert.Equal(t, "false", revoked)
+}
diff --git a/integration/credential_suspension_integration_test.go b/integration/credential_suspension_integration_test.go
new file mode 100644
index 0000000..624c53d
--- /dev/null
+++ b/integration/credential_suspension_integration_test.go
@@ -0,0 +1,234 @@
+package integration
+
+import (
+	"fmt"
+	"testing"
+
+	"github.com/TBD54566975/ssi-sdk/credential"
+	"github.com/TBD54566975/ssi-sdk/credential/status"
+	"github.com/goccy/go-json"
+	"github.com/stretchr/testify/assert"
+)
+
+var credentialSuspensionContext = NewTestContext("CredentialSuspension")
+
+func TestSuspensionCreateIssuerDIDKeyIntegration(t *testing.T) {
+	if testing.Short() {
+		t.Skip("skipping integration test")
+	}
+
+	didKeyOutput, err := CreateDIDKey()
+	assert.NoError(t, err)
+
+	issuerDID, err := getJSONElement(didKeyOutput, "$.did.id")
+	assert.NoError(t, err)
+	assert.Contains(t, issuerDID, "did:key")
+	SetValue(credentialSuspensionContext, "issuerDID", issuerDID)
+
+	issuerKID, err := getJSONElement(didKeyOutput, "$.did.verificationMethod[0].id")
+	assert.NoError(t, err)
+	assert.NotEmpty(t, issuerKID)
+	SetValue(credentialSuspensionContext, "issuerKID", issuerKID)
+}
+
+func TestSuspensionCreateSchemaIntegration(t *testing.T) {
+	if testing.Short() {
+		t.Skip("skipping integration test")
+	}
+
+	output, err := CreateKYCSchema()
+	assert.NoError(t, err)
+
+	schemaID, err := getJSONElement(output, "$.id")
+	assert.NoError(t, err)
+	assert.NotEmpty(t, schemaID)
+	SetValue(credentialSuspensionContext, "schemaID", schemaID)
+}
+
+func TestSuspensionCreateVerifiableCredentialIntegration(t *testing.T) {
+	if testing.Short() {
+		t.Skip("skipping integration test")
+	}
+
+	issuerDID, err := GetValue(credentialSuspensionContext, "issuerDID")
+	assert.NoError(t, err)
+	assert.NotEmpty(t, issuerDID)
+
+	issuerKID, err := GetValue(credentialSuspensionContext, "issuerKID")
+	assert.NoError(t, err)
+	assert.NotEmpty(t, issuerKID)
+
+	schemaID, err := GetValue(credentialSuspensionContext, "schemaID")
+	assert.NoError(t, err)
+	assert.NotEmpty(t, schemaID)
+
+	vcOutput, err := CreateVerifiableCredential(credInputParams{
+		IssuerID:    issuerDID.(string),
+		IssuerKID:   issuerKID.(string),
+		SchemaID:    schemaID.(string),
+		SubjectID:   issuerDID.(string),
+		Suspendable: true,
+	})
+	assert.NoError(t, err)
+	assert.NotEmpty(t, vcOutput)
+
+	cred, err := getJSONElement(vcOutput, "$.credential")
+	assert.NoError(t, err)
+	assert.NotEmpty(t, cred)
+	SetValue(credentialSuspensionContext, "cred", cred)
+
+	credStatusURL, err := getJSONElement(vcOutput, "$.credential.credentialStatus.id")
+	assert.NoError(t, err)
+	assert.NotEmpty(t, credStatusURL)
+	assert.Contains(t, credStatusURL, "http")
+	SetValue(credentialSuspensionContext, "credStatusURL", credStatusURL)
+
+	fmt.Print(credStatusURL)
+
+	statusListCredentialURL, err := getJSONElement(vcOutput, "$.credential.credentialStatus.statusListCredential")
+	assert.NoError(t, err)
+	assert.NotEmpty(t, statusListCredentialURL)
+	assert.Contains(t, statusListCredentialURL, "http")
+	SetValue(credentialSuspensionContext, "statusListCredentialURL", statusListCredentialURL)
+
+	credStatusListCredentialOutput, err := get(statusListCredentialURL)
+	assert.NoError(t, err)
+	assert.NotEmpty(t, credStatusListCredentialOutput)
+
+	encodedListOriginal, err := getJSONElement(credStatusListCredentialOutput, "$.credential.credentialSubject.encodedList")
+	assert.NoError(t, err)
+	assert.NotEmpty(t, encodedListOriginal)
+	SetValue(credentialSuspensionContext, "encodedListOriginal", encodedListOriginal)
+}
+
+func TestSuspensionCheckStatusIntegration(t *testing.T) {
+	if testing.Short() {
+		t.Skip("skipping integration test")
+	}
+
+	credStatusURL, err := GetValue(credentialSuspensionContext, "credStatusURL")
+	assert.NoError(t, err)
+	assert.NotEmpty(t, credStatusURL)
+
+	credStatusOutput, err := get(credStatusURL.(string))
+	assert.NoError(t, err)
+	assert.NotEmpty(t, credStatusOutput)
+
+	suspended, err := getJSONElement(credStatusOutput, "$.suspended")
+	assert.NoError(t, err)
+	assert.Equal(t, "false", suspended)
+
+	suspendedOutput, err := put(credStatusURL.(string), getJSONFromFile("suspended-input.json"))
+	assert.NoError(t, err)
+
+	suspended, err = getJSONElement(suspendedOutput, "$.suspended")
+	assert.NoError(t, err)
+	assert.Equal(t, "true", suspended)
+
+	credStatusOutput, err = get(credStatusURL.(string))
+	assert.NoError(t, err)
+	assert.NotEmpty(t, credStatusOutput)
+
+	suspended, err = getJSONElement(credStatusOutput, "$.suspended")
+	assert.NoError(t, err)
+	assert.Equal(t, "true", suspended)
+}
+
+func TestSuspensionCheckStatusListCredentialIntegration(t *testing.T) {
+	if testing.Short() {
+		t.Skip("skipping integration test")
+	}
+
+	statusListCredentialURL, err := GetValue(credentialSuspensionContext, "statusListCredentialURL")
+	assert.NoError(t, err)
+	assert.NotEmpty(t, statusListCredentialURL)
+
+	credStatusListCredentialOutput, err := get(statusListCredentialURL.(string))
+	assert.NoError(t, err)
+	assert.NotEmpty(t, credStatusListCredentialOutput)
+
+	statusListType, err := getJSONElement(credStatusListCredentialOutput, "$.credential.credentialSubject.type")
+	assert.NoError(t, err)
+	assert.NotEmpty(t, statusListType)
+	assert.Equal(t, "StatusList2021", statusListType)
+
+	encodedList, err := getJSONElement(credStatusListCredentialOutput, "$.credential.credentialSubject.encodedList")
+	assert.NoError(t, err)
+	assert.NotEmpty(t, encodedList)
+
+	encodedListOriginal, err := GetValue(credentialSuspensionContext, "encodedListOriginal")
+	assert.NoError(t, err)
+	assert.NotEmpty(t, encodedListOriginal)
+
+	assert.NotEqual(t, encodedListOriginal.(string), encodedList)
+}
+
+func TestSuspensionValidateCredentialInStatusListIntegration(t *testing.T) {
+	if testing.Short() {
+		t.Skip("skipping integration test")
+	}
+
+	credJSON, err := GetValue(credentialSuspensionContext, "cred")
+	assert.NoError(t, err)
+	assert.NotEmpty(t, credJSON)
+
+	var vc credential.VerifiableCredential
+	err = json.Unmarshal([]byte(credJSON.(string)), &vc)
+	assert.NoError(t, err)
+	assert.NotEmpty(t, vc)
+
+	statusListCredentialURL, err := GetValue(credentialSuspensionContext, "statusListCredentialURL")
+	assert.NoError(t, err)
+	assert.NotEmpty(t, statusListCredentialURL)
+
+	credStatusListCredentialOutput, err := get(statusListCredentialURL.(string))
+	assert.NoError(t, err)
+	assert.NotEmpty(t, credStatusListCredentialOutput)
+
+	credStatusListCredentialJSON, err := getJSONElement(credStatusListCredentialOutput, "$.credential")
+	assert.NoError(t, err)
+	assert.NotEmpty(t, credStatusListCredentialJSON)
+
+	var vcStatusList credential.VerifiableCredential
+	err = json.Unmarshal([]byte(credStatusListCredentialJSON), &vcStatusList)
+	assert.NoError(t, err)
+	assert.NotEmpty(t, vcStatusList)
+
+	// Validate the StatusListIndex in flipped in the credStatusList
+	valid, err := status.ValidateCredentialInStatusList(vc, vcStatusList)
+	assert.NoError(t, err)
+	assert.True(t, valid)
+}
+
+func TestSuspensionUnSuspendCredential(t *testing.T) {
+	if testing.Short() {
+		t.Skip("skipping integration test")
+	}
+
+	credStatusURL, err := GetValue(credentialSuspensionContext, "credStatusURL")
+	assert.NoError(t, err)
+	assert.NotEmpty(t, credStatusURL)
+
+	credStatusOutput, err := get(credStatusURL.(string))
+	assert.NoError(t, err)
+	assert.NotEmpty(t, credStatusOutput)
+
+	suspended, err := getJSONElement(credStatusOutput, "$.suspended")
+	assert.NoError(t, err)
+	assert.Equal(t, "true", suspended)
+
+	suspendedOutput, err := put(credStatusURL.(string), `{"suspended":false}`)
+	assert.NoError(t, err)
+
+	suspended, err = getJSONElement(suspendedOutput, "$.suspended")
+	assert.NoError(t, err)
+	assert.Equal(t, "false", suspended)
+
+	credStatusOutput, err = get(credStatusURL.(string))
+	assert.NoError(t, err)
+	assert.NotEmpty(t, credStatusOutput)
+
+	suspended, err = getJSONElement(credStatusOutput, "$.suspended")
+	assert.NoError(t, err)
+	assert.Equal(t, "false", suspended)
+}
diff --git a/integration/didion_integration_test.go b/integration/didion_integration_test.go
index 6817724..56567bd 100644
--- a/integration/didion_integration_test.go
+++ b/integration/didion_integration_test.go
@@ -106,7 +106,7 @@ func TestDIDIONCreateVerifiableCredentialIntegration(t *testing.T) {
 		IssuerKID: issuerKID.(string),
 		SchemaID:  schemaID.(string),
 		SubjectID: issuerDID.(string),
-	}, false)
+	})
 	assert.NoError(t, err)
 	assert.NotEmpty(t, vcOutput)
 
diff --git a/integration/didweb_integration_test.go b/integration/didweb_integration_test.go
index eacee8f..7ee3180 100644
--- a/integration/didweb_integration_test.go
+++ b/integration/didweb_integration_test.go
@@ -91,7 +91,7 @@ func TestDIDWebCreateVerifiableCredentialIntegration(t *testing.T) {
 		IssuerKID: issuerKID.(string),
 		SchemaID:  schemaID.(string),
 		SubjectID: issuerDID.(string),
-	}, false)
+	})
 	assert.NoError(t, err)
 	assert.NotEmpty(t, vcOutput)
 
diff --git a/integration/testdata/credential-input.json b/integration/testdata/credential-input.json
index d38469c..f528e15 100644
--- a/integration/testdata/credential-input.json
+++ b/integration/testdata/credential-input.json
@@ -11,12 +11,14 @@
       "postalCode": "78724",
       "streetAddress": "123 Janktopia Ave."
     },
-    "taxID":"123"
+    "taxId":"123"
   },
   "issuer": "{{.IssuerID}}",
   "issuerKid": "{{.IssuerKID}}",
   "subject": "{{.SubjectID}}",
   "@context":"https://www.w3.org/2018/credentials/v1",
   "expiry": "2051-10-05T14:48:00.000Z",
-  "schemaId": "{{.SchemaID}}"
+  "schemaId": "{{.SchemaID}}",
+  "revocable": {{.Revocable}},
+  "suspendable": {{.Suspendable}}
 }
\ No newline at end of file
diff --git a/integration/testdata/credential-revocable-input.json b/integration/testdata/credential-revocable-input.json
deleted file mode 100644
index 7560dfa..0000000
--- a/integration/testdata/credential-revocable-input.json
+++ /dev/null
@@ -1,23 +0,0 @@
-{
-  "data": {
-    "givenName":"ricky bobby",
-    "additionalName":"hank hill",
-    "familyName":"simpson",
-    "birthDate":"2009-01-03",
-    "postalAddress": {
-      "addressCountry": "U.S.A",
-      "addressLocality": "Austin",
-      "addressRegion": "TX",
-      "postalCode": "78724",
-      "streetAddress": "123 Janktopia Ave."
-    },
-    "taxID":"123"
-  },
-  "issuer": "{{.IssuerID}}",
-  "issuerKid": "{{.IssuerKID}}",
-  "subject": "{{.SubjectID}}",
-  "@context":"https://www.w3.org/2018/credentials/v1",
-  "expiry": "2051-10-05T14:48:00.000Z",
-  "schemaId": "{{.SchemaID}}",
-  "revocable": true
-}
\ No newline at end of file
diff --git a/integration/testdata/manifest-input.json b/integration/testdata/manifest-input.json
index 9800bb2..84ed328 100644
--- a/integration/testdata/manifest-input.json
+++ b/integration/testdata/manifest-input.json
@@ -74,9 +74,9 @@
               }
             },
             {
-              "id":"taxID",
+              "id":"taxId",
               "path":[
-                "$.vc.credentialSubject.taxID"
+                "$.vc.credentialSubject.taxId"
               ],
               "filter":{
                 "type":"string"
diff --git a/integration/testdata/schema-input.json b/integration/testdata/schema-input.json
index 9598721..4bbeb0e 100644
--- a/integration/testdata/schema-input.json
+++ b/integration/testdata/schema-input.json
@@ -42,7 +42,7 @@
           }
         }
       },
-      "taxID":{
+      "taxId":{
         "type":"string"
       }
     },
diff --git a/integration/testdata/suspended-input.json b/integration/testdata/suspended-input.json
new file mode 100644
index 0000000..e1ef0fb
--- /dev/null
+++ b/integration/testdata/suspended-input.json
@@ -0,0 +1,3 @@
+{
+  "suspended":true
+}
\ No newline at end of file
diff --git a/pkg/server/router/credential_test.go b/pkg/server/router/credential_test.go
index 3bdee70..d1d928b 100644
--- a/pkg/server/router/credential_test.go
+++ b/pkg/server/router/credential_test.go
@@ -543,6 +543,141 @@ func TestCredentialRouter(t *testing.T) {
 		assert.NoError(tt, err)
 		assert.Equal(tt, updatedStatus.Revoked, true)
 
+		updatedCred, err := credService.GetCredential(context.Background(), credential.GetCredentialRequest{ID: createdCred.ID})
+		assert.NoError(tt, err)
+		assert.Equal(tt, updatedCred.Revoked, true)
+
+		credStatusListAfterRevoke, err := credService.GetCredentialStatusList(context.Background(), credential.GetCredentialStatusListRequest{ID: credStatusListID})
+		assert.NoError(tt, err)
+		assert.Equal(tt, credStatusListAfterRevoke.Credential.ID, statusEntry.StatusListCredential)
+
+		// Validate the StatusListIndex in flipped in the credStatusList
+		valid, err = status.ValidateCredentialInStatusList(*createdCred.Credential, *credStatusListAfterRevoke.Credential)
+		assert.NoError(tt, err)
+		assert.True(tt, valid)
+
+		credentialSubjectAfterRevoke := credStatusListAfterRevoke.Container.Credential.CredentialSubject
+		assert.NotEmpty(tt, credentialSubjectAfterRevoke)
+
+		encodedListAfterRevoke := credentialSubjectAfterRevoke["encodedList"]
+		assert.NotEmpty(tt, encodedListAfterRevoke)
+
+		assert.NotEqualValues(tt, encodedListAfterRevoke, encodedList)
+
+	})
+
+	t.Run("Credential Status List Test Update Suspended Status", func(tt *testing.T) {
+		bolt := setupTestDB(tt)
+		assert.NotEmpty(tt, bolt)
+
+		serviceConfig := config.CredentialServiceConfig{BaseServiceConfig: &config.BaseServiceConfig{Name: "credential", ServiceEndpoint: "http://localhost:1234"}}
+		keyStoreService := testKeyStoreService(tt, bolt)
+		didService := testDIDService(tt, bolt, keyStoreService)
+		schemaService := testSchemaService(tt, bolt, keyStoreService, didService)
+		credService, err := credential.NewCredentialService(serviceConfig, bolt, keyStoreService, didService.GetResolver(), schemaService)
+		assert.NoError(tt, err)
+		assert.NotEmpty(tt, credService)
+		// check type and status
+		assert.Equal(tt, framework.Credential, credService.Type())
+		assert.Equal(tt, framework.StatusReady, credService.Status().Status)
+
+		// create a did
+		issuerDID, err := didService.CreateDIDByMethod(context.Background(), did.CreateDIDRequest{Method: didsdk.KeyMethod, KeyType: crypto.Ed25519})
+		assert.NoError(tt, err)
+		assert.NotEmpty(tt, issuerDID)
+
+		// create a schema
+		emailSchema := map[string]any{
+			"type": "object",
+			"properties": map[string]any{
+				"email": map[string]any{
+					"type": "string",
+				},
+			},
+			"required":             []any{"email"},
+			"additionalProperties": false,
+		}
+
+		createdSchema, err := schemaService.CreateSchema(context.Background(), schema.CreateSchemaRequest{Author: issuerDID.DID.ID, Name: "simple schema", Schema: emailSchema})
+		assert.NoError(tt, err)
+		assert.NotEmpty(tt, createdSchema)
+
+		issuer := issuerDID.DID.ID
+		subject := "did:test:345"
+
+		nonSuspendableCred, err := credService.CreateCredential(context.Background(), credential.CreateCredentialRequest{
+			Issuer:    issuer,
+			IssuerKID: issuerDID.DID.VerificationMethod[0].ID,
+			Subject:   subject,
+			SchemaID:  createdSchema.ID,
+			Data: map[string]any{
+				"email": "cant@revoke.me",
+			},
+			Expiry: time.Now().Add(24 * time.Hour).Format(time.RFC3339),
+		})
+		assert.NoError(tt, err)
+
+		_, err = credService.UpdateCredentialStatus(context.Background(), credential.UpdateCredentialStatusRequest{ID: nonSuspendableCred.ID, Suspended: true})
+		assert.ErrorContains(tt, err, "has no credentialStatus field")
+
+		createdCred, err := credService.CreateCredential(context.Background(), credential.CreateCredentialRequest{
+			Issuer:    issuer,
+			IssuerKID: issuerDID.DID.VerificationMethod[0].ID,
+			Subject:   subject,
+			SchemaID:  createdSchema.ID,
+			Data: map[string]any{
+				"email": "Satoshi@Nakamoto.btc",
+			},
+			Expiry:      time.Now().Add(24 * time.Hour).Format(time.RFC3339),
+			Suspendable: true,
+		})
+
+		assert.NoError(tt, err)
+		assert.NotEmpty(tt, createdCred)
+		assert.NotEmpty(tt, createdCred.CredentialJWT)
+
+		statusBytes, err := json.Marshal(createdCred.Credential.CredentialStatus)
+		assert.NoError(tt, err)
+
+		var statusEntry status.StatusList2021Entry
+		err = json.Unmarshal(statusBytes, &statusEntry)
+		assert.NoError(tt, err)
+
+		assert.Contains(tt, statusEntry.ID, fmt.Sprintf("http://localhost:1234/v1/credentials/%s/status", createdCred.ID))
+		assert.Contains(tt, statusEntry.StatusListCredential, "http://localhost:1234/v1/credentials/status")
+		assert.NotEmpty(tt, statusEntry.StatusListIndex)
+
+		credStatus, err := credService.GetCredentialStatus(context.Background(), credential.GetCredentialStatusRequest{ID: createdCred.ID})
+		assert.NoError(tt, err)
+		assert.Equal(tt, credStatus.Suspended, false)
+
+		credStatusListStr := statusEntry.StatusListCredential
+
+		_, credStatusListID, ok := strings.Cut(credStatusListStr, "/v1/credentials/status/")
+		assert.True(tt, ok)
+		credStatusList, err := credService.GetCredentialStatusList(context.Background(), credential.GetCredentialStatusListRequest{ID: credStatusListID})
+		assert.NoError(tt, err)
+		assert.Equal(tt, credStatusList.Credential.ID, statusEntry.StatusListCredential)
+
+		credentialSubject := credStatusList.Container.Credential.CredentialSubject
+		assert.NotEmpty(tt, credentialSubject)
+
+		encodedList := credentialSubject["encodedList"]
+		assert.NotEmpty(tt, encodedList)
+
+		// Validate the StatusListIndex is not flipped in the credStatusList
+		valid, err := status.ValidateCredentialInStatusList(*createdCred.Credential, *credStatusList.Credential)
+		assert.NoError(tt, err)
+		assert.False(tt, valid)
+
+		updatedStatus, err := credService.UpdateCredentialStatus(context.Background(), credential.UpdateCredentialStatusRequest{ID: createdCred.ID, Suspended: true})
+		assert.NoError(tt, err)
+		assert.Equal(tt, updatedStatus.Suspended, true)
+
+		updatedCred, err := credService.GetCredential(context.Background(), credential.GetCredentialRequest{ID: createdCred.ID})
+		assert.NoError(tt, err)
+		assert.Equal(tt, updatedCred.Suspended, true)
+
 		credStatusListAfterRevoke, err := credService.GetCredentialStatusList(context.Background(), credential.GetCredentialStatusListRequest{ID: credStatusListID})
 		assert.NoError(tt, err)
 		assert.Equal(tt, credStatusListAfterRevoke.Credential.ID, statusEntry.StatusListCredential)