diff --git a/pkg/server/router/credential_test.go b/pkg/server/router/credential_test.go index ffae661a5..14a417312 100644 --- a/pkg/server/router/credential_test.go +++ b/pkg/server/router/credential_test.go @@ -19,1016 +19,1023 @@ import ( "github.com/tbd54566975/ssi-service/pkg/service/did" "github.com/tbd54566975/ssi-service/pkg/service/framework" "github.com/tbd54566975/ssi-service/pkg/service/schema" + "github.com/tbd54566975/ssi-service/pkg/storage" + "github.com/tbd54566975/ssi-service/pkg/testutil" ) func TestCredentialRouter(t *testing.T) { - t.Run("Nil Service", func(tt *testing.T) { - credRouter, err := NewCredentialRouter(nil) - assert.Error(tt, err) - assert.Empty(tt, credRouter) - assert.Contains(tt, err.Error(), "service cannot be nil") - }) - - t.Run("Bad Service", func(tt *testing.T) { - credRouter, err := NewCredentialRouter(&testService{}) - assert.Error(tt, err) - assert.Empty(tt, credRouter) - assert.Contains(tt, err.Error(), "could not create credential router with service type: test") - }) - - t.Run("Credential Service Test", func(tt *testing.T) { - bolt := setupTestDB(tt) - assert.NotEmpty(tt, bolt) - - serviceConfig := config.CredentialServiceConfig{BaseServiceConfig: &config.BaseServiceConfig{Name: "credential"}} - 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 credential - - issuerDID, err := didService.CreateDIDByMethod(context.Background(), did.CreateDIDRequest{Method: didsdk.KeyMethod, KeyType: crypto.Ed25519}) - assert.NoError(tt, err) - assert.NotEmpty(tt, issuerDID) - - issuer := issuerDID.DID.ID - subject := "did:test:345" - createdCred, err := credService.CreateCredential(context.Background(), credential.CreateCredentialRequest{ - Issuer: issuer, - IssuerKID: issuerDID.DID.VerificationMethod[0].ID, - Subject: subject, - Data: map[string]any{ - "firstName": "Satoshi", - "lastName": "Nakamoto", - }, - Expiry: time.Now().Add(24 * time.Hour).Format(time.RFC3339), - }) - assert.NoError(tt, err) - assert.NotEmpty(tt, createdCred) - assert.NotEmpty(tt, createdCred.CredentialJWT) - - cred := createdCred.Credential - - // make sure it has the right data - assert.Equal(tt, issuer, cred.Issuer) - assert.Equal(tt, subject, cred.CredentialSubject[credsdk.VerifiableCredentialIDProperty]) - assert.Equal(tt, "Satoshi", cred.CredentialSubject["firstName"]) - assert.Equal(tt, "Nakamoto", cred.CredentialSubject["lastName"]) - - // get it back - gotCred, err := credService.GetCredential(context.Background(), credential.GetCredentialRequest{ID: idFromURI(cred.ID)}) - assert.NoError(tt, err) - assert.NotEmpty(tt, gotCred) - - // compare for object equality - assert.Equal(tt, createdCred.CredentialJWT, gotCred.CredentialJWT) - - // verify it - verified, err := credService.VerifyCredential(context.Background(), credential.VerifyCredentialRequest{CredentialJWT: gotCred.CredentialJWT}) - assert.NoError(tt, err) - assert.True(tt, verified.Verified) - - // get a cred that doesn't exist - _, err = credService.GetCredential(context.Background(), credential.GetCredentialRequest{ID: "bad"}) - assert.Error(tt, err) - assert.Contains(tt, err.Error(), "credential not found with id: bad") - - // get by schema - no schema - bySchema, err := credService.ListCredentialsBySchema(context.Background(), credential.ListCredentialBySchemaRequest{Schema: ""}) - assert.NoError(tt, err) - assert.Len(tt, bySchema.Credentials, 1) - assert.EqualValues(tt, cred.CredentialSchema, bySchema.Credentials[0].Credential.CredentialSchema) - - // get by subject - bySubject, err := credService.ListCredentialsBySubject(context.Background(), credential.ListCredentialBySubjectRequest{Subject: subject}) - assert.NoError(tt, err) - assert.Len(tt, bySubject.Credentials, 1) - - assert.Equal(tt, cred.ID, bySubject.Credentials[0].ID) - assert.Equal(tt, cred.CredentialSubject[credsdk.VerifiableCredentialIDProperty], bySubject.Credentials[0].Credential.CredentialSubject[credsdk.VerifiableCredentialIDProperty]) - - // get by issuer - byIssuer, err := credService.ListCredentialsByIssuer(context.Background(), credential.ListCredentialByIssuerRequest{Issuer: issuer}) - assert.NoError(tt, err) - assert.Len(tt, byIssuer.Credentials, 1) - - assert.Equal(tt, cred.ID, byIssuer.Credentials[0].Credential.ID) - assert.Equal(tt, cred.Issuer, byIssuer.Credentials[0].Credential.Issuer) - - // create another cred with the same issuer, different subject, different schema that doesn't exist - _, err = credService.CreateCredential(context.Background(), credential.CreateCredentialRequest{ - Issuer: issuer, - IssuerKID: issuerDID.DID.VerificationMethod[0].ID, - Subject: "did:abcd:efghi", - SchemaID: "https://test-schema.com", - Data: map[string]any{ - "email": "satoshi@nakamoto.com", - }, - Expiry: time.Now().Add(24 * time.Hour).Format(time.RFC3339), - }) - assert.Error(tt, err) - assert.Contains(tt, err.Error(), "schema not found with id: https://test-schema.com") - - // create schema - createdSchema, err := schemaService.CreateSchema(context.Background(), schema.CreateSchemaRequest{Issuer: issuerDID.DID.ID, Name: "simple schema", Schema: getEmailSchema()}) - assert.NoError(tt, err) - assert.NotEmpty(tt, createdSchema) - - // create another cred with the same issuer, different subject, different schema that does exist - createdCredWithSchema, err := credService.CreateCredential(context.Background(), credential.CreateCredentialRequest{ - Issuer: issuer, - IssuerKID: issuerDID.DID.VerificationMethod[0].ID, - Subject: "did:abcd:efghi", - SchemaID: createdSchema.ID, - Data: map[string]any{ - "email": "satoshi@nakamoto.com", - }, - Expiry: time.Now().Add(24 * time.Hour).Format(time.RFC3339), - }) - assert.NoError(tt, err) - assert.NotEmpty(tt, createdCredWithSchema) - - // get by issuer - byIssuer, err = credService.ListCredentialsByIssuer(context.Background(), credential.ListCredentialByIssuerRequest{Issuer: issuer}) - assert.NoError(tt, err) - assert.Len(tt, byIssuer.Credentials, 2) - - // make sure the schema and subject queries are consistent - bySchema, err = credService.ListCredentialsBySchema(context.Background(), credential.ListCredentialBySchemaRequest{Schema: ""}) - assert.NoError(tt, err) - assert.Len(tt, bySchema.Credentials, 1) - - assert.Equal(tt, cred.ID, bySchema.Credentials[0].ID) - assert.EqualValues(tt, cred.CredentialSchema, bySchema.Credentials[0].Credential.CredentialSchema) - - bySubject, err = credService.ListCredentialsBySubject(context.Background(), credential.ListCredentialBySubjectRequest{Subject: subject}) - assert.NoError(tt, err) - assert.Len(tt, bySubject.Credentials, 1) - - assert.Equal(tt, cred.ID, bySubject.Credentials[0].ID) - assert.Equal(tt, cred.CredentialSubject[credsdk.VerifiableCredentialIDProperty], bySubject.Credentials[0].Credential.CredentialSubject[credsdk.VerifiableCredentialIDProperty]) - - // delete a cred that doesn't exist (no error since idempotent) - err = credService.DeleteCredential(context.Background(), credential.DeleteCredentialRequest{ID: "bad"}) - assert.NoError(tt, err) - - // delete a credential that does exist - err = credService.DeleteCredential(context.Background(), credential.DeleteCredentialRequest{ID: cred.ID}) - assert.NoError(tt, err) - - // get it back - _, err = credService.GetCredential(context.Background(), credential.GetCredentialRequest{ID: cred.ID}) - assert.Error(tt, err) - assert.Contains(tt, err.Error(), fmt.Sprintf("credential not found with id: %s", cred.ID)) - }) - - t.Run("Credential Status List Test", func(tt *testing.T) { - bolt := setupTestDB(tt) - assert.NotEmpty(tt, bolt) - - serviceConfig := config.CredentialServiceConfig{BaseServiceConfig: &config.BaseServiceConfig{Name: "credential", ServiceEndpoint: "v1/credentials"}} - 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 - createdSchema, err := schemaService.CreateSchema(context.Background(), schema.CreateSchemaRequest{Issuer: issuerDID.DID.ID, Name: "simple schema", Schema: getEmailSchema()}) - assert.NoError(tt, err) - assert.NotEmpty(tt, createdSchema) - - issuer := issuerDID.DID.ID - subject := "did:test:345" - - 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), - Revocable: true, - }) - - assert.NoError(tt, err) - assert.NotEmpty(tt, createdCred) - assert.NotEmpty(tt, createdCred.CredentialJWT) - - credStatusMap, ok := createdCred.Credential.CredentialStatus.(map[string]any) - assert.True(tt, ok) - - assert.Equal(tt, credStatusMap["id"], createdCred.ID+"/status") - assert.Contains(tt, credStatusMap["statusListCredential"], "v1/credentials/status") - assert.NotEmpty(tt, credStatusMap["statusListIndex"]) - - createdCredTwo, 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": "Satoshi2@Nakamoto2.btc", - }, - Expiry: time.Now().Add(24 * time.Hour).Format(time.RFC3339), - Revocable: true, - }) - - assert.NoError(tt, err) - assert.NotEmpty(tt, createdCredTwo) - assert.NotEmpty(tt, createdCredTwo.CredentialJWT) - - credStatusMapTwo, ok := createdCredTwo.Credential.CredentialStatus.(map[string]any) - assert.True(tt, ok) - - assert.Equal(tt, credStatusMapTwo["id"], createdCredTwo.ID+"/status") - assert.Contains(tt, credStatusMapTwo["statusListCredential"], "v1/credentials/status") - assert.NotEmpty(tt, credStatusMapTwo["statusListIndex"]) - - // Cred with same pair share the same statusListCredential - assert.Equal(tt, credStatusMapTwo["statusListCredential"], credStatusMap["statusListCredential"]) - - createdSchemaTwo, err := schemaService.CreateSchema(context.Background(), schema.CreateSchemaRequest{Issuer: issuerDID.DID.ID, Name: "simple schema", Schema: getEmailSchema()}) - assert.NoError(tt, err) - assert.NotEmpty(tt, createdSchemaTwo) - - createdCredThree, err := credService.CreateCredential(context.Background(), credential.CreateCredentialRequest{ - Issuer: issuer, - IssuerKID: issuerDID.DID.VerificationMethod[0].ID, - Subject: subject, - SchemaID: createdSchemaTwo.ID, - Data: map[string]any{ - "email": "Satoshi2@Nakamoto2.btc", - }, - Expiry: time.Now().Add(24 * time.Hour).Format(time.RFC3339), - Revocable: true, - }) - - assert.NoError(tt, err) - assert.NotEmpty(tt, createdCredThree) - assert.NotEmpty(tt, createdCredThree.CredentialJWT) - - credStatusMapThree, ok := createdCredThree.Credential.CredentialStatus.(map[string]any) - assert.True(tt, ok) - - assert.Contains(tt, credStatusMapThree["id"], createdCredThree.ID) - assert.Contains(tt, credStatusMapThree["statusListCredential"], "v1/credentials/status") - assert.NotEmpty(tt, credStatusMapThree["statusListIndex"]) - - // Cred with different pair have different statusListCredential - assert.NotEqual(tt, credStatusMapThree["statusListCredential"], credStatusMap["statusListCredential"]) - }) - - t.Run("Credential Status List Test No Schemas", func(tt *testing.T) { - bolt := setupTestDB(tt) - assert.NotEmpty(tt, bolt) - - serviceConfig := config.CredentialServiceConfig{BaseServiceConfig: &config.BaseServiceConfig{Name: "credential", ServiceEndpoint: "/v1/credentials"}} - 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) - - issuer := issuerDID.DID.ID - subject := "did:test:345" - - createdCred, err := credService.CreateCredential(context.Background(), credential.CreateCredentialRequest{ - Issuer: issuer, - IssuerKID: issuerDID.DID.VerificationMethod[0].ID, - Subject: subject, - Data: map[string]any{ - "email": "Satoshi@Nakamoto.btc", - }, - Expiry: time.Now().Add(24 * time.Hour).Format(time.RFC3339), - Revocable: true, - }) - - assert.NoError(tt, err) - assert.NotEmpty(tt, createdCred) - assert.NotEmpty(tt, createdCred.CredentialJWT) - - credStatusMap, ok := createdCred.Credential.CredentialStatus.(map[string]any) - assert.True(tt, ok) - - assert.Contains(tt, credStatusMap["id"], fmt.Sprintf("%s/status", createdCred.ID)) - assert.Contains(tt, credStatusMap["statusListCredential"], "v1/credentials/status") - assert.NotEmpty(tt, credStatusMap["statusListIndex"]) - - createdCredTwo, err := credService.CreateCredential(context.Background(), credential.CreateCredentialRequest{ - Issuer: issuer, - IssuerKID: issuerDID.DID.VerificationMethod[0].ID, - Subject: subject, - Data: map[string]any{ - "email": "Satoshi2@Nakamoto2.btc", - }, - Expiry: time.Now().Add(24 * time.Hour).Format(time.RFC3339), - Revocable: true, - }) - - assert.NoError(tt, err) - assert.NotEmpty(tt, createdCredTwo) - assert.NotEmpty(tt, createdCredTwo.CredentialJWT) - - credStatusMapTwo, ok := createdCredTwo.Credential.CredentialStatus.(map[string]any) - assert.True(tt, ok) - - assert.Contains(tt, credStatusMapTwo["id"], fmt.Sprintf("%s/status", createdCredTwo.ID)) - assert.Contains(tt, credStatusMapTwo["statusListCredential"], "v1/credentials/status") - assert.NotEmpty(tt, credStatusMapTwo["statusListIndex"]) - - // Cred with same pair share the same statusListCredential - assert.Equal(tt, credStatusMapTwo["statusListCredential"], credStatusMap["statusListCredential"]) - - // create schema - createdSchema, err := schemaService.CreateSchema(context.Background(), schema.CreateSchemaRequest{Issuer: issuerDID.DID.ID, Name: "simple schema", Schema: getEmailSchema()}) - assert.NoError(tt, err) - assert.NotEmpty(tt, createdSchema) - - createdCredThree, 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": "Satoshi2@Nakamoto2.btc", - }, - Expiry: time.Now().Add(24 * time.Hour).Format(time.RFC3339), - Revocable: true, - }) - - assert.NoError(tt, err) - assert.NotEmpty(tt, createdCredThree) - assert.NotEmpty(tt, createdCredThree.CredentialJWT) - - credStatusMapThree, ok := createdCredThree.Credential.CredentialStatus.(map[string]any) - assert.True(tt, ok) - - assert.Contains(tt, credStatusMapThree["id"], fmt.Sprintf("%s/status", createdCredThree.ID)) - assert.Contains(tt, credStatusMapThree["statusListCredential"], "v1/credentials/status") - assert.NotEmpty(tt, credStatusMapThree["statusListIndex"]) - - // Cred with different pair have different statusListCredential - assert.NotEqual(tt, credStatusMapThree["statusListCredential"], credStatusMap["statusListCredential"]) - }) - - t.Run("Credential Status List Test Update Revoked Status", func(tt *testing.T) { - bolt := setupTestDB(tt) - assert.NotEmpty(tt, bolt) - - serviceConfig := config.CredentialServiceConfig{BaseServiceConfig: &config.BaseServiceConfig{Name: "credential", ServiceEndpoint: "http://localhost:1234/v1/credentials"}} - 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 - createdSchema, err := schemaService.CreateSchema(context.Background(), schema.CreateSchemaRequest{Issuer: issuerDID.DID.ID, Name: "simple schema", Schema: getEmailSchema()}) - assert.NoError(tt, err) - assert.NotEmpty(tt, createdSchema) - - issuer := issuerDID.DID.ID - subject := "did:test:345" - - nonRevokableCred, 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: idFromURI(nonRevokableCred.ID), Revoked: 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), - Revocable: 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("%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: idFromURI(createdCred.ID)}) - assert.NoError(tt, err) - assert.Equal(tt, credStatus.Revoked, 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: idFromURI(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: idFromURI(createdCred.ID), Revoked: true}) - assert.NoError(tt, err) - assert.Equal(tt, updatedStatus.Revoked, true) - - updatedCred, err := credService.GetCredential(context.Background(), credential.GetCredentialRequest{ID: idFromURI(createdCred.ID)}) - assert.NoError(tt, err) - assert.Equal(tt, updatedCred.Revoked, true) - - credStatusListAfterRevoke, err := credService.GetCredentialStatusList(context.Background(), credential.GetCredentialStatusListRequest{ID: idFromURI(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/v1/credentials"}} - 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 - createdSchema, err := schemaService.CreateSchema(context.Background(), schema.CreateSchemaRequest{Issuer: issuerDID.DID.ID, Name: "simple schema", Schema: getEmailSchema()}) - 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: idFromURI(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("%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: idFromURI(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: idFromURI(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: idFromURI(createdCred.ID), Suspended: true}) - assert.NoError(tt, err) - assert.Equal(tt, updatedStatus.Suspended, true) - - updatedCred, err := credService.GetCredential(context.Background(), credential.GetCredentialRequest{ID: idFromURI(createdCred.ID)}) - assert.NoError(tt, err) - assert.Equal(tt, updatedCred.Suspended, true) - - credStatusListAfterRevoke, err := credService.GetCredentialStatusList(context.Background(), credential.GetCredentialStatusListRequest{ID: idFromURI(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("Create Multiple Suspendable Credential Different IssuerDID SchemaID StatusPurpose Triples", func(tt *testing.T) { - bolt := setupTestDB(tt) - assert.NotEmpty(tt, bolt) - - serviceConfig := config.CredentialServiceConfig{BaseServiceConfig: &config.BaseServiceConfig{Name: "credential", ServiceEndpoint: "http://localhost:1234/v1/credentials"}} - 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 - createdSchema, err := schemaService.CreateSchema(context.Background(), schema.CreateSchemaRequest{Issuer: issuerDID.DID.ID, Name: "simple schema", Schema: getEmailSchema()}) - assert.NoError(tt, err) - assert.NotEmpty(tt, createdSchema) - - subject := "did:test:345" - - createdCred, err := credService.CreateCredential(context.Background(), credential.CreateCredentialRequest{ - Issuer: issuerDID.DID.ID, - 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), - Revocable: true, - }) - assert.NoError(tt, err) - assert.NotEmpty(tt, createdCred) - - createdCredSuspendable, err := credService.CreateCredential(context.Background(), credential.CreateCredentialRequest{ - Issuer: issuerDID.DID.ID, - 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, createdCredSuspendable) - - revocationKey := strings.Join([]string{"is:" + issuerDID.DID.ID, "sc:" + createdSchema.ID, "sp:" + string(status.StatusRevocation)}, "-") - - slcExists, err := bolt.Exists(context.Background(), "status-list-credential", revocationKey) - assert.NoError(tt, err) - assert.True(tt, slcExists) - - indexPoolExists, err := bolt.Exists(context.Background(), "status-list-index-pool", revocationKey) - assert.NoError(tt, err) - assert.True(tt, indexPoolExists) - - currentIndexExists, err := bolt.Exists(context.Background(), "status-list-current-index", revocationKey) - assert.NoError(tt, err) - assert.True(tt, currentIndexExists) - - suspensionKey := strings.Join([]string{"is:" + issuerDID.DID.ID, "sc:" + createdSchema.ID, "sp:" + string(status.StatusSuspension)}, "-") - - slcExists, err = bolt.Exists(context.Background(), "status-list-credential", suspensionKey) - assert.NoError(tt, err) - assert.True(tt, slcExists) - - indexPoolExists, err = bolt.Exists(context.Background(), "status-list-index-pool", suspensionKey) - assert.NoError(tt, err) - assert.True(tt, indexPoolExists) - - currentIndexExists, err = bolt.Exists(context.Background(), "status-list-current-index", suspensionKey) - assert.NoError(tt, err) - assert.True(tt, currentIndexExists) - }) - - t.Run("Create Suspendable Credential", func(tt *testing.T) { - issuer, issuerKID, schemaID, credService := createCredServicePrereqs(tt) - subject := "did:test:345" - - createdCred, err := credService.CreateCredential(context.Background(), credential.CreateCredentialRequest{ - Issuer: issuer, - IssuerKID: issuerKID, - Subject: subject, - SchemaID: schemaID, - 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("%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: idFromURI(createdCred.ID)}) - assert.NoError(tt, err) - assert.Equal(tt, credStatus.Revoked, false) - 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: idFromURI(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) - }) - - t.Run("Update Suspendable Credential To Suspended", func(tt *testing.T) { - issuer, issuerKID, schemaID, credService := createCredServicePrereqs(tt) - subject := "did:test:345" - - createdCred, err := credService.CreateCredential(context.Background(), credential.CreateCredentialRequest{ - Issuer: issuer, - IssuerKID: issuerKID, - Subject: subject, - SchemaID: schemaID, - 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("%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: idFromURI(createdCred.ID)}) - assert.NoError(tt, err) - assert.Equal(tt, credStatus.Revoked, false) - 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: idFromURI(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: idFromURI(createdCred.ID), Suspended: true}) - assert.NoError(tt, err) - assert.Equal(tt, updatedStatus.Suspended, true) - assert.Equal(tt, updatedStatus.Revoked, false) - - credStatusListAfterRevoke, err := credService.GetCredentialStatusList(context.Background(), credential.GetCredentialStatusListRequest{ID: idFromURI(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) - - encodedListAfterSuspended := credentialSubjectAfterRevoke["encodedList"] - assert.NotEmpty(tt, encodedListAfterSuspended) - - assert.NotEqualValues(tt, encodedListAfterSuspended, encodedList) - }) - - t.Run("Update Suspendable Credential To Suspended then Unsuspended", func(tt *testing.T) { - issuer, issuerKID, schemaID, credService := createCredServicePrereqs(tt) - subject := "did:test:345" - - createdCred, err := credService.CreateCredential(context.Background(), credential.CreateCredentialRequest{ - Issuer: issuer, - IssuerKID: issuerKID, - Subject: subject, - SchemaID: schemaID, - 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("%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: idFromURI(createdCred.ID)}) - assert.NoError(tt, err) - assert.Equal(tt, credStatus.Revoked, false) - 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: idFromURI(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: idFromURI(createdCred.ID), Suspended: true}) - assert.NoError(tt, err) - assert.Equal(tt, updatedStatus.Suspended, true) - assert.Equal(tt, updatedStatus.Revoked, false) - - credStatusListAfterRevoke, err := credService.GetCredentialStatusList(context.Background(), credential.GetCredentialStatusListRequest{ID: idFromURI(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) - - encodedListAfterSuspended := credentialSubjectAfterRevoke["encodedList"] - assert.NotEmpty(tt, encodedListAfterSuspended) - - assert.NotEqualValues(tt, encodedListAfterSuspended, encodedList) - - updatedStatus, err = credService.UpdateCredentialStatus(context.Background(), credential.UpdateCredentialStatusRequest{ID: idFromURI(createdCred.ID), Suspended: false}) - assert.NoError(tt, err) - assert.Equal(tt, updatedStatus.Suspended, false) - assert.Equal(tt, updatedStatus.Revoked, false) - }) - - t.Run("Create Suspendable and Revocable Credential Should Be Error", func(tt *testing.T) { - issuer, issuerKID, schemaID, credService := createCredServicePrereqs(tt) - subject := "did:test:345" - - createdCred, err := credService.CreateCredential(context.Background(), credential.CreateCredentialRequest{ - Issuer: issuer, - IssuerKID: issuerKID, - Subject: subject, - SchemaID: schemaID, - Data: map[string]any{ - "email": "Satoshi@Nakamoto.btc", - }, - Expiry: time.Now().Add(24 * time.Hour).Format(time.RFC3339), - Revocable: true, - Suspendable: true, - }) - - assert.Error(tt, err) - assert.ErrorContains(tt, err, "credential may have at most one status") - assert.Empty(tt, createdCred) - }) - - t.Run("Update Suspendable and Revocable Credential Should Be Error", func(tt *testing.T) { - issuer, issuerKID, schemaID, credService := createCredServicePrereqs(tt) - subject := "did:test:345" - - createdCred, err := credService.CreateCredential(context.Background(), credential.CreateCredentialRequest{ - Issuer: issuer, - IssuerKID: issuerKID, - Subject: subject, - SchemaID: schemaID, - 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) - - updatedStatus, err := credService.UpdateCredentialStatus(context.Background(), credential.UpdateCredentialStatusRequest{ID: idFromURI(createdCred.ID), Revoked: true, Suspended: true}) - assert.Nil(tt, updatedStatus) - assert.Error(tt, err) - assert.ErrorContains(tt, err, "cannot update both suspended and revoked status") - }) - - t.Run("Update Suspended On Revoked Credential Should Be Error", func(tt *testing.T) { - issuer, issuerKID, schemaID, credService := createCredServicePrereqs(tt) - subject := "did:test:345" - - createdCred, err := credService.CreateCredential(context.Background(), credential.CreateCredentialRequest{ - Issuer: issuer, - IssuerKID: issuerKID, - Subject: subject, - SchemaID: schemaID, - Data: map[string]any{ - "email": "Satoshi@Nakamoto.btc", - }, - Expiry: time.Now().Add(24 * time.Hour).Format(time.RFC3339), - Revocable: true, + for _, test := range testutil.TestDatabases { + t.Run(test.Name, func(t *testing.T) { + + t.Run("Nil Service", func(tt *testing.T) { + credRouter, err := NewCredentialRouter(nil) + assert.Error(tt, err) + assert.Empty(tt, credRouter) + assert.Contains(tt, err.Error(), "service cannot be nil") + }) + + t.Run("Bad Service", func(tt *testing.T) { + credRouter, err := NewCredentialRouter(&testService{}) + assert.Error(tt, err) + assert.Empty(tt, credRouter) + assert.Contains(tt, err.Error(), "could not create credential router with service type: test") + }) + + t.Run("Credential Service Test", func(tt *testing.T) { + s := test.ServiceStorage(t) + assert.NotEmpty(tt, s) + + serviceConfig := config.CredentialServiceConfig{BaseServiceConfig: &config.BaseServiceConfig{Name: "credential"}} + keyStoreService := testKeyStoreService(tt, s) + didService := testDIDService(tt, s, keyStoreService) + schemaService := testSchemaService(tt, s, keyStoreService, didService) + credService, err := credential.NewCredentialService(serviceConfig, s, 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 credential + + issuerDID, err := didService.CreateDIDByMethod(context.Background(), did.CreateDIDRequest{Method: didsdk.KeyMethod, KeyType: crypto.Ed25519}) + assert.NoError(tt, err) + assert.NotEmpty(tt, issuerDID) + + issuer := issuerDID.DID.ID + subject := "did:test:345" + createdCred, err := credService.CreateCredential(context.Background(), credential.CreateCredentialRequest{ + Issuer: issuer, + IssuerKID: issuerDID.DID.VerificationMethod[0].ID, + Subject: subject, + Data: map[string]any{ + "firstName": "Satoshi", + "lastName": "Nakamoto", + }, + Expiry: time.Now().Add(24 * time.Hour).Format(time.RFC3339), + }) + assert.NoError(tt, err) + assert.NotEmpty(tt, createdCred) + assert.NotEmpty(tt, createdCred.CredentialJWT) + + cred := createdCred.Credential + + // make sure it has the right data + assert.Equal(tt, issuer, cred.Issuer) + assert.Equal(tt, subject, cred.CredentialSubject[credsdk.VerifiableCredentialIDProperty]) + assert.Equal(tt, "Satoshi", cred.CredentialSubject["firstName"]) + assert.Equal(tt, "Nakamoto", cred.CredentialSubject["lastName"]) + + // get it back + gotCred, err := credService.GetCredential(context.Background(), credential.GetCredentialRequest{ID: idFromURI(cred.ID)}) + assert.NoError(tt, err) + assert.NotEmpty(tt, gotCred) + + // compare for object equality + assert.Equal(tt, createdCred.CredentialJWT, gotCred.CredentialJWT) + + // verify it + verified, err := credService.VerifyCredential(context.Background(), credential.VerifyCredentialRequest{CredentialJWT: gotCred.CredentialJWT}) + assert.NoError(tt, err) + assert.True(tt, verified.Verified) + + // get a cred that doesn't exist + _, err = credService.GetCredential(context.Background(), credential.GetCredentialRequest{ID: "bad"}) + assert.Error(tt, err) + assert.Contains(tt, err.Error(), "credential not found with id: bad") + + // get by schema - no schema + bySchema, err := credService.ListCredentialsBySchema(context.Background(), credential.ListCredentialBySchemaRequest{Schema: ""}) + assert.NoError(tt, err) + assert.Len(tt, bySchema.Credentials, 1) + assert.EqualValues(tt, cred.CredentialSchema, bySchema.Credentials[0].Credential.CredentialSchema) + + // get by subject + bySubject, err := credService.ListCredentialsBySubject(context.Background(), credential.ListCredentialBySubjectRequest{Subject: subject}) + assert.NoError(tt, err) + assert.Len(tt, bySubject.Credentials, 1) + + assert.Equal(tt, cred.ID, bySubject.Credentials[0].ID) + assert.Equal(tt, cred.CredentialSubject[credsdk.VerifiableCredentialIDProperty], bySubject.Credentials[0].Credential.CredentialSubject[credsdk.VerifiableCredentialIDProperty]) + + // get by issuer + byIssuer, err := credService.ListCredentialsByIssuer(context.Background(), credential.ListCredentialByIssuerRequest{Issuer: issuer}) + assert.NoError(tt, err) + assert.Len(tt, byIssuer.Credentials, 1) + + assert.Equal(tt, cred.ID, byIssuer.Credentials[0].Credential.ID) + assert.Equal(tt, cred.Issuer, byIssuer.Credentials[0].Credential.Issuer) + + // create another cred with the same issuer, different subject, different schema that doesn't exist + _, err = credService.CreateCredential(context.Background(), credential.CreateCredentialRequest{ + Issuer: issuer, + IssuerKID: issuerDID.DID.VerificationMethod[0].ID, + Subject: "did:abcd:efghi", + SchemaID: "https://test-schema.com", + Data: map[string]any{ + "email": "satoshi@nakamoto.com", + }, + Expiry: time.Now().Add(24 * time.Hour).Format(time.RFC3339), + }) + assert.Error(tt, err) + assert.Contains(tt, err.Error(), "schema not found with id: https://test-schema.com") + + // create schema + createdSchema, err := schemaService.CreateSchema(context.Background(), schema.CreateSchemaRequest{Issuer: issuerDID.DID.ID, Name: "simple schema", Schema: getEmailSchema()}) + assert.NoError(tt, err) + assert.NotEmpty(tt, createdSchema) + + // create another cred with the same issuer, different subject, different schema that does exist + createdCredWithSchema, err := credService.CreateCredential(context.Background(), credential.CreateCredentialRequest{ + Issuer: issuer, + IssuerKID: issuerDID.DID.VerificationMethod[0].ID, + Subject: "did:abcd:efghi", + SchemaID: createdSchema.ID, + Data: map[string]any{ + "email": "satoshi@nakamoto.com", + }, + Expiry: time.Now().Add(24 * time.Hour).Format(time.RFC3339), + }) + assert.NoError(tt, err) + assert.NotEmpty(tt, createdCredWithSchema) + + // get by issuer + byIssuer, err = credService.ListCredentialsByIssuer(context.Background(), credential.ListCredentialByIssuerRequest{Issuer: issuer}) + assert.NoError(tt, err) + assert.Len(tt, byIssuer.Credentials, 2) + + // make sure the schema and subject queries are consistent + bySchema, err = credService.ListCredentialsBySchema(context.Background(), credential.ListCredentialBySchemaRequest{Schema: ""}) + assert.NoError(tt, err) + assert.Len(tt, bySchema.Credentials, 1) + + assert.Equal(tt, cred.ID, bySchema.Credentials[0].ID) + assert.EqualValues(tt, cred.CredentialSchema, bySchema.Credentials[0].Credential.CredentialSchema) + + bySubject, err = credService.ListCredentialsBySubject(context.Background(), credential.ListCredentialBySubjectRequest{Subject: subject}) + assert.NoError(tt, err) + assert.Len(tt, bySubject.Credentials, 1) + + assert.Equal(tt, cred.ID, bySubject.Credentials[0].ID) + assert.Equal(tt, cred.CredentialSubject[credsdk.VerifiableCredentialIDProperty], bySubject.Credentials[0].Credential.CredentialSubject[credsdk.VerifiableCredentialIDProperty]) + + // delete a cred that doesn't exist (no error since idempotent) + err = credService.DeleteCredential(context.Background(), credential.DeleteCredentialRequest{ID: "bad"}) + assert.NoError(tt, err) + + // delete a credential that does exist + err = credService.DeleteCredential(context.Background(), credential.DeleteCredentialRequest{ID: cred.ID}) + assert.NoError(tt, err) + + // get it back + _, err = credService.GetCredential(context.Background(), credential.GetCredentialRequest{ID: cred.ID}) + assert.Error(tt, err) + assert.Contains(tt, err.Error(), fmt.Sprintf("credential not found with id: %s", cred.ID)) + }) + + t.Run("Credential Status List Test", func(tt *testing.T) { + s := test.ServiceStorage(t) + assert.NotEmpty(tt, s) + + serviceConfig := config.CredentialServiceConfig{BaseServiceConfig: &config.BaseServiceConfig{Name: "credential", ServiceEndpoint: "v1/credentials"}} + keyStoreService := testKeyStoreService(tt, s) + didService := testDIDService(tt, s, keyStoreService) + schemaService := testSchemaService(tt, s, keyStoreService, didService) + credService, err := credential.NewCredentialService(serviceConfig, s, 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 + createdSchema, err := schemaService.CreateSchema(context.Background(), schema.CreateSchemaRequest{Issuer: issuerDID.DID.ID, Name: "simple schema", Schema: getEmailSchema()}) + assert.NoError(tt, err) + assert.NotEmpty(tt, createdSchema) + + issuer := issuerDID.DID.ID + subject := "did:test:345" + + 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), + Revocable: true, + }) + + assert.NoError(tt, err) + assert.NotEmpty(tt, createdCred) + assert.NotEmpty(tt, createdCred.CredentialJWT) + + credStatusMap, ok := createdCred.Credential.CredentialStatus.(map[string]any) + assert.True(tt, ok) + + assert.Equal(tt, credStatusMap["id"], createdCred.ID+"/status") + assert.Contains(tt, credStatusMap["statusListCredential"], "v1/credentials/status") + assert.NotEmpty(tt, credStatusMap["statusListIndex"]) + + createdCredTwo, 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": "Satoshi2@Nakamoto2.btc", + }, + Expiry: time.Now().Add(24 * time.Hour).Format(time.RFC3339), + Revocable: true, + }) + + assert.NoError(tt, err) + assert.NotEmpty(tt, createdCredTwo) + assert.NotEmpty(tt, createdCredTwo.CredentialJWT) + + credStatusMapTwo, ok := createdCredTwo.Credential.CredentialStatus.(map[string]any) + assert.True(tt, ok) + + assert.Equal(tt, credStatusMapTwo["id"], createdCredTwo.ID+"/status") + assert.Contains(tt, credStatusMapTwo["statusListCredential"], "v1/credentials/status") + assert.NotEmpty(tt, credStatusMapTwo["statusListIndex"]) + + // Cred with same pair share the same statusListCredential + assert.Equal(tt, credStatusMapTwo["statusListCredential"], credStatusMap["statusListCredential"]) + + createdSchemaTwo, err := schemaService.CreateSchema(context.Background(), schema.CreateSchemaRequest{Issuer: issuerDID.DID.ID, Name: "simple schema", Schema: getEmailSchema()}) + assert.NoError(tt, err) + assert.NotEmpty(tt, createdSchemaTwo) + + createdCredThree, err := credService.CreateCredential(context.Background(), credential.CreateCredentialRequest{ + Issuer: issuer, + IssuerKID: issuerDID.DID.VerificationMethod[0].ID, + Subject: subject, + SchemaID: createdSchemaTwo.ID, + Data: map[string]any{ + "email": "Satoshi2@Nakamoto2.btc", + }, + Expiry: time.Now().Add(24 * time.Hour).Format(time.RFC3339), + Revocable: true, + }) + + assert.NoError(tt, err) + assert.NotEmpty(tt, createdCredThree) + assert.NotEmpty(tt, createdCredThree.CredentialJWT) + + credStatusMapThree, ok := createdCredThree.Credential.CredentialStatus.(map[string]any) + assert.True(tt, ok) + + assert.Contains(tt, credStatusMapThree["id"], createdCredThree.ID) + assert.Contains(tt, credStatusMapThree["statusListCredential"], "v1/credentials/status") + assert.NotEmpty(tt, credStatusMapThree["statusListIndex"]) + + // Cred with different pair have different statusListCredential + assert.NotEqual(tt, credStatusMapThree["statusListCredential"], credStatusMap["statusListCredential"]) + }) + + t.Run("Credential Status List Test No Schemas", func(tt *testing.T) { + s := test.ServiceStorage(t) + assert.NotEmpty(tt, s) + + serviceConfig := config.CredentialServiceConfig{BaseServiceConfig: &config.BaseServiceConfig{Name: "credential", ServiceEndpoint: "/v1/credentials"}} + keyStoreService := testKeyStoreService(tt, s) + didService := testDIDService(tt, s, keyStoreService) + schemaService := testSchemaService(tt, s, keyStoreService, didService) + + credService, err := credential.NewCredentialService(serviceConfig, s, 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) + + issuer := issuerDID.DID.ID + subject := "did:test:345" + + createdCred, err := credService.CreateCredential(context.Background(), credential.CreateCredentialRequest{ + Issuer: issuer, + IssuerKID: issuerDID.DID.VerificationMethod[0].ID, + Subject: subject, + Data: map[string]any{ + "email": "Satoshi@Nakamoto.btc", + }, + Expiry: time.Now().Add(24 * time.Hour).Format(time.RFC3339), + Revocable: true, + }) + + assert.NoError(tt, err) + assert.NotEmpty(tt, createdCred) + assert.NotEmpty(tt, createdCred.CredentialJWT) + + credStatusMap, ok := createdCred.Credential.CredentialStatus.(map[string]any) + assert.True(tt, ok) + + assert.Contains(tt, credStatusMap["id"], fmt.Sprintf("%s/status", createdCred.ID)) + assert.Contains(tt, credStatusMap["statusListCredential"], "v1/credentials/status") + assert.NotEmpty(tt, credStatusMap["statusListIndex"]) + + createdCredTwo, err := credService.CreateCredential(context.Background(), credential.CreateCredentialRequest{ + Issuer: issuer, + IssuerKID: issuerDID.DID.VerificationMethod[0].ID, + Subject: subject, + Data: map[string]any{ + "email": "Satoshi2@Nakamoto2.btc", + }, + Expiry: time.Now().Add(24 * time.Hour).Format(time.RFC3339), + Revocable: true, + }) + + assert.NoError(tt, err) + assert.NotEmpty(tt, createdCredTwo) + assert.NotEmpty(tt, createdCredTwo.CredentialJWT) + + credStatusMapTwo, ok := createdCredTwo.Credential.CredentialStatus.(map[string]any) + assert.True(tt, ok) + + assert.Contains(tt, credStatusMapTwo["id"], fmt.Sprintf("%s/status", createdCredTwo.ID)) + assert.Contains(tt, credStatusMapTwo["statusListCredential"], "v1/credentials/status") + assert.NotEmpty(tt, credStatusMapTwo["statusListIndex"]) + + // Cred with same pair share the same statusListCredential + assert.Equal(tt, credStatusMapTwo["statusListCredential"], credStatusMap["statusListCredential"]) + + // create schema + createdSchema, err := schemaService.CreateSchema(context.Background(), schema.CreateSchemaRequest{Issuer: issuerDID.DID.ID, Name: "simple schema", Schema: getEmailSchema()}) + assert.NoError(tt, err) + assert.NotEmpty(tt, createdSchema) + + createdCredThree, 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": "Satoshi2@Nakamoto2.btc", + }, + Expiry: time.Now().Add(24 * time.Hour).Format(time.RFC3339), + Revocable: true, + }) + + assert.NoError(tt, err) + assert.NotEmpty(tt, createdCredThree) + assert.NotEmpty(tt, createdCredThree.CredentialJWT) + + credStatusMapThree, ok := createdCredThree.Credential.CredentialStatus.(map[string]any) + assert.True(tt, ok) + + assert.Contains(tt, credStatusMapThree["id"], fmt.Sprintf("%s/status", createdCredThree.ID)) + assert.Contains(tt, credStatusMapThree["statusListCredential"], "v1/credentials/status") + assert.NotEmpty(tt, credStatusMapThree["statusListIndex"]) + + // Cred with different pair have different statusListCredential + assert.NotEqual(tt, credStatusMapThree["statusListCredential"], credStatusMap["statusListCredential"]) + }) + + t.Run("Credential Status List Test Update Revoked Status", func(tt *testing.T) { + s := test.ServiceStorage(t) + assert.NotEmpty(tt, s) + + serviceConfig := config.CredentialServiceConfig{BaseServiceConfig: &config.BaseServiceConfig{Name: "credential", ServiceEndpoint: "http://localhost:1234/v1/credentials"}} + keyStoreService := testKeyStoreService(tt, s) + didService := testDIDService(tt, s, keyStoreService) + schemaService := testSchemaService(tt, s, keyStoreService, didService) + credService, err := credential.NewCredentialService(serviceConfig, s, 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 + createdSchema, err := schemaService.CreateSchema(context.Background(), schema.CreateSchemaRequest{Issuer: issuerDID.DID.ID, Name: "simple schema", Schema: getEmailSchema()}) + assert.NoError(tt, err) + assert.NotEmpty(tt, createdSchema) + + issuer := issuerDID.DID.ID + subject := "did:test:345" + + nonRevokableCred, 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: idFromURI(nonRevokableCred.ID), Revoked: 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), + Revocable: 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("%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: idFromURI(createdCred.ID)}) + assert.NoError(tt, err) + assert.Equal(tt, credStatus.Revoked, 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: idFromURI(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: idFromURI(createdCred.ID), Revoked: true}) + assert.NoError(tt, err) + assert.Equal(tt, updatedStatus.Revoked, true) + + updatedCred, err := credService.GetCredential(context.Background(), credential.GetCredentialRequest{ID: idFromURI(createdCred.ID)}) + assert.NoError(tt, err) + assert.Equal(tt, updatedCred.Revoked, true) + + credStatusListAfterRevoke, err := credService.GetCredentialStatusList(context.Background(), credential.GetCredentialStatusListRequest{ID: idFromURI(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) { + s := test.ServiceStorage(t) + assert.NotEmpty(tt, s) + + serviceConfig := config.CredentialServiceConfig{BaseServiceConfig: &config.BaseServiceConfig{Name: "credential", ServiceEndpoint: "http://localhost:1234/v1/credentials"}} + keyStoreService := testKeyStoreService(tt, s) + didService := testDIDService(tt, s, keyStoreService) + schemaService := testSchemaService(tt, s, keyStoreService, didService) + credService, err := credential.NewCredentialService(serviceConfig, s, 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 + createdSchema, err := schemaService.CreateSchema(context.Background(), schema.CreateSchemaRequest{Issuer: issuerDID.DID.ID, Name: "simple schema", Schema: getEmailSchema()}) + 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: idFromURI(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("%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: idFromURI(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: idFromURI(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: idFromURI(createdCred.ID), Suspended: true}) + assert.NoError(tt, err) + assert.Equal(tt, updatedStatus.Suspended, true) + + updatedCred, err := credService.GetCredential(context.Background(), credential.GetCredentialRequest{ID: idFromURI(createdCred.ID)}) + assert.NoError(tt, err) + assert.Equal(tt, updatedCred.Suspended, true) + + credStatusListAfterRevoke, err := credService.GetCredentialStatusList(context.Background(), credential.GetCredentialStatusListRequest{ID: idFromURI(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("Create Multiple Suspendable Credential Different IssuerDID SchemaID StatusPurpose Triples", func(tt *testing.T) { + s := test.ServiceStorage(t) + assert.NotEmpty(tt, s) + + serviceConfig := config.CredentialServiceConfig{BaseServiceConfig: &config.BaseServiceConfig{Name: "credential", ServiceEndpoint: "http://localhost:1234/v1/credentials"}} + keyStoreService := testKeyStoreService(tt, s) + didService := testDIDService(tt, s, keyStoreService) + schemaService := testSchemaService(tt, s, keyStoreService, didService) + credService, err := credential.NewCredentialService(serviceConfig, s, 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 + createdSchema, err := schemaService.CreateSchema(context.Background(), schema.CreateSchemaRequest{Issuer: issuerDID.DID.ID, Name: "simple schema", Schema: getEmailSchema()}) + assert.NoError(tt, err) + assert.NotEmpty(tt, createdSchema) + + subject := "did:test:345" + + createdCred, err := credService.CreateCredential(context.Background(), credential.CreateCredentialRequest{ + Issuer: issuerDID.DID.ID, + 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), + Revocable: true, + }) + assert.NoError(tt, err) + assert.NotEmpty(tt, createdCred) + + createdCredSuspendable, err := credService.CreateCredential(context.Background(), credential.CreateCredentialRequest{ + Issuer: issuerDID.DID.ID, + 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, createdCredSuspendable) + + revocationKey := strings.Join([]string{"is:" + issuerDID.DID.ID, "sc:" + createdSchema.ID, "sp:" + string(status.StatusRevocation)}, "-") + + slcExists, err := s.Exists(context.Background(), "status-list-credential", revocationKey) + assert.NoError(tt, err) + assert.True(tt, slcExists) + + indexPoolExists, err := s.Exists(context.Background(), "status-list-index-pool", revocationKey) + assert.NoError(tt, err) + assert.True(tt, indexPoolExists) + + currentIndexExists, err := s.Exists(context.Background(), "status-list-current-index", revocationKey) + assert.NoError(tt, err) + assert.True(tt, currentIndexExists) + + suspensionKey := strings.Join([]string{"is:" + issuerDID.DID.ID, "sc:" + createdSchema.ID, "sp:" + string(status.StatusSuspension)}, "-") + + slcExists, err = s.Exists(context.Background(), "status-list-credential", suspensionKey) + assert.NoError(tt, err) + assert.True(tt, slcExists) + + indexPoolExists, err = s.Exists(context.Background(), "status-list-index-pool", suspensionKey) + assert.NoError(tt, err) + assert.True(tt, indexPoolExists) + + currentIndexExists, err = s.Exists(context.Background(), "status-list-current-index", suspensionKey) + assert.NoError(tt, err) + assert.True(tt, currentIndexExists) + }) + + t.Run("Create Suspendable Credential", func(tt *testing.T) { + issuer, issuerKID, schemaID, credService := createCredServicePrereqs(tt, test.ServiceStorage(t)) + subject := "did:test:345" + + createdCred, err := credService.CreateCredential(context.Background(), credential.CreateCredentialRequest{ + Issuer: issuer, + IssuerKID: issuerKID, + Subject: subject, + SchemaID: schemaID, + 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("%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: idFromURI(createdCred.ID)}) + assert.NoError(tt, err) + assert.Equal(tt, credStatus.Revoked, false) + 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: idFromURI(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) + }) + + t.Run("Update Suspendable Credential To Suspended", func(tt *testing.T) { + issuer, issuerKID, schemaID, credService := createCredServicePrereqs(tt, test.ServiceStorage(t)) + subject := "did:test:345" + + createdCred, err := credService.CreateCredential(context.Background(), credential.CreateCredentialRequest{ + Issuer: issuer, + IssuerKID: issuerKID, + Subject: subject, + SchemaID: schemaID, + 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("%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: idFromURI(createdCred.ID)}) + assert.NoError(tt, err) + assert.Equal(tt, credStatus.Revoked, false) + 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: idFromURI(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: idFromURI(createdCred.ID), Suspended: true}) + assert.NoError(tt, err) + assert.Equal(tt, updatedStatus.Suspended, true) + assert.Equal(tt, updatedStatus.Revoked, false) + + credStatusListAfterRevoke, err := credService.GetCredentialStatusList(context.Background(), credential.GetCredentialStatusListRequest{ID: idFromURI(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) + + encodedListAfterSuspended := credentialSubjectAfterRevoke["encodedList"] + assert.NotEmpty(tt, encodedListAfterSuspended) + + assert.NotEqualValues(tt, encodedListAfterSuspended, encodedList) + }) + + t.Run("Update Suspendable Credential To Suspended then Unsuspended", func(tt *testing.T) { + issuer, issuerKID, schemaID, credService := createCredServicePrereqs(tt, test.ServiceStorage(t)) + subject := "did:test:345" + + createdCred, err := credService.CreateCredential(context.Background(), credential.CreateCredentialRequest{ + Issuer: issuer, + IssuerKID: issuerKID, + Subject: subject, + SchemaID: schemaID, + 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("%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: idFromURI(createdCred.ID)}) + assert.NoError(tt, err) + assert.Equal(tt, credStatus.Revoked, false) + 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: idFromURI(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: idFromURI(createdCred.ID), Suspended: true}) + assert.NoError(tt, err) + assert.Equal(tt, updatedStatus.Suspended, true) + assert.Equal(tt, updatedStatus.Revoked, false) + + credStatusListAfterRevoke, err := credService.GetCredentialStatusList(context.Background(), credential.GetCredentialStatusListRequest{ID: idFromURI(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) + + encodedListAfterSuspended := credentialSubjectAfterRevoke["encodedList"] + assert.NotEmpty(tt, encodedListAfterSuspended) + + assert.NotEqualValues(tt, encodedListAfterSuspended, encodedList) + + updatedStatus, err = credService.UpdateCredentialStatus(context.Background(), credential.UpdateCredentialStatusRequest{ID: idFromURI(createdCred.ID), Suspended: false}) + assert.NoError(tt, err) + assert.Equal(tt, updatedStatus.Suspended, false) + assert.Equal(tt, updatedStatus.Revoked, false) + }) + + t.Run("Create Suspendable and Revocable Credential Should Be Error", func(tt *testing.T) { + issuer, issuerKID, schemaID, credService := createCredServicePrereqs(tt, test.ServiceStorage(t)) + subject := "did:test:345" + + createdCred, err := credService.CreateCredential(context.Background(), credential.CreateCredentialRequest{ + Issuer: issuer, + IssuerKID: issuerKID, + Subject: subject, + SchemaID: schemaID, + Data: map[string]any{ + "email": "Satoshi@Nakamoto.btc", + }, + Expiry: time.Now().Add(24 * time.Hour).Format(time.RFC3339), + Revocable: true, + Suspendable: true, + }) + + assert.Error(tt, err) + assert.ErrorContains(tt, err, "credential may have at most one status") + assert.Empty(tt, createdCred) + }) + + t.Run("Update Suspendable and Revocable Credential Should Be Error", func(tt *testing.T) { + issuer, issuerKID, schemaID, credService := createCredServicePrereqs(tt, test.ServiceStorage(t)) + subject := "did:test:345" + + createdCred, err := credService.CreateCredential(context.Background(), credential.CreateCredentialRequest{ + Issuer: issuer, + IssuerKID: issuerKID, + Subject: subject, + SchemaID: schemaID, + 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) + + updatedStatus, err := credService.UpdateCredentialStatus(context.Background(), credential.UpdateCredentialStatusRequest{ID: idFromURI(createdCred.ID), Revoked: true, Suspended: true}) + assert.Nil(tt, updatedStatus) + assert.Error(tt, err) + assert.ErrorContains(tt, err, "cannot update both suspended and revoked status") + }) + + t.Run("Update Suspended On Revoked Credential Should Be Error", func(tt *testing.T) { + issuer, issuerKID, schemaID, credService := createCredServicePrereqs(tt, test.ServiceStorage(t)) + subject := "did:test:345" + + createdCred, err := credService.CreateCredential(context.Background(), credential.CreateCredentialRequest{ + Issuer: issuer, + IssuerKID: issuerKID, + Subject: subject, + SchemaID: schemaID, + Data: map[string]any{ + "email": "Satoshi@Nakamoto.btc", + }, + Expiry: time.Now().Add(24 * time.Hour).Format(time.RFC3339), + Revocable: true, + }) + + assert.NoError(tt, err) + assert.NotEmpty(tt, createdCred) + + updatedStatus, err := credService.UpdateCredentialStatus(context.Background(), credential.UpdateCredentialStatusRequest{ID: idFromURI(createdCred.ID), Suspended: true}) + assert.Nil(tt, updatedStatus) + assert.Error(tt, err) + assert.ErrorContains(tt, err, "has a different status purpose value than the status credential") + }) }) - - assert.NoError(tt, err) - assert.NotEmpty(tt, createdCred) - - updatedStatus, err := credService.UpdateCredentialStatus(context.Background(), credential.UpdateCredentialStatusRequest{ID: idFromURI(createdCred.ID), Suspended: true}) - assert.Nil(tt, updatedStatus) - assert.Error(tt, err) - assert.ErrorContains(tt, err, "has a different status purpose value than the status credential") - }) + } } func idFromURI(cred string) string { return cred[len(cred)-36:] } -func createCredServicePrereqs(tt *testing.T) (issuer, issuerKID, schemaID string, credSvc credential.Service) { - bolt := setupTestDB(tt) - require.NotEmpty(tt, bolt) +func createCredServicePrereqs(tt *testing.T, s storage.ServiceStorage) (issuer, issuerKID, schemaID string, credSvc credential.Service) { + require.NotEmpty(tt, s) serviceConfig := config.CredentialServiceConfig{BaseServiceConfig: &config.BaseServiceConfig{Name: "credential", ServiceEndpoint: "http://localhost:1234/v1/credentials"}} - 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) + keyStoreService := testKeyStoreService(tt, s) + didService := testDIDService(tt, s, keyStoreService) + schemaService := testSchemaService(tt, s, keyStoreService, didService) + credService, err := credential.NewCredentialService(serviceConfig, s, keyStoreService, didService.GetResolver(), schemaService) require.NoError(tt, err) require.NotEmpty(tt, credService) diff --git a/pkg/server/router/did_test.go b/pkg/server/router/did_test.go index 2f08d8c69..8fe4ca24f 100644 --- a/pkg/server/router/did_test.go +++ b/pkg/server/router/did_test.go @@ -2,11 +2,13 @@ package router import ( "context" + "strings" "testing" "github.com/TBD54566975/ssi-sdk/crypto" didsdk "github.com/TBD54566975/ssi-sdk/did" "github.com/stretchr/testify/assert" + "github.com/tbd54566975/ssi-service/pkg/testutil" "github.com/tbd54566975/ssi-service/config" "github.com/tbd54566975/ssi-service/pkg/service/did" @@ -28,210 +30,220 @@ func TestDIDRouter(t *testing.T) { assert.Contains(tt, err.Error(), "could not create DID router with service type: test") }) - t.Run("List DIDs supports paging", func(tt *testing.T) { - db := setupTestDB(tt) - assert.NotEmpty(tt, db) - keyStoreService := testKeyStoreService(tt, db) - methods := []string{didsdk.KeyMethod.String()} - serviceConfig := config.DIDServiceConfig{Methods: methods, LocalResolutionMethods: methods} - didService, err := did.NewDIDService(serviceConfig, db, keyStoreService) - assert.NoError(tt, err) - assert.NotEmpty(tt, didService) - createDID(tt, didService) - createDID(tt, didService) - - one := 1 - listDIDsResponse1, err := didService.ListDIDsByMethod(context.Background(), - did.ListDIDsRequest{ - Method: didsdk.KeyMethod, - PageSize: &one, - }) - assert.NoError(tt, err) - assert.Len(tt, listDIDsResponse1.DIDs, 1) - assert.NotEmpty(tt, listDIDsResponse1.NextPageToken) - - listDIDsResponse2, err := didService.ListDIDsByMethod(context.Background(), - did.ListDIDsRequest{ - Method: didsdk.KeyMethod, - PageSize: &one, - PageToken: &listDIDsResponse1.NextPageToken, - }) - assert.NoError(tt, err) - assert.Len(tt, listDIDsResponse2.DIDs, 1) - assert.Empty(tt, listDIDsResponse2.NextPageToken) - }) - - t.Run("DID Service Test", func(tt *testing.T) { - db := setupTestDB(tt) - assert.NotEmpty(tt, db) - - keyStoreService := testKeyStoreService(tt, db) - methods := []string{didsdk.KeyMethod.String()} - serviceConfig := config.DIDServiceConfig{Methods: methods, LocalResolutionMethods: methods} - didService, err := did.NewDIDService(serviceConfig, db, keyStoreService) - assert.NoError(tt, err) - assert.NotEmpty(tt, didService) - - // check type and status - assert.Equal(tt, framework.DID, didService.Type()) - assert.Equal(tt, framework.StatusReady, didService.Status().Status) - - // get unknown handler - _, err = didService.GetDIDByMethod(context.Background(), did.GetDIDRequest{Method: "bad"}) - assert.Error(tt, err) - assert.Contains(tt, err.Error(), "could not get handler for method") - - supported := didService.GetSupportedMethods() - assert.NotEmpty(tt, supported) - assert.Len(tt, supported.Methods, 1) - assert.Equal(tt, didsdk.KeyMethod, supported.Methods[0]) - - // bad key type - _, err = didService.CreateDIDByMethod(context.Background(), did.CreateDIDRequest{Method: didsdk.KeyMethod, KeyType: "bad"}) - assert.Error(tt, err) - assert.Contains(tt, err.Error(), "could not create did:key") - - // good key type - createDIDResponse, err := didService.CreateDIDByMethod(context.Background(), did.CreateDIDRequest{Method: didsdk.KeyMethod, KeyType: crypto.Ed25519}) - assert.NoError(tt, err) - assert.NotEmpty(tt, createDIDResponse) - - // check the DID is a did:key - assert.Contains(tt, createDIDResponse.DID.ID, "did:key") - - // get it back - getDIDResponse, err := didService.GetDIDByMethod(context.Background(), did.GetDIDRequest{Method: didsdk.KeyMethod, ID: createDIDResponse.DID.ID}) - assert.NoError(tt, err) - assert.NotEmpty(tt, getDIDResponse) - - // make sure it's the same value - assert.Equal(tt, createDIDResponse.DID.ID, getDIDResponse.DID.ID) - - // create a second DID - createDIDResponse2, err := didService.CreateDIDByMethod(context.Background(), did.CreateDIDRequest{Method: didsdk.KeyMethod, KeyType: crypto.Ed25519}) - assert.NoError(tt, err) - assert.NotEmpty(tt, createDIDResponse2) - - // get all DIDs back - getDIDsResponse, err := didService.ListDIDsByMethod(context.Background(), did.ListDIDsRequest{Method: didsdk.KeyMethod}) - assert.NoError(tt, err) - assert.NotEmpty(tt, getDIDsResponse) - assert.Len(tt, getDIDsResponse.DIDs, 2) - - knownDIDs := map[string]bool{createDIDResponse.DID.ID: true, createDIDResponse2.DID.ID: true} - for _, gotDID := range getDIDsResponse.DIDs { - if _, ok := knownDIDs[gotDID.ID]; !ok { - tt.Error("got unknown DID") - } else { - delete(knownDIDs, gotDID.ID) + for _, test := range testutil.TestDatabases { + t.Run(test.Name, func(t *testing.T) { + + // TODO: Fix pagesize issue on redis - https://github.com/TBD54566975/ssi-service/issues/538 + if !strings.Contains(test.Name, "Redis") { + t.Run("List DIDs supports paging", func(tt *testing.T) { + db := test.ServiceStorage(t) + assert.NotEmpty(tt, db) + keyStoreService := testKeyStoreService(tt, db) + methods := []string{didsdk.KeyMethod.String()} + serviceConfig := config.DIDServiceConfig{Methods: methods, LocalResolutionMethods: methods} + didService, err := did.NewDIDService(serviceConfig, db, keyStoreService) + assert.NoError(tt, err) + assert.NotEmpty(tt, didService) + createDID(tt, didService) + createDID(tt, didService) + + one := 1 + listDIDsResponse1, err := didService.ListDIDsByMethod(context.Background(), + did.ListDIDsRequest{ + Method: didsdk.KeyMethod, + PageSize: &one, + }) + + assert.NoError(tt, err) + assert.Len(tt, listDIDsResponse1.DIDs, 1) + assert.NotEmpty(tt, listDIDsResponse1.NextPageToken) + + listDIDsResponse2, err := didService.ListDIDsByMethod(context.Background(), + did.ListDIDsRequest{ + Method: didsdk.KeyMethod, + PageSize: &one, + PageToken: &listDIDsResponse1.NextPageToken, + }) + + assert.NoError(tt, err) + assert.Len(tt, listDIDsResponse2.DIDs, 1) + assert.Empty(tt, listDIDsResponse2.NextPageToken) + + }) } - } - assert.Len(tt, knownDIDs, 0) - - // delete dids - err = didService.SoftDeleteDIDByMethod(context.Background(), did.DeleteDIDRequest{Method: didsdk.KeyMethod, ID: createDIDResponse.DID.ID}) - assert.NoError(tt, err) - - err = didService.SoftDeleteDIDByMethod(context.Background(), did.DeleteDIDRequest{Method: didsdk.KeyMethod, ID: createDIDResponse2.DID.ID}) - assert.NoError(tt, err) - - // get all DIDs back - getDIDsResponse, err = didService.ListDIDsByMethod(context.Background(), did.ListDIDsRequest{Method: didsdk.KeyMethod}) - assert.NoError(tt, err) - assert.NotEmpty(tt, getDIDsResponse) - assert.Len(tt, getDIDsResponse.DIDs, 0) - - // get deleted DIDs back - getDIDsResponse, err = didService.ListDIDsByMethod(context.Background(), did.ListDIDsRequest{Method: didsdk.KeyMethod, Deleted: true}) - assert.NoError(tt, err) - assert.NotEmpty(tt, getDIDsResponse) - assert.Len(tt, getDIDsResponse.DIDs, 2) - }) - - t.Run("DID Web Service Test", func(tt *testing.T) { - db := setupTestDB(tt) - assert.NotEmpty(tt, db) - - keyStoreService := testKeyStoreService(tt, db) - methods := []string{didsdk.KeyMethod.String(), didsdk.WebMethod.String()} - serviceConfig := config.DIDServiceConfig{Methods: methods, LocalResolutionMethods: methods} - didService, err := did.NewDIDService(serviceConfig, db, keyStoreService) - assert.NoError(tt, err) - assert.NotEmpty(tt, didService) - // check type and status - assert.Equal(tt, framework.DID, didService.Type()) - assert.Equal(tt, framework.StatusReady, didService.Status().Status) - - // get unknown handler - _, err = didService.GetDIDByMethod(context.Background(), did.GetDIDRequest{Method: "bad"}) - assert.Error(tt, err) - assert.Contains(tt, err.Error(), "could not get handler for method") - - supported := didService.GetSupportedMethods() - assert.NotEmpty(tt, supported) - assert.Len(tt, supported.Methods, 2) - - assert.ElementsMatch(tt, supported.Methods, []didsdk.Method{didsdk.KeyMethod, didsdk.WebMethod}) - - // bad key type - createOpts := did.CreateWebDIDOptions{DIDWebID: "did:web:example.com"} - _, err = didService.CreateDIDByMethod(context.Background(), did.CreateDIDRequest{Method: didsdk.WebMethod, KeyType: "bad", Options: createOpts}) - assert.Error(tt, err) - assert.Contains(tt, err.Error(), "could not generate key for did:web") - - // good key type - createDIDResponse, err := didService.CreateDIDByMethod(context.Background(), did.CreateDIDRequest{Method: didsdk.WebMethod, KeyType: crypto.Ed25519, Options: createOpts}) - assert.NoError(tt, err) - assert.NotEmpty(tt, createDIDResponse) - - // check the DID is a did:key - assert.Contains(tt, createDIDResponse.DID.ID, "did:web") - - // get it back - getDIDResponse, err := didService.GetDIDByMethod(context.Background(), did.GetDIDRequest{Method: didsdk.WebMethod, ID: createDIDResponse.DID.ID}) - assert.NoError(tt, err) - assert.NotEmpty(tt, getDIDResponse) - - // make sure it's the same value - assert.Equal(tt, createDIDResponse.DID.ID, getDIDResponse.DID.ID) - - // create a second DID - createOpts = did.CreateWebDIDOptions{DIDWebID: "did:web:tbd.website"} - createDIDResponse2, err := didService.CreateDIDByMethod(context.Background(), did.CreateDIDRequest{Method: didsdk.WebMethod, KeyType: crypto.Ed25519, Options: createOpts}) - assert.NoError(tt, err) - assert.NotEmpty(tt, createDIDResponse2) - - // get all DIDs back - getDIDsResponse, err := didService.ListDIDsByMethod(context.Background(), did.ListDIDsRequest{Method: didsdk.WebMethod}) - assert.NoError(tt, err) - assert.NotEmpty(tt, getDIDsResponse) - assert.Len(tt, getDIDsResponse.DIDs, 2) - - knownDIDs := map[string]bool{createDIDResponse.DID.ID: true, createDIDResponse2.DID.ID: true} - for _, gotDID := range getDIDsResponse.DIDs { - if _, ok := knownDIDs[gotDID.ID]; !ok { - tt.Error("got unknown DID") - } else { - delete(knownDIDs, gotDID.ID) - } - } - assert.Len(tt, knownDIDs, 0) - - // delete dids - err = didService.SoftDeleteDIDByMethod(context.Background(), did.DeleteDIDRequest{Method: didsdk.WebMethod, ID: createDIDResponse.DID.ID}) - assert.NoError(tt, err) - - err = didService.SoftDeleteDIDByMethod(context.Background(), did.DeleteDIDRequest{Method: didsdk.WebMethod, ID: createDIDResponse2.DID.ID}) - assert.NoError(tt, err) - - // get all DIDs back - getDIDsResponse, err = didService.ListDIDsByMethod(context.Background(), did.ListDIDsRequest{Method: didsdk.WebMethod}) - assert.NoError(tt, err) - assert.NotEmpty(tt, getDIDsResponse) - assert.Len(tt, getDIDsResponse.DIDs, 0) - }) + t.Run("DID Service Test", func(tt *testing.T) { + db := test.ServiceStorage(t) + assert.NotEmpty(tt, db) + + keyStoreService := testKeyStoreService(tt, db) + methods := []string{didsdk.KeyMethod.String()} + serviceConfig := config.DIDServiceConfig{Methods: methods, LocalResolutionMethods: methods} + didService, err := did.NewDIDService(serviceConfig, db, keyStoreService) + assert.NoError(tt, err) + assert.NotEmpty(tt, didService) + + // check type and status + assert.Equal(tt, framework.DID, didService.Type()) + assert.Equal(tt, framework.StatusReady, didService.Status().Status) + + // get unknown handler + _, err = didService.GetDIDByMethod(context.Background(), did.GetDIDRequest{Method: "bad"}) + assert.Error(tt, err) + assert.Contains(tt, err.Error(), "could not get handler for method") + + supported := didService.GetSupportedMethods() + assert.NotEmpty(tt, supported) + assert.Len(tt, supported.Methods, 1) + assert.Equal(tt, didsdk.KeyMethod, supported.Methods[0]) + + // bad key type + _, err = didService.CreateDIDByMethod(context.Background(), did.CreateDIDRequest{Method: didsdk.KeyMethod, KeyType: "bad"}) + assert.Error(tt, err) + assert.Contains(tt, err.Error(), "could not create did:key") + + // good key type + createDIDResponse, err := didService.CreateDIDByMethod(context.Background(), did.CreateDIDRequest{Method: didsdk.KeyMethod, KeyType: crypto.Ed25519}) + assert.NoError(tt, err) + assert.NotEmpty(tt, createDIDResponse) + + // check the DID is a did:key + assert.Contains(tt, createDIDResponse.DID.ID, "did:key") + + // get it back + getDIDResponse, err := didService.GetDIDByMethod(context.Background(), did.GetDIDRequest{Method: didsdk.KeyMethod, ID: createDIDResponse.DID.ID}) + assert.NoError(tt, err) + assert.NotEmpty(tt, getDIDResponse) + + // make sure it's the same value + assert.Equal(tt, createDIDResponse.DID.ID, getDIDResponse.DID.ID) + + // create a second DID + createDIDResponse2, err := didService.CreateDIDByMethod(context.Background(), did.CreateDIDRequest{Method: didsdk.KeyMethod, KeyType: crypto.Ed25519}) + assert.NoError(tt, err) + assert.NotEmpty(tt, createDIDResponse2) + + // get all DIDs back + getDIDsResponse, err := didService.ListDIDsByMethod(context.Background(), did.ListDIDsRequest{Method: didsdk.KeyMethod}) + assert.NoError(tt, err) + assert.NotEmpty(tt, getDIDsResponse) + assert.Len(tt, getDIDsResponse.DIDs, 2) + + knownDIDs := map[string]bool{createDIDResponse.DID.ID: true, createDIDResponse2.DID.ID: true} + for _, gotDID := range getDIDsResponse.DIDs { + if _, ok := knownDIDs[gotDID.ID]; !ok { + tt.Error("got unknown DID") + } else { + delete(knownDIDs, gotDID.ID) + } + } + assert.Len(tt, knownDIDs, 0) + + // delete dids + err = didService.SoftDeleteDIDByMethod(context.Background(), did.DeleteDIDRequest{Method: didsdk.KeyMethod, ID: createDIDResponse.DID.ID}) + assert.NoError(tt, err) + + err = didService.SoftDeleteDIDByMethod(context.Background(), did.DeleteDIDRequest{Method: didsdk.KeyMethod, ID: createDIDResponse2.DID.ID}) + assert.NoError(tt, err) + + // get all DIDs back + getDIDsResponse, err = didService.ListDIDsByMethod(context.Background(), did.ListDIDsRequest{Method: didsdk.KeyMethod}) + assert.NoError(tt, err) + assert.NotEmpty(tt, getDIDsResponse) + assert.Len(tt, getDIDsResponse.DIDs, 0) + + // get deleted DIDs back + getDIDsResponse, err = didService.ListDIDsByMethod(context.Background(), did.ListDIDsRequest{Method: didsdk.KeyMethod, Deleted: true}) + assert.NoError(tt, err) + assert.NotEmpty(tt, getDIDsResponse) + assert.Len(tt, getDIDsResponse.DIDs, 2) + }) + t.Run("DID Web Service Test", func(tt *testing.T) { + db := test.ServiceStorage(t) + assert.NotEmpty(tt, db) + + keyStoreService := testKeyStoreService(tt, db) + methods := []string{didsdk.KeyMethod.String(), didsdk.WebMethod.String()} + serviceConfig := config.DIDServiceConfig{Methods: methods, LocalResolutionMethods: methods} + didService, err := did.NewDIDService(serviceConfig, db, keyStoreService) + assert.NoError(tt, err) + assert.NotEmpty(tt, didService) + + // check type and status + assert.Equal(tt, framework.DID, didService.Type()) + assert.Equal(tt, framework.StatusReady, didService.Status().Status) + + // get unknown handler + _, err = didService.GetDIDByMethod(context.Background(), did.GetDIDRequest{Method: "bad"}) + assert.Error(tt, err) + assert.Contains(tt, err.Error(), "could not get handler for method") + + supported := didService.GetSupportedMethods() + assert.NotEmpty(tt, supported) + assert.Len(tt, supported.Methods, 2) + + assert.ElementsMatch(tt, supported.Methods, []didsdk.Method{didsdk.KeyMethod, didsdk.WebMethod}) + + // bad key type + createOpts := did.CreateWebDIDOptions{DIDWebID: "did:web:example.com"} + _, err = didService.CreateDIDByMethod(context.Background(), did.CreateDIDRequest{Method: didsdk.WebMethod, KeyType: "bad", Options: createOpts}) + assert.Error(tt, err) + assert.Contains(tt, err.Error(), "could not generate key for did:web") + + // good key type + createDIDResponse, err := didService.CreateDIDByMethod(context.Background(), did.CreateDIDRequest{Method: didsdk.WebMethod, KeyType: crypto.Ed25519, Options: createOpts}) + assert.NoError(tt, err) + assert.NotEmpty(tt, createDIDResponse) + + // check the DID is a did:key + assert.Contains(tt, createDIDResponse.DID.ID, "did:web") + + // get it back + getDIDResponse, err := didService.GetDIDByMethod(context.Background(), did.GetDIDRequest{Method: didsdk.WebMethod, ID: createDIDResponse.DID.ID}) + assert.NoError(tt, err) + assert.NotEmpty(tt, getDIDResponse) + + // make sure it's the same value + assert.Equal(tt, createDIDResponse.DID.ID, getDIDResponse.DID.ID) + + // create a second DID + createOpts = did.CreateWebDIDOptions{DIDWebID: "did:web:tbd.website"} + createDIDResponse2, err := didService.CreateDIDByMethod(context.Background(), did.CreateDIDRequest{Method: didsdk.WebMethod, KeyType: crypto.Ed25519, Options: createOpts}) + assert.NoError(tt, err) + assert.NotEmpty(tt, createDIDResponse2) + + // get all DIDs back + getDIDsResponse, err := didService.ListDIDsByMethod(context.Background(), did.ListDIDsRequest{Method: didsdk.WebMethod}) + assert.NoError(tt, err) + assert.NotEmpty(tt, getDIDsResponse) + assert.Len(tt, getDIDsResponse.DIDs, 2) + + knownDIDs := map[string]bool{createDIDResponse.DID.ID: true, createDIDResponse2.DID.ID: true} + for _, gotDID := range getDIDsResponse.DIDs { + if _, ok := knownDIDs[gotDID.ID]; !ok { + tt.Error("got unknown DID") + } else { + delete(knownDIDs, gotDID.ID) + } + } + assert.Len(tt, knownDIDs, 0) + + // delete dids + err = didService.SoftDeleteDIDByMethod(context.Background(), did.DeleteDIDRequest{Method: didsdk.WebMethod, ID: createDIDResponse.DID.ID}) + assert.NoError(tt, err) + + err = didService.SoftDeleteDIDByMethod(context.Background(), did.DeleteDIDRequest{Method: didsdk.WebMethod, ID: createDIDResponse2.DID.ID}) + assert.NoError(tt, err) + + // get all DIDs back + getDIDsResponse, err = didService.ListDIDsByMethod(context.Background(), did.ListDIDsRequest{Method: didsdk.WebMethod}) + assert.NoError(tt, err) + assert.NotEmpty(tt, getDIDsResponse) + assert.Len(tt, getDIDsResponse.DIDs, 0) + }) + }) + } } func createDID(tt *testing.T, didService *did.Service) { diff --git a/pkg/server/router/keystore_test.go b/pkg/server/router/keystore_test.go index 880073d53..6ffe57bac 100644 --- a/pkg/server/router/keystore_test.go +++ b/pkg/server/router/keystore_test.go @@ -7,6 +7,7 @@ import ( "github.com/TBD54566975/ssi-sdk/crypto" "github.com/mr-tron/base58" "github.com/stretchr/testify/assert" + "github.com/tbd54566975/ssi-service/pkg/testutil" "github.com/tbd54566975/ssi-service/config" "github.com/tbd54566975/ssi-service/pkg/service/framework" @@ -29,69 +30,73 @@ func TestKeyStoreRouter(t *testing.T) { assert.Contains(tt, err.Error(), "could not create key store router with service type: test") }) - t.Run("Key Store Service Test", func(tt *testing.T) { - bolt := setupTestDB(tt) - assert.NotEmpty(tt, bolt) - - serviceConfig := config.KeyStoreServiceConfig{ - BaseServiceConfig: &config.BaseServiceConfig{Name: "keystore"}, - MasterKeyPassword: "test-password", - } - keyStoreService, err := keystore.NewKeyStoreService(serviceConfig, bolt) - assert.NoError(tt, err) - assert.NotEmpty(tt, keyStoreService) - - // check type and status - assert.Equal(tt, framework.KeyStore, keyStoreService.Type()) - assert.Equal(tt, framework.StatusReady, keyStoreService.Status().Status) - - // store an invalid key type - err = keyStoreService.StoreKey(context.Background(), keystore.StoreKeyRequest{ - ID: "test-kid", - Type: "bad", - Controller: "me", - PrivateKeyBase58: base58.Encode([]byte("bad")), + for _, test := range testutil.TestDatabases { + t.Run(test.Name, func(t *testing.T) { + t.Run("Key Store Service Test", func(tt *testing.T) { + db := test.ServiceStorage(t) + assert.NotEmpty(tt, db) + + serviceConfig := config.KeyStoreServiceConfig{ + BaseServiceConfig: &config.BaseServiceConfig{Name: "keystore"}, + MasterKeyPassword: "test-password", + } + keyStoreService, err := keystore.NewKeyStoreService(serviceConfig, db) + assert.NoError(tt, err) + assert.NotEmpty(tt, keyStoreService) + + // check type and status + assert.Equal(tt, framework.KeyStore, keyStoreService.Type()) + assert.Equal(tt, framework.StatusReady, keyStoreService.Status().Status) + + // store an invalid key type + err = keyStoreService.StoreKey(context.Background(), keystore.StoreKeyRequest{ + ID: "test-kid", + Type: "bad", + Controller: "me", + PrivateKeyBase58: base58.Encode([]byte("bad")), + }) + assert.Error(tt, err) + assert.Contains(tt, err.Error(), "unsupported key type: bad") + + // store a valid key + _, privKey, err := crypto.GenerateKeyByKeyType(crypto.Ed25519) + assert.NoError(tt, err) + assert.NotEmpty(tt, privKey) + + keyID := "did:test:me#key-1" + privKeyBytes, err := crypto.PrivKeyToBytes(privKey) + assert.NoError(tt, err) + err = keyStoreService.StoreKey(context.Background(), keystore.StoreKeyRequest{ + ID: keyID, + Type: crypto.Ed25519, + Controller: "did:test:me", + PrivateKeyBase58: base58.Encode(privKeyBytes), + }) + assert.NoError(tt, err) + + // get a key that doesn't exist + gotDetails, err := keyStoreService.GetKeyDetails(context.Background(), keystore.GetKeyDetailsRequest{ID: "bad"}) + assert.Error(tt, err) + assert.Empty(tt, gotDetails) + assert.Contains(tt, err.Error(), "could not get key details for key: bad") + + // get a key that exists + gotDetails, err = keyStoreService.GetKeyDetails(context.Background(), keystore.GetKeyDetailsRequest{ID: keyID}) + assert.NoError(tt, err) + assert.NotEmpty(tt, gotDetails) + + // make sure the details match + assert.Equal(tt, keyID, gotDetails.ID) + assert.Equal(tt, crypto.Ed25519, gotDetails.Type) + assert.Equal(tt, "did:test:me", gotDetails.Controller) + + // revoked key checks + err = keyStoreService.RevokeKey(context.Background(), keystore.RevokeKeyRequest{ID: keyID}) + assert.NoError(tt, err) + gotDetails, err = keyStoreService.GetKeyDetails(context.Background(), keystore.GetKeyDetailsRequest{ID: keyID}) + assert.NoError(tt, err) + assert.True(tt, gotDetails.Revoked) + }) }) - assert.Error(tt, err) - assert.Contains(tt, err.Error(), "unsupported key type: bad") - - // store a valid key - _, privKey, err := crypto.GenerateKeyByKeyType(crypto.Ed25519) - assert.NoError(tt, err) - assert.NotEmpty(tt, privKey) - - keyID := "did:test:me#key-1" - privKeyBytes, err := crypto.PrivKeyToBytes(privKey) - assert.NoError(tt, err) - err = keyStoreService.StoreKey(context.Background(), keystore.StoreKeyRequest{ - ID: keyID, - Type: crypto.Ed25519, - Controller: "did:test:me", - PrivateKeyBase58: base58.Encode(privKeyBytes), - }) - assert.NoError(tt, err) - - // get a key that doesn't exist - gotDetails, err := keyStoreService.GetKeyDetails(context.Background(), keystore.GetKeyDetailsRequest{ID: "bad"}) - assert.Error(tt, err) - assert.Empty(tt, gotDetails) - assert.Contains(tt, err.Error(), "could not get key details for key: bad") - - // get a key that exists - gotDetails, err = keyStoreService.GetKeyDetails(context.Background(), keystore.GetKeyDetailsRequest{ID: keyID}) - assert.NoError(tt, err) - assert.NotEmpty(tt, gotDetails) - - // make sure the details match - assert.Equal(tt, keyID, gotDetails.ID) - assert.Equal(tt, crypto.Ed25519, gotDetails.Type) - assert.Equal(tt, "did:test:me", gotDetails.Controller) - - // revoked key checks - err = keyStoreService.RevokeKey(context.Background(), keystore.RevokeKeyRequest{ID: keyID}) - assert.NoError(tt, err) - gotDetails, err = keyStoreService.GetKeyDetails(context.Background(), keystore.GetKeyDetailsRequest{ID: keyID}) - assert.NoError(tt, err) - assert.True(tt, gotDetails.Revoked) - }) + } } diff --git a/pkg/server/router/manifest_test.go b/pkg/server/router/manifest_test.go index 519096567..6c2c4e8b9 100644 --- a/pkg/server/router/manifest_test.go +++ b/pkg/server/router/manifest_test.go @@ -22,6 +22,7 @@ import ( "github.com/tbd54566975/ssi-service/pkg/service/operation/storage" presmodel "github.com/tbd54566975/ssi-service/pkg/service/presentation/model" "github.com/tbd54566975/ssi-service/pkg/service/schema" + "github.com/tbd54566975/ssi-service/pkg/testutil" ) func TestManifestRouter(t *testing.T) { @@ -40,190 +41,194 @@ func TestManifestRouter(t *testing.T) { assert.Contains(tt, err.Error(), "could not create manifest router with service type: test") }) - t.Run("Manifest Service Test", func(tt *testing.T) { - bolt := setupTestDB(tt) - assert.NotEmpty(tt, bolt) - - keyStoreService := testKeyStoreService(tt, bolt) - didService := testDIDService(tt, bolt, keyStoreService) - schemaService := testSchemaService(tt, bolt, keyStoreService, didService) - credentialService := testCredentialService(tt, bolt, keyStoreService, didService, schemaService) - presentationService := testPresentationDefinitionService(tt, bolt, didService, schemaService, keyStoreService) - manifestService := testManifestService(tt, bolt, keyStoreService, didService, credentialService, presentationService) - assert.NotEmpty(tt, manifestService) - - tt.Run("CreateManifest with presentation ID and value error", func(ttt *testing.T) { - defID := "an ID I know" - createManifestRequest := getValidManifestRequest("issuerDID", "issuerKID", "schemaID") - createManifestRequest.PresentationDefinitionRef.ID = &defID - - _, err := manifestService.CreateManifest(context.Background(), createManifestRequest) - - assert.Error(ttt, err) - assert.ErrorContains(ttt, err, `only one of "id" and "value" can be provided`) - }) - - tt.Run("CreateManifest with bad presentation ID returns error", func(ttt *testing.T) { - defID := "a bad ID" - createManifestRequest := getValidManifestRequest("issuerDID", "issuerKID", "schemaID") - createManifestRequest.PresentationDefinitionRef = &model.PresentationDefinitionRef{ - ID: &defID, - } - - _, err := manifestService.CreateManifest(context.Background(), createManifestRequest) - - assert.Error(ttt, err) - assert.ErrorContains(ttt, err, "presentation definition not found") - }) - - tt.Run("CreateManifest with presentation ID returns manifest", func(ttt *testing.T) { - definition := createPresentationDefinition(ttt) - resp, err := presentationService.CreatePresentationDefinition(context.Background(), presmodel.CreatePresentationDefinitionRequest{ - PresentationDefinition: *definition, - }) - assert.NoError(ttt, err) - assert.NotEmpty(ttt, resp) - - createManifestRequest := getValidManifestRequest("issuerDID", "issuerKID", "schemaID") - createManifestRequest.PresentationDefinitionRef = &model.PresentationDefinitionRef{ - ID: &resp.PresentationDefinition.ID, - } - manifest, err := manifestService.CreateManifest(context.Background(), createManifestRequest) - - assert.NoError(ttt, err) - assert.Equal(ttt, resp.PresentationDefinition, *manifest.Manifest.PresentationDefinition) - }) - - tt.Run("multiple behaviors", func(ttt *testing.T) { - // check type and status - assert.Equal(ttt, framework.Manifest, manifestService.Type()) - assert.Equal(ttt, framework.StatusReady, manifestService.Status().Status) - - // create issuer and applicant DIDs - createDIDRequest := did.CreateDIDRequest{ - Method: didsdk.KeyMethod, - KeyType: crypto.Ed25519, - } - issuerDID, err := didService.CreateDIDByMethod(context.Background(), createDIDRequest) - assert.NoError(ttt, err) - assert.NotEmpty(ttt, issuerDID) - - applicantPrivKey, applicantDIDKey, err := key.GenerateDIDKey(crypto.Ed25519) - assert.NoError(ttt, err) - assert.NotEmpty(ttt, applicantPrivKey) - assert.NotEmpty(ttt, applicantDIDKey) - - applicantDID, err := applicantDIDKey.Expand() - assert.NoError(ttt, err) - assert.NotEmpty(ttt, applicantDID) - - // create a schema for the creds to be issued against - licenseSchema := map[string]any{ - "$schema": "https://json-schema.org/draft-07/schema", - "type": "object", - "properties": map[string]any{ - "credentialSubject": map[string]any{ - "type": "object", + for _, test := range testutil.TestDatabases { + t.Run(test.Name, func(t *testing.T) { + t.Run("Manifest Service Test", func(tt *testing.T) { + db := test.ServiceStorage(t) + assert.NotEmpty(tt, db) + + keyStoreService := testKeyStoreService(tt, db) + didService := testDIDService(tt, db, keyStoreService) + schemaService := testSchemaService(tt, db, keyStoreService, didService) + credentialService := testCredentialService(tt, db, keyStoreService, didService, schemaService) + presentationService := testPresentationDefinitionService(tt, db, didService, schemaService, keyStoreService) + manifestService := testManifestService(tt, db, keyStoreService, didService, credentialService, presentationService) + assert.NotEmpty(tt, manifestService) + + tt.Run("CreateManifest with presentation ID and value error", func(ttt *testing.T) { + defID := "an ID I know" + createManifestRequest := getValidManifestRequest("issuerDID", "issuerKID", "schemaID") + createManifestRequest.PresentationDefinitionRef.ID = &defID + + _, err := manifestService.CreateManifest(context.Background(), createManifestRequest) + + assert.Error(ttt, err) + assert.ErrorContains(ttt, err, `only one of "id" and "value" can be provided`) + }) + + tt.Run("CreateManifest with bad presentation ID returns error", func(ttt *testing.T) { + defID := "a bad ID" + createManifestRequest := getValidManifestRequest("issuerDID", "issuerKID", "schemaID") + createManifestRequest.PresentationDefinitionRef = &model.PresentationDefinitionRef{ + ID: &defID, + } + + _, err := manifestService.CreateManifest(context.Background(), createManifestRequest) + + assert.Error(ttt, err) + assert.ErrorContains(ttt, err, "presentation definition not found") + }) + + tt.Run("CreateManifest with presentation ID returns manifest", func(ttt *testing.T) { + definition := createPresentationDefinition(ttt) + resp, err := presentationService.CreatePresentationDefinition(context.Background(), presmodel.CreatePresentationDefinitionRequest{ + PresentationDefinition: *definition, + }) + assert.NoError(ttt, err) + assert.NotEmpty(ttt, resp) + + createManifestRequest := getValidManifestRequest("issuerDID", "issuerKID", "schemaID") + createManifestRequest.PresentationDefinitionRef = &model.PresentationDefinitionRef{ + ID: &resp.PresentationDefinition.ID, + } + manifest, err := manifestService.CreateManifest(context.Background(), createManifestRequest) + + assert.NoError(ttt, err) + assert.Equal(ttt, resp.PresentationDefinition, *manifest.Manifest.PresentationDefinition) + }) + + tt.Run("multiple behaviors", func(ttt *testing.T) { + // check type and status + assert.Equal(ttt, framework.Manifest, manifestService.Type()) + assert.Equal(ttt, framework.StatusReady, manifestService.Status().Status) + + // create issuer and applicant DIDs + createDIDRequest := did.CreateDIDRequest{ + Method: didsdk.KeyMethod, + KeyType: crypto.Ed25519, + } + issuerDID, err := didService.CreateDIDByMethod(context.Background(), createDIDRequest) + assert.NoError(ttt, err) + assert.NotEmpty(ttt, issuerDID) + + applicantPrivKey, applicantDIDKey, err := key.GenerateDIDKey(crypto.Ed25519) + assert.NoError(ttt, err) + assert.NotEmpty(ttt, applicantPrivKey) + assert.NotEmpty(ttt, applicantDIDKey) + + applicantDID, err := applicantDIDKey.Expand() + assert.NoError(ttt, err) + assert.NotEmpty(ttt, applicantDID) + + // create a schema for the creds to be issued against + licenseSchema := map[string]any{ + "$schema": "https://json-schema.org/draft-07/schema", + "type": "object", "properties": map[string]any{ - "id": map[string]any{ - "type": "string", + "credentialSubject": map[string]any{ + "type": "object", + "properties": map[string]any{ + "id": map[string]any{ + "type": "string", + }, + "licenseType": map[string]any{ + "type": "string", + }, + }, + "required": []any{"licenseType", "id"}, }, - "licenseType": map[string]any{ - "type": "string", + }, + } + kid := issuerDID.DID.VerificationMethod[0].ID + createdSchema, err := schemaService.CreateSchema(context.Background(), schema.CreateSchemaRequest{Issuer: issuerDID.DID.ID, IssuerKID: kid, Name: "license schema", Schema: licenseSchema}) + assert.NoError(ttt, err) + assert.NotEmpty(ttt, createdSchema) + + // issue a credential against the schema to the subject, from the issuer + createdCred, err := credentialService.CreateCredential(context.Background(), credential.CreateCredentialRequest{ + Issuer: issuerDID.DID.ID, + IssuerKID: kid, + Subject: applicantDID.ID, + SchemaID: createdSchema.ID, + Data: map[string]any{"licenseType": "WA-DL-CLASS-A"}, + }) + assert.NoError(ttt, err) + assert.NotEmpty(ttt, createdCred) + + // good manifest request, which asks for a single verifiable credential in the VC-JWT format + createManifestRequest := getValidManifestRequest(issuerDID.DID.ID, kid, createdSchema.ID) + createdManifest, err := manifestService.CreateManifest(context.Background(), createManifestRequest) + assert.NoError(ttt, err) + assert.NotEmpty(ttt, createdManifest) + + manifestRequestRequest := getValidManifestRequestRequest(issuerDID, kid, createdManifest) + manifestRequest, err := manifestService.CreateRequest(context.Background(), manifestRequestRequest) + assert.NoError(ttt, err) + assert.Equal(ttt, createdManifest.Manifest.ID, manifestRequest.ManifestID) + assert.NotEmpty(ttt, manifestRequest.CredentialManifestJWT.String()) + + got, err := manifestService.GetRequest(context.Background(), &model.GetRequestRequest{ID: manifestRequest.ID}) + assert.NoError(t, err) + assert.Equal(t, manifestRequest, got) + + err = manifestService.DeleteRequest(context.Background(), model.DeleteRequestRequest{ID: manifestRequest.ID}) + assert.NoError(t, err) + + _, err = manifestService.GetRequest(context.Background(), &model.GetRequestRequest{ID: manifestRequest.ID}) + assert.Error(t, err) + assert.ErrorContains(t, err, "request not found") + + verificationResponse, err := manifestService.VerifyManifest(context.Background(), model.VerifyManifestRequest{ManifestJWT: manifestRequest.CredentialManifestJWT}) + assert.NoError(ttt, err) + assert.NotEmpty(ttt, verificationResponse) + assert.True(ttt, verificationResponse.Verified) + + m := createdManifest.Manifest + assert.NotEmpty(ttt, m) + + // good application request + containers := []credmodel.Container{{ + ID: createdCred.ID, + CredentialJWT: createdCred.CredentialJWT, + }} + applicationRequest := getValidApplicationRequest(m.ID, m.PresentationDefinition.ID, m.PresentationDefinition.InputDescriptors[0].ID, containers) + + // sign application + signer, err := keyaccess.NewJWKKeyAccess(applicantDID.ID, applicantDID.VerificationMethod[0].ID, applicantPrivKey) + assert.NoError(ttt, err) + signed, err := signer.SignJSON(applicationRequest) + assert.NoError(ttt, err) + + submitApplicationRequest := SubmitApplicationRequest{ApplicationJWT: *signed} + sar, err := submitApplicationRequest.toServiceRequest() + assert.NoError(ttt, err) + createdApplicationResponseOp, err := manifestService.ProcessApplicationSubmission(context.Background(), *sar) + assert.NoError(ttt, err) + assert.False(ttt, createdApplicationResponseOp.Done) + + createdApplicationResponse, err := manifestService.ReviewApplication(context.Background(), model.ReviewApplicationRequest{ + ID: storage.StatusObjectID(createdApplicationResponseOp.ID), + Approved: true, + Reason: "ApprovalMan is here", + CredentialOverrides: map[string]model.CredentialOverride{ + "id1": { + Data: map[string]any{"licenseType": "Class D"}, + }, + "id2": { + Data: map[string]any{"licenseType": "Class D"}, }, }, - "required": []any{"licenseType", "id"}, - }, - }, - } - kid := issuerDID.DID.VerificationMethod[0].ID - createdSchema, err := schemaService.CreateSchema(context.Background(), schema.CreateSchemaRequest{Issuer: issuerDID.DID.ID, IssuerKID: kid, Name: "license schema", Schema: licenseSchema}) - assert.NoError(ttt, err) - assert.NotEmpty(ttt, createdSchema) - - // issue a credential against the schema to the subject, from the issuer - createdCred, err := credentialService.CreateCredential(context.Background(), credential.CreateCredentialRequest{ - Issuer: issuerDID.DID.ID, - IssuerKID: kid, - Subject: applicantDID.ID, - SchemaID: createdSchema.ID, - Data: map[string]any{"licenseType": "WA-DL-CLASS-A"}, + }) + assert.NoError(ttt, err) + assert.NotEmpty(ttt, createdManifest) + assert.NotEmpty(ttt, createdApplicationResponse.Response.ID) + assert.NotEmpty(ttt, createdApplicationResponse.Response.Fulfillment) + assert.Empty(ttt, createdApplicationResponse.Response.Denial) + assert.Equal(ttt, len(createManifestRequest.OutputDescriptors), len(createdApplicationResponse.Credentials)) + }) }) - assert.NoError(ttt, err) - assert.NotEmpty(ttt, createdCred) - - // good manifest request, which asks for a single verifiable credential in the VC-JWT format - createManifestRequest := getValidManifestRequest(issuerDID.DID.ID, kid, createdSchema.ID) - createdManifest, err := manifestService.CreateManifest(context.Background(), createManifestRequest) - assert.NoError(ttt, err) - assert.NotEmpty(ttt, createdManifest) - - manifestRequestRequest := getValidManifestRequestRequest(issuerDID, kid, createdManifest) - manifestRequest, err := manifestService.CreateRequest(context.Background(), manifestRequestRequest) - assert.NoError(ttt, err) - assert.Equal(ttt, createdManifest.Manifest.ID, manifestRequest.ManifestID) - assert.NotEmpty(ttt, manifestRequest.CredentialManifestJWT.String()) - - got, err := manifestService.GetRequest(context.Background(), &model.GetRequestRequest{ID: manifestRequest.ID}) - assert.NoError(t, err) - assert.Equal(t, manifestRequest, got) - - err = manifestService.DeleteRequest(context.Background(), model.DeleteRequestRequest{ID: manifestRequest.ID}) - assert.NoError(t, err) - - _, err = manifestService.GetRequest(context.Background(), &model.GetRequestRequest{ID: manifestRequest.ID}) - assert.Error(t, err) - assert.ErrorContains(t, err, "request not found") - - verificationResponse, err := manifestService.VerifyManifest(context.Background(), model.VerifyManifestRequest{ManifestJWT: manifestRequest.CredentialManifestJWT}) - assert.NoError(ttt, err) - assert.NotEmpty(ttt, verificationResponse) - assert.True(ttt, verificationResponse.Verified) - - m := createdManifest.Manifest - assert.NotEmpty(ttt, m) - - // good application request - containers := []credmodel.Container{{ - ID: createdCred.ID, - CredentialJWT: createdCred.CredentialJWT, - }} - applicationRequest := getValidApplicationRequest(m.ID, m.PresentationDefinition.ID, m.PresentationDefinition.InputDescriptors[0].ID, containers) - - // sign application - signer, err := keyaccess.NewJWKKeyAccess(applicantDID.ID, applicantDID.VerificationMethod[0].ID, applicantPrivKey) - assert.NoError(ttt, err) - signed, err := signer.SignJSON(applicationRequest) - assert.NoError(ttt, err) - - submitApplicationRequest := SubmitApplicationRequest{ApplicationJWT: *signed} - sar, err := submitApplicationRequest.toServiceRequest() - assert.NoError(ttt, err) - createdApplicationResponseOp, err := manifestService.ProcessApplicationSubmission(context.Background(), *sar) - assert.NoError(ttt, err) - assert.False(ttt, createdApplicationResponseOp.Done) - - createdApplicationResponse, err := manifestService.ReviewApplication(context.Background(), model.ReviewApplicationRequest{ - ID: storage.StatusObjectID(createdApplicationResponseOp.ID), - Approved: true, - Reason: "ApprovalMan is here", - CredentialOverrides: map[string]model.CredentialOverride{ - "id1": { - Data: map[string]any{"licenseType": "Class D"}, - }, - "id2": { - Data: map[string]any{"licenseType": "Class D"}, - }, - }, - }) - assert.NoError(ttt, err) - assert.NotEmpty(ttt, createdManifest) - assert.NotEmpty(ttt, createdApplicationResponse.Response.ID) - assert.NotEmpty(ttt, createdApplicationResponse.Response.Fulfillment) - assert.Empty(ttt, createdApplicationResponse.Response.Denial) - assert.Equal(ttt, len(createManifestRequest.OutputDescriptors), len(createdApplicationResponse.Credentials)) }) - }) + } } func getValidManifestRequestRequest(issuerDID *did.CreateDIDResponse, kid string, createdManifest *model.CreateManifestResponse) model.CreateRequestRequest { diff --git a/pkg/server/router/presentation_test.go b/pkg/server/router/presentation_test.go index 94e524c8a..43d0b7eac 100644 --- a/pkg/server/router/presentation_test.go +++ b/pkg/server/router/presentation_test.go @@ -20,6 +20,7 @@ import ( "github.com/tbd54566975/ssi-service/pkg/service/did" "github.com/tbd54566975/ssi-service/pkg/service/presentation" "github.com/tbd54566975/ssi-service/pkg/service/presentation/model" + "github.com/tbd54566975/ssi-service/pkg/testutil" ) func TestPresentationDefinitionRouter(t *testing.T) { @@ -39,188 +40,192 @@ func TestPresentationDefinitionRouter(t *testing.T) { } func TestPresentationDefinitionService(t *testing.T) { - s := setupTestDB(t) - assert.NotEmpty(t, s) - - keyStoreService := testKeyStoreService(t, s) - didService := testDIDService(t, s, keyStoreService) - schemaService := testSchemaService(t, s, keyStoreService, didService) - authorDID, err := didService.CreateDIDByMethod(context.Background(), did.CreateDIDRequest{ - Method: didsdk.KeyMethod, - KeyType: crypto.Ed25519, - }) - require.NoError(t, err) - pubKeyJWK := authorDID.DID.VerificationMethod[0].PublicKeyJWK - require.NotEmpty(t, pubKeyJWK) - pubKey, err := pubKeyJWK.ToPublicKey() - require.NoError(t, err) - ka, err := keyaccess.NewJWKKeyAccessVerifier(authorDID.DID.ID, authorDID.DID.ID, pubKey) - require.NoError(t, err) - - service, err := presentation.NewPresentationService(config.PresentationServiceConfig{}, s, didService.GetResolver(), schemaService, keyStoreService) - require.NoError(t, err) - - t.Run("Create returns the created definition", func(t *testing.T) { - pd := createPresentationDefinition(t) - created, err := service.CreatePresentationDefinition(context.Background(), model.CreatePresentationDefinitionRequest{ - PresentationDefinition: *pd, - }) - - assert.NoError(t, err) - assert.Equal(t, pd, &created.PresentationDefinition) - }) - - t.Run("Get returns the created definition", func(t *testing.T) { - pd := createPresentationDefinition(t) - _, err := service.CreatePresentationDefinition(context.Background(), model.CreatePresentationDefinitionRequest{ - PresentationDefinition: *pd, - }) - assert.NoError(t, err) - - getPd, err := service.GetPresentationDefinition(context.Background(), model.GetPresentationDefinitionRequest{ID: pd.ID}) - - assert.NoError(t, err) - assert.Equal(t, pd.ID, getPd.PresentationDefinition.ID) - assert.Equal(t, pd, &getPd.PresentationDefinition) - }) - - t.Run("Get does not return after deletion", func(t *testing.T) { - pd := createPresentationDefinition(t) - _, err := service.CreatePresentationDefinition(context.Background(), model.CreatePresentationDefinitionRequest{ - PresentationDefinition: *pd, - }) - assert.NoError(t, err) - - assert.NoError(t, service.DeletePresentationDefinition(context.Background(), model.DeletePresentationDefinitionRequest{ID: pd.ID})) - - _, err = service.GetPresentationDefinition(context.Background(), model.GetPresentationDefinitionRequest{ID: pd.ID}) - assert.Error(t, err) - }) - - t.Run("Delete can be called with any ID", func(t *testing.T) { - err := service.DeletePresentationDefinition(context.Background(), model.DeletePresentationDefinitionRequest{ID: "some crazy ID"}) - assert.NoError(t, err) - }) - - t.Run("Signed request is return when creating request", func(t *testing.T) { - pd := createPresentationDefinition(t) - _, err := service.CreatePresentationDefinition(context.Background(), model.CreatePresentationDefinitionRequest{ - PresentationDefinition: *pd, - }) - assert.NoError(t, err) - expectedReq := model.Request{ - Request: common.Request{ - Audience: []string{"did:web:heman"}, - IssuerDID: authorDID.DID.ID, - IssuerKID: authorDID.DID.VerificationMethod[0].ID, - Expiration: time.Date(2023, 10, 10, 10, 10, 10, 0, time.UTC), - }, - PresentationDefinitionID: pd.ID, - } - req, err := service.CreateRequest(context.Background(), model.CreateRequestRequest{ - PresentationRequest: expectedReq, - }) - - assert.NoError(t, err) - assert.NotEmpty(t, req.ID) - assert.NotEqual(t, req.ID, pd.ID) - assert.NoError(t, ka.Verify(req.PresentationDefinitionJWT)) - payload, err := base64.RawURLEncoding.DecodeString(strings.Split(req.PresentationDefinitionJWT.String(), ".")[1]) - assert.NoError(t, err) - var got exchange.PresentationDefinitionEnvelope - assert.NoError(t, json.Unmarshal(payload, &got)) - assert.Equal(t, *pd, got.PresentationDefinition) - assert.Equal(t, expectedReq.Audience, req.Audience) - assert.Equal(t, expectedReq.IssuerDID, req.IssuerDID) - assert.Equal(t, expectedReq.IssuerKID, req.IssuerKID) - assert.Equal(t, expectedReq.PresentationDefinitionID, req.PresentationDefinitionID) - assert.Equal(t, expectedReq.Expiration, req.Expiration) - }) - - t.Run("Get request returns the created request", func(t *testing.T) { - pd := createPresentationDefinition(t) - _, err := service.CreatePresentationDefinition(context.Background(), model.CreatePresentationDefinitionRequest{ - PresentationDefinition: *pd, - }) - assert.NoError(t, err) - req, err := service.CreateRequest(context.Background(), model.CreateRequestRequest{ - PresentationRequest: model.Request{ - Request: common.Request{ - Audience: []string{"did:web:heman"}, - IssuerDID: authorDID.DID.ID, - IssuerKID: authorDID.DID.VerificationMethod[0].ID, - Expiration: time.Now().Add(30 * time.Second), - }, - PresentationDefinitionID: pd.ID, - }, + for _, test := range testutil.TestDatabases { + t.Run(test.Name, func(t *testing.T) { + s := test.ServiceStorage(t) + assert.NotEmpty(t, s) + + keyStoreService := testKeyStoreService(t, s) + didService := testDIDService(t, s, keyStoreService) + schemaService := testSchemaService(t, s, keyStoreService, didService) + authorDID, err := didService.CreateDIDByMethod(context.Background(), did.CreateDIDRequest{ + Method: didsdk.KeyMethod, + KeyType: crypto.Ed25519, + }) + require.NoError(t, err) + pubKeyJWK := authorDID.DID.VerificationMethod[0].PublicKeyJWK + require.NotEmpty(t, pubKeyJWK) + pubKey, err := pubKeyJWK.ToPublicKey() + require.NoError(t, err) + ka, err := keyaccess.NewJWKKeyAccessVerifier(authorDID.DID.ID, authorDID.DID.ID, pubKey) + require.NoError(t, err) + + service, err := presentation.NewPresentationService(config.PresentationServiceConfig{}, s, didService.GetResolver(), schemaService, keyStoreService) + require.NoError(t, err) + + t.Run("Create returns the created definition", func(t *testing.T) { + pd := createPresentationDefinition(t) + created, err := service.CreatePresentationDefinition(context.Background(), model.CreatePresentationDefinitionRequest{ + PresentationDefinition: *pd, + }) + + assert.NoError(t, err) + assert.Equal(t, pd, &created.PresentationDefinition) + }) + + t.Run("Get returns the created definition", func(t *testing.T) { + pd := createPresentationDefinition(t) + _, err := service.CreatePresentationDefinition(context.Background(), model.CreatePresentationDefinitionRequest{ + PresentationDefinition: *pd, + }) + assert.NoError(t, err) + + getPd, err := service.GetPresentationDefinition(context.Background(), model.GetPresentationDefinitionRequest{ID: pd.ID}) + + assert.NoError(t, err) + assert.Equal(t, pd.ID, getPd.PresentationDefinition.ID) + assert.Equal(t, pd, &getPd.PresentationDefinition) + }) + + t.Run("Get does not return after deletion", func(t *testing.T) { + pd := createPresentationDefinition(t) + _, err := service.CreatePresentationDefinition(context.Background(), model.CreatePresentationDefinitionRequest{ + PresentationDefinition: *pd, + }) + assert.NoError(t, err) + + assert.NoError(t, service.DeletePresentationDefinition(context.Background(), model.DeletePresentationDefinitionRequest{ID: pd.ID})) + + _, err = service.GetPresentationDefinition(context.Background(), model.GetPresentationDefinitionRequest{ID: pd.ID}) + assert.Error(t, err) + }) + + t.Run("Delete can be called with any ID", func(t *testing.T) { + err := service.DeletePresentationDefinition(context.Background(), model.DeletePresentationDefinitionRequest{ID: "some crazy ID"}) + assert.NoError(t, err) + }) + + t.Run("Signed request is return when creating request", func(t *testing.T) { + pd := createPresentationDefinition(t) + _, err := service.CreatePresentationDefinition(context.Background(), model.CreatePresentationDefinitionRequest{ + PresentationDefinition: *pd, + }) + assert.NoError(t, err) + expectedReq := model.Request{ + Request: common.Request{ + Audience: []string{"did:web:heman"}, + IssuerDID: authorDID.DID.ID, + IssuerKID: authorDID.DID.VerificationMethod[0].ID, + Expiration: time.Date(2023, 10, 10, 10, 10, 10, 0, time.UTC), + }, + PresentationDefinitionID: pd.ID, + } + req, err := service.CreateRequest(context.Background(), model.CreateRequestRequest{ + PresentationRequest: expectedReq, + }) + + assert.NoError(t, err) + assert.NotEmpty(t, req.ID) + assert.NotEqual(t, req.ID, pd.ID) + assert.NoError(t, ka.Verify(req.PresentationDefinitionJWT)) + payload, err := base64.RawURLEncoding.DecodeString(strings.Split(req.PresentationDefinitionJWT.String(), ".")[1]) + assert.NoError(t, err) + var got exchange.PresentationDefinitionEnvelope + assert.NoError(t, json.Unmarshal(payload, &got)) + assert.Equal(t, *pd, got.PresentationDefinition) + assert.Equal(t, expectedReq.Audience, req.Audience) + assert.Equal(t, expectedReq.IssuerDID, req.IssuerDID) + assert.Equal(t, expectedReq.IssuerKID, req.IssuerKID) + assert.Equal(t, expectedReq.PresentationDefinitionID, req.PresentationDefinitionID) + assert.Equal(t, expectedReq.Expiration, req.Expiration) + }) + + t.Run("Get request returns the created request", func(t *testing.T) { + pd := createPresentationDefinition(t) + _, err := service.CreatePresentationDefinition(context.Background(), model.CreatePresentationDefinitionRequest{ + PresentationDefinition: *pd, + }) + assert.NoError(t, err) + req, err := service.CreateRequest(context.Background(), model.CreateRequestRequest{ + PresentationRequest: model.Request{ + Request: common.Request{ + Audience: []string{"did:web:heman"}, + IssuerDID: authorDID.DID.ID, + IssuerKID: authorDID.DID.VerificationMethod[0].ID, + Expiration: time.Now().Add(30 * time.Second), + }, + PresentationDefinitionID: pd.ID, + }, + }) + assert.NoError(t, err) + + got, err := service.GetRequest(context.Background(), &model.GetRequestRequest{ID: req.ID}) + assert.NoError(t, err) + assert.Equal(t, req, got) + }) + + t.Run("Returns not found after deleting request", func(t *testing.T) { + pd := createPresentationDefinition(t) + _, err := service.CreatePresentationDefinition(context.Background(), model.CreatePresentationDefinitionRequest{ + PresentationDefinition: *pd, + }) + assert.NoError(t, err) + req, err := service.CreateRequest(context.Background(), model.CreateRequestRequest{ + PresentationRequest: model.Request{ + Request: common.Request{ + IssuerDID: authorDID.DID.ID, + IssuerKID: authorDID.DID.VerificationMethod[0].ID, + Expiration: time.Now().Add(30 * time.Second), + }, + PresentationDefinitionID: pd.ID, + }, + }) + assert.NoError(t, err) + + err = service.DeleteRequest(context.Background(), model.DeleteRequestRequest{ID: req.ID}) + assert.NoError(t, err) + + _, err = service.GetRequest(context.Background(), &model.GetRequestRequest{ID: req.ID}) + assert.Error(t, err) + assert.ErrorContains(t, err, "request not found") + }) + + t.Run("Error returned when missing required fields", func(t *testing.T) { + _, err := service.CreateRequest(context.Background(), model.CreateRequestRequest{ + PresentationRequest: model.Request{ + Request: common.Request{ + IssuerDID: "issuer id", + IssuerKID: "kid", + }, + }, + }) + assert.Error(t, err) + assert.ErrorContains(t, err, "failed on the 'required' tag") + + _, err = service.CreateRequest(context.Background(), model.CreateRequestRequest{ + PresentationRequest: model.Request{ + Request: common.Request{ + IssuerDID: "issuer id", + }, + PresentationDefinitionID: "something", + }, + }) + assert.Error(t, err) + assert.ErrorContains(t, err, "failed on the 'required' tag") + + _, err = service.CreateRequest(context.Background(), model.CreateRequestRequest{ + PresentationRequest: model.Request{ + Request: common.Request{ + IssuerKID: "kid", + }, + PresentationDefinitionID: "something", + }, + }) + assert.Error(t, err) + assert.ErrorContains(t, err, "failed on the 'required' tag") + }) }) - assert.NoError(t, err) - - got, err := service.GetRequest(context.Background(), &model.GetRequestRequest{ID: req.ID}) - assert.NoError(t, err) - assert.Equal(t, req, got) - }) - - t.Run("Returns not found after deleting request", func(t *testing.T) { - pd := createPresentationDefinition(t) - _, err := service.CreatePresentationDefinition(context.Background(), model.CreatePresentationDefinitionRequest{ - PresentationDefinition: *pd, - }) - assert.NoError(t, err) - req, err := service.CreateRequest(context.Background(), model.CreateRequestRequest{ - PresentationRequest: model.Request{ - Request: common.Request{ - IssuerDID: authorDID.DID.ID, - IssuerKID: authorDID.DID.VerificationMethod[0].ID, - Expiration: time.Now().Add(30 * time.Second), - }, - PresentationDefinitionID: pd.ID, - }, - }) - assert.NoError(t, err) - - err = service.DeleteRequest(context.Background(), model.DeleteRequestRequest{ID: req.ID}) - assert.NoError(t, err) - - _, err = service.GetRequest(context.Background(), &model.GetRequestRequest{ID: req.ID}) - assert.Error(t, err) - assert.ErrorContains(t, err, "request not found") - }) - - t.Run("Error returned when missing required fields", func(t *testing.T) { - _, err := service.CreateRequest(context.Background(), model.CreateRequestRequest{ - PresentationRequest: model.Request{ - Request: common.Request{ - IssuerDID: "issuer id", - IssuerKID: "kid", - }, - }, - }) - assert.Error(t, err) - assert.ErrorContains(t, err, "failed on the 'required' tag") - - _, err = service.CreateRequest(context.Background(), model.CreateRequestRequest{ - PresentationRequest: model.Request{ - Request: common.Request{ - IssuerDID: "issuer id", - }, - PresentationDefinitionID: "something", - }, - }) - assert.Error(t, err) - assert.ErrorContains(t, err, "failed on the 'required' tag") - - _, err = service.CreateRequest(context.Background(), model.CreateRequestRequest{ - PresentationRequest: model.Request{ - Request: common.Request{ - IssuerKID: "kid", - }, - PresentationDefinitionID: "something", - }, - }) - assert.Error(t, err) - assert.ErrorContains(t, err, "failed on the 'required' tag") - }) + } } func createPresentationDefinition(t *testing.T) *exchange.PresentationDefinition { diff --git a/pkg/server/router/router_test.go b/pkg/server/router/router_test.go index ddc8b0b5b..8fd86ac33 100644 --- a/pkg/server/router/router_test.go +++ b/pkg/server/router/router_test.go @@ -1,15 +1,9 @@ package router import ( - "os" - "testing" - didsdk "github.com/TBD54566975/ssi-sdk/did" - "github.com/stretchr/testify/require" - "github.com/tbd54566975/ssi-service/config" "github.com/tbd54566975/ssi-service/pkg/service/framework" - "github.com/tbd54566975/ssi-service/pkg/storage" ) // generic test config to be used by all tests in this package @@ -34,21 +28,3 @@ func (s *testService) Config() config.ServicesConfig { ManifestConfig: config.ManifestServiceConfig{}, } } - -func setupTestDB(t *testing.T) storage.ServiceStorage { - file, err := os.CreateTemp("", "bolt") - require.NoError(t, err) - name := file.Name() - err = file.Close() - require.NoError(t, err) - s, err := storage.NewStorage(storage.Bolt, storage.Option{ - ID: storage.BoltDBFilePathOption, - Option: name, - }) - require.NoError(t, err) - t.Cleanup(func() { - _ = s.Close() - _ = os.Remove(s.URI()) - }) - return s -} diff --git a/pkg/server/router/schema_test.go b/pkg/server/router/schema_test.go index 4c83b4955..790d6f0f6 100644 --- a/pkg/server/router/schema_test.go +++ b/pkg/server/router/schema_test.go @@ -10,6 +10,7 @@ import ( "github.com/tbd54566975/ssi-service/config" "github.com/tbd54566975/ssi-service/pkg/service/framework" "github.com/tbd54566975/ssi-service/pkg/service/schema" + "github.com/tbd54566975/ssi-service/pkg/testutil" ) func TestSchemaRouter(t *testing.T) { @@ -28,143 +29,152 @@ func TestSchemaRouter(t *testing.T) { assert.Contains(tt, err.Error(), "could not create schema router with service type: test") }) - t.Run("Schema Service Test", func(tt *testing.T) { - bolt := setupTestDB(tt) - assert.NotEmpty(tt, bolt) - - serviceConfig := config.SchemaServiceConfig{BaseServiceConfig: &config.BaseServiceConfig{Name: "schema"}} - keyStoreService := testKeyStoreService(tt, bolt) - didService := testDIDService(tt, bolt, keyStoreService) - schemaService, err := schema.NewSchemaService(serviceConfig, bolt, keyStoreService, didService.GetResolver()) - assert.NoError(tt, err) - assert.NotEmpty(tt, schemaService) - - // check type and status - assert.Equal(tt, framework.Schema, schemaService.Type()) - assert.Equal(tt, framework.StatusReady, schemaService.Status().Status) - - // get all schemas (none) - gotSchemas, err := schemaService.ListSchemas(context.Background()) - assert.NoError(tt, err) - assert.Empty(tt, gotSchemas.Schemas) - - // get schema that doesn't exist - _, err = schemaService.GetSchema(context.Background(), schema.GetSchemaRequest{ID: "bad"}) - assert.Error(tt, err) - assert.Contains(tt, err.Error(), "error getting schema") - - // create a schema - simpleSchema := map[string]any{ - "$schema": "https://json-schema.org/draft-07/schema", - "type": "object", - "properties": map[string]any{ - "credentialSubject": map[string]any{ - "type": "object", + for _, test := range testutil.TestDatabases { + t.Run(test.Name, func(t *testing.T) { + + t.Run("Schema Service Test", func(tt *testing.T) { + db := test.ServiceStorage(t) + assert.NotEmpty(tt, db) + + serviceConfig := config.SchemaServiceConfig{BaseServiceConfig: &config.BaseServiceConfig{Name: "schema"}} + keyStoreService := testKeyStoreService(tt, db) + didService := testDIDService(tt, db, keyStoreService) + schemaService, err := schema.NewSchemaService(serviceConfig, db, keyStoreService, didService.GetResolver()) + assert.NoError(tt, err) + assert.NotEmpty(tt, schemaService) + + // check type and status + assert.Equal(tt, framework.Schema, schemaService.Type()) + assert.Equal(tt, framework.StatusReady, schemaService.Status().Status) + + // get all schemas (none) + gotSchemas, err := schemaService.ListSchemas(context.Background()) + assert.NoError(tt, err) + assert.Empty(tt, gotSchemas.Schemas) + + // get schema that doesn't exist + _, err = schemaService.GetSchema(context.Background(), schema.GetSchemaRequest{ID: "bad"}) + assert.Error(tt, err) + assert.Contains(tt, err.Error(), "error getting schema") + + // create a schema + simpleSchema := map[string]any{ + "$schema": "https://json-schema.org/draft-07/schema", + "type": "object", "properties": map[string]any{ - "id": map[string]any{ - "type": "string", - }, - "firstName": map[string]any{ - "type": "string", - }, - "lastName": map[string]any{ - "type": "string", + "credentialSubject": map[string]any{ + "type": "object", + "properties": map[string]any{ + "id": map[string]any{ + "type": "string", + }, + "firstName": map[string]any{ + "type": "string", + }, + "lastName": map[string]any{ + "type": "string", + }, + }, + "required": []any{"firstName", "lastName"}, }, }, - "required": []any{"firstName", "lastName"}, - }, - }, - } - createdSchema, err := schemaService.CreateSchema(context.Background(), schema.CreateSchemaRequest{Issuer: "me", Name: "simple schema", Schema: simpleSchema}) - assert.NoError(tt, err) - assert.NotEmpty(tt, createdSchema) - assert.NotEmpty(tt, createdSchema.ID) - assert.Equal(tt, "simple schema", createdSchema.Schema.Name()) - - // get schema by ID - gotSchema, err := schemaService.GetSchema(context.Background(), schema.GetSchemaRequest{ID: createdSchema.ID}) - assert.NoError(tt, err) - assert.NotEmpty(tt, gotSchema) - assert.EqualValues(tt, createdSchema.Schema, gotSchema.Schema) - - // get all schemas, expect one - gotSchemas, err = schemaService.ListSchemas(context.Background()) - assert.NoError(tt, err) - assert.NotEmpty(tt, gotSchemas.Schemas) - assert.Len(tt, gotSchemas.Schemas, 1) - - // store another - createdSchema, err = schemaService.CreateSchema(context.Background(), schema.CreateSchemaRequest{Issuer: "me", Name: "simple schema 2", Schema: simpleSchema}) - assert.NoError(tt, err) - assert.NotEmpty(tt, createdSchema) - assert.NotEmpty(tt, createdSchema.ID) - assert.Equal(tt, "simple schema 2", createdSchema.Schema.Name()) - assert.Equal(tt, credschema.JSONSchema2023Type, createdSchema.Type) - - // get all schemas, expect two - gotSchemas, err = schemaService.ListSchemas(context.Background()) - assert.NoError(tt, err) - assert.NotEmpty(tt, gotSchemas.Schemas) - assert.Len(tt, gotSchemas.Schemas, 2) - - // make sure their IDs are different - assert.True(tt, gotSchemas.Schemas[0].ID != gotSchemas.Schemas[1].ID) - - // delete the first schema - err = schemaService.DeleteSchema(context.Background(), schema.DeleteSchemaRequest{ID: gotSchemas.Schemas[0].ID}) - assert.NoError(tt, err) - - // get all schemas, expect one - gotSchemas, err = schemaService.ListSchemas(context.Background()) - assert.NoError(tt, err) - assert.NotEmpty(tt, gotSchemas.Schemas) - assert.Len(tt, gotSchemas.Schemas, 1) - }) + } + createdSchema, err := schemaService.CreateSchema(context.Background(), schema.CreateSchemaRequest{Issuer: "me", Name: "simple schema", Schema: simpleSchema}) + assert.NoError(tt, err) + assert.NotEmpty(tt, createdSchema) + assert.NotEmpty(tt, createdSchema.ID) + assert.Equal(tt, "simple schema", createdSchema.Schema.Name()) + + // get schema by ID + gotSchema, err := schemaService.GetSchema(context.Background(), schema.GetSchemaRequest{ID: createdSchema.ID}) + assert.NoError(tt, err) + assert.NotEmpty(tt, gotSchema) + assert.EqualValues(tt, createdSchema.Schema, gotSchema.Schema) + + // get all schemas, expect one + gotSchemas, err = schemaService.ListSchemas(context.Background()) + assert.NoError(tt, err) + assert.NotEmpty(tt, gotSchemas.Schemas) + assert.Len(tt, gotSchemas.Schemas, 1) + + // store another + createdSchema, err = schemaService.CreateSchema(context.Background(), schema.CreateSchemaRequest{Issuer: "me", Name: "simple schema 2", Schema: simpleSchema}) + assert.NoError(tt, err) + assert.NotEmpty(tt, createdSchema) + assert.NotEmpty(tt, createdSchema.ID) + assert.Equal(tt, "simple schema 2", createdSchema.Schema.Name()) + assert.Equal(tt, credschema.JSONSchema2023Type, createdSchema.Type) + + // get all schemas, expect two + gotSchemas, err = schemaService.ListSchemas(context.Background()) + assert.NoError(tt, err) + assert.NotEmpty(tt, gotSchemas.Schemas) + assert.Len(tt, gotSchemas.Schemas, 2) + + // make sure their IDs are different + assert.True(tt, gotSchemas.Schemas[0].ID != gotSchemas.Schemas[1].ID) + + // delete the first schema + err = schemaService.DeleteSchema(context.Background(), schema.DeleteSchemaRequest{ID: gotSchemas.Schemas[0].ID}) + assert.NoError(tt, err) + + // get all schemas, expect one + gotSchemas, err = schemaService.ListSchemas(context.Background()) + assert.NoError(tt, err) + assert.NotEmpty(tt, gotSchemas.Schemas) + assert.Len(tt, gotSchemas.Schemas, 1) + }) + }) + } } func TestSchemaSigning(t *testing.T) { - t.Run("Unsigned Schema Test", func(tt *testing.T) { - bolt := setupTestDB(tt) - assert.NotEmpty(tt, bolt) - - serviceConfig := config.SchemaServiceConfig{BaseServiceConfig: &config.BaseServiceConfig{Name: "schema"}} - keyStoreService := testKeyStoreService(tt, bolt) - didService := testDIDService(tt, bolt, keyStoreService) - schemaService, err := schema.NewSchemaService(serviceConfig, bolt, keyStoreService, didService.GetResolver()) - assert.NoError(tt, err) - assert.NotEmpty(tt, schemaService) - - // check type and status - assert.Equal(tt, framework.Schema, schemaService.Type()) - assert.Equal(tt, framework.StatusReady, schemaService.Status().Status) - - // create a schema and don't sign it - simpleSchema := map[string]any{ - "$schema": "https://json-schema.org/draft-07/schema", - "type": "object", - "properties": map[string]any{ - "credentialSubject": map[string]any{ - "type": "object", + for _, test := range testutil.TestDatabases { + t.Run(test.Name, func(t *testing.T) { + t.Run("Unsigned Schema Test", func(tt *testing.T) { + db := test.ServiceStorage(t) + assert.NotEmpty(tt, db) + + serviceConfig := config.SchemaServiceConfig{BaseServiceConfig: &config.BaseServiceConfig{Name: "schema"}} + keyStoreService := testKeyStoreService(tt, db) + didService := testDIDService(tt, db, keyStoreService) + schemaService, err := schema.NewSchemaService(serviceConfig, db, keyStoreService, didService.GetResolver()) + assert.NoError(tt, err) + assert.NotEmpty(tt, schemaService) + + // check type and status + assert.Equal(tt, framework.Schema, schemaService.Type()) + assert.Equal(tt, framework.StatusReady, schemaService.Status().Status) + + // create a schema and don't sign it + simpleSchema := map[string]any{ + "$schema": "https://json-schema.org/draft-07/schema", + "type": "object", "properties": map[string]any{ - "id": map[string]any{ - "type": "string", - }, - "firstName": map[string]any{ - "type": "string", - }, - "lastName": map[string]any{ - "type": "string", + "credentialSubject": map[string]any{ + "type": "object", + "properties": map[string]any{ + "id": map[string]any{ + "type": "string", + }, + "firstName": map[string]any{ + "type": "string", + }, + "lastName": map[string]any{ + "type": "string", + }, + }, + "required": []any{"firstName", "lastName"}, }, }, - "required": []any{"firstName", "lastName"}, - }, - }, - } - createdSchema, err := schemaService.CreateSchema(context.Background(), schema.CreateSchemaRequest{Issuer: "me", Name: "simple schema", Schema: simpleSchema}) - assert.NoError(tt, err) - assert.NotEmpty(tt, createdSchema) - assert.NotEmpty(tt, createdSchema.ID) - assert.Equal(tt, "simple schema", createdSchema.Schema.Name()) - }) + } + createdSchema, err := schemaService.CreateSchema(context.Background(), schema.CreateSchemaRequest{Issuer: "me", Name: "simple schema", Schema: simpleSchema}) + assert.NoError(tt, err) + assert.NotEmpty(tt, createdSchema) + assert.NotEmpty(tt, createdSchema.ID) + assert.Equal(tt, "simple schema", createdSchema.Schema.Name()) + }) + }) + } } diff --git a/pkg/server/router/webhook_test.go b/pkg/server/router/webhook_test.go index 2b98fe830..2dbce6ab3 100644 --- a/pkg/server/router/webhook_test.go +++ b/pkg/server/router/webhook_test.go @@ -9,6 +9,7 @@ import ( "github.com/tbd54566975/ssi-service/config" "github.com/tbd54566975/ssi-service/pkg/service/framework" "github.com/tbd54566975/ssi-service/pkg/service/webhook" + "github.com/tbd54566975/ssi-service/pkg/testutil" ) func TestWebhookRouter(t *testing.T) { @@ -26,17 +27,21 @@ func TestWebhookRouter(t *testing.T) { assert.Contains(tt, err.Error(), "could not create webhook router with service type: test") }) - t.Run("Webhook Service Test", func(tt *testing.T) { - db := setupTestDB(tt) - require.NotEmpty(tt, db) - - serviceConfig := config.WebhookServiceConfig{WebhookTimeout: "10s"} - webhookService, err := webhook.NewWebhookService(serviceConfig, db) - assert.NoError(tt, err) - assert.NotEmpty(tt, webhookService) - - // check type and status - assert.Equal(tt, framework.Webhook, webhookService.Type()) - assert.Equal(tt, framework.StatusReady, webhookService.Status().Status) - }) + for _, test := range testutil.TestDatabases { + t.Run(test.Name, func(t *testing.T) { + t.Run("Webhook Service Test", func(tt *testing.T) { + db := test.ServiceStorage(t) + require.NotEmpty(tt, db) + + serviceConfig := config.WebhookServiceConfig{WebhookTimeout: "10s"} + webhookService, err := webhook.NewWebhookService(serviceConfig, db) + assert.NoError(tt, err) + assert.NotEmpty(tt, webhookService) + + // check type and status + assert.Equal(tt, framework.Webhook, webhookService.Type()) + assert.Equal(tt, framework.StatusReady, webhookService.Status().Status) + }) + }) + } } diff --git a/pkg/server/server_credential_test.go b/pkg/server/server_credential_test.go index 66d8f7def..bdfaedbab 100644 --- a/pkg/server/server_credential_test.go +++ b/pkg/server/server_credential_test.go @@ -11,6 +11,7 @@ import ( "github.com/goccy/go-json" "github.com/google/uuid" + "github.com/tbd54566975/ssi-service/pkg/testutil" credsdk "github.com/TBD54566975/ssi-sdk/credential" "github.com/TBD54566975/ssi-sdk/crypto" @@ -26,1104 +27,1110 @@ import ( ) func TestCredentialAPI(t *testing.T) { - t.Run("Batch Create Credentials", func(tt *testing.T) { - bolt := setupTestDB(tt) - require.NotEmpty(tt, bolt) - - keyStoreService := testKeyStoreService(tt, bolt) - didService := testDIDService(tt, bolt, keyStoreService) - schemaService := testSchemaService(tt, bolt, keyStoreService, didService) - credRouter := testCredentialRouter(tt, bolt, keyStoreService, didService, schemaService) - - issuerDID, err := didService.CreateDIDByMethod(context.Background(), did.CreateDIDRequest{ - Method: didsdk.KeyMethod, - KeyType: crypto.Ed25519, - }) - assert.NoError(tt, err) - assert.NotEmpty(tt, issuerDID) - batchCreateCredentialsRequest := router.BatchCreateCredentialsRequest{ - Requests: []router.CreateCredentialRequest{ - { + for _, test := range testutil.TestDatabases { + t.Run(test.Name, func(t *testing.T) { + + t.Run("Batch Create Credentials", func(tt *testing.T) { + db := test.ServiceStorage(t) + require.NotEmpty(tt, db) + + keyStoreService := testKeyStoreService(tt, db) + didService := testDIDService(tt, db, keyStoreService) + schemaService := testSchemaService(tt, db, keyStoreService, didService) + credRouter := testCredentialRouter(tt, db, keyStoreService, didService, schemaService) + + issuerDID, err := didService.CreateDIDByMethod(context.Background(), did.CreateDIDRequest{ + Method: didsdk.KeyMethod, + KeyType: crypto.Ed25519, + }) + assert.NoError(tt, err) + assert.NotEmpty(tt, issuerDID) + + batchCreateCredentialsRequest := router.BatchCreateCredentialsRequest{ + Requests: []router.CreateCredentialRequest{ + { + Issuer: issuerDID.DID.ID, + IssuerKID: issuerDID.DID.VerificationMethod[0].ID, + Subject: "did:abc:456", + Data: map[string]any{ + "firstName": "Jack", + "lastName": "Dorsey", + }, + Expiry: time.Now().Add(24 * time.Hour).Format(time.RFC3339), + }, + { + Issuer: issuerDID.DID.ID, + IssuerKID: issuerDID.DID.VerificationMethod[0].ID, + Subject: "did:abc:789", + Data: map[string]any{ + "firstName": "Lemony", + "lastName": "Snickets", + }, + Expiry: time.Now().Add(12 * time.Hour).Format(time.RFC3339), + Revocable: true, + }, + }, + } + tt.Run("Returns Many Credentials", func(ttt *testing.T) { + requestValue := newRequestValue(ttt, batchCreateCredentialsRequest) + req := httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/credentials/batchCreate", requestValue) + w := httptest.NewRecorder() + c := newRequestContext(w, req) + credRouter.BatchCreateCredentials(c) + assert.True(ttt, util.Is2xxResponse(w.Code)) + + var resp router.BatchCreateCredentialsResponse + err = json.NewDecoder(w.Body).Decode(&resp) + assert.NoError(ttt, err) + + assert.Len(ttt, resp.Credentials, 2) + + assert.NotEmpty(ttt, resp.Credentials[0].CredentialJWT) + assert.Equal(ttt, resp.Credentials[0].Credential.Issuer, issuerDID.DID.ID) + assert.Equal(ttt, "did:abc:456", resp.Credentials[0].Credential.CredentialSubject.GetID()) + assert.Equal(ttt, "Jack", resp.Credentials[0].Credential.CredentialSubject["firstName"]) + assert.Equal(ttt, "Dorsey", resp.Credentials[0].Credential.CredentialSubject["lastName"]) + assert.Empty(ttt, resp.Credentials[0].Credential.CredentialStatus) + + assert.NotEmpty(ttt, resp.Credentials[1].CredentialJWT) + assert.Equal(ttt, resp.Credentials[1].Credential.Issuer, issuerDID.DID.ID) + assert.Equal(ttt, "did:abc:789", resp.Credentials[1].Credential.CredentialSubject.GetID()) + assert.Equal(ttt, "Lemony", resp.Credentials[1].Credential.CredentialSubject["firstName"]) + assert.Equal(ttt, "Snickets", resp.Credentials[1].Credential.CredentialSubject["lastName"]) + assert.NotEmpty(ttt, resp.Credentials[1].Credential.CredentialStatus) + }) + + tt.Run("Fails with malformed request", func(ttt *testing.T) { + batchCreateCredentialsRequest := batchCreateCredentialsRequest + // missing the data field + batchCreateCredentialsRequest.Requests = append(batchCreateCredentialsRequest.Requests, router.CreateCredentialRequest{ + Issuer: issuerDID.DID.ID, + IssuerKID: issuerDID.DID.VerificationMethod[0].ID, + Subject: "did:abc:456", + Expiry: time.Now().Add(24 * time.Hour).Format(time.RFC3339), + }) + + requestValue := newRequestValue(ttt, batchCreateCredentialsRequest) + req := httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/credentials/batchCreate", requestValue) + w := httptest.NewRecorder() + c := newRequestContext(w, req) + credRouter.BatchCreateCredentials(c) + assert.Equal(ttt, http.StatusBadRequest, w.Code) + assert.Contains(ttt, w.Body.String(), "invalid batch create credential request") + }) + + tt.Run("Fails with more than 1000 requests", func(ttt *testing.T) { + batchCreateCredentialsRequest := batchCreateCredentialsRequest + // missing the data field + for i := 0; i < 1000; i++ { + batchCreateCredentialsRequest.Requests = append(batchCreateCredentialsRequest.Requests, batchCreateCredentialsRequest.Requests[0]) + } + + requestValue := newRequestValue(ttt, batchCreateCredentialsRequest) + req := httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/credentials/batchCreate", requestValue) + w := httptest.NewRecorder() + c := newRequestContext(w, req) + credRouter.BatchCreateCredentials(c) + assert.Equal(ttt, http.StatusBadRequest, w.Code) + assert.Contains(ttt, w.Body.String(), "max number of requests is 1000") + }) + }) + t.Run("Test Create Credential", func(tt *testing.T) { + db := test.ServiceStorage(t) + require.NotEmpty(tt, db) + + keyStoreService := testKeyStoreService(tt, db) + didService := testDIDService(tt, db, keyStoreService) + schemaService := testSchemaService(tt, db, keyStoreService, didService) + credRouter := testCredentialRouter(tt, db, keyStoreService, didService, schemaService) + + issuerDID, err := didService.CreateDIDByMethod(context.Background(), did.CreateDIDRequest{ + Method: didsdk.KeyMethod, + KeyType: crypto.Ed25519, + }) + assert.NoError(tt, err) + assert.NotEmpty(tt, issuerDID) + + // missing required field: data + badCredRequest := router.CreateCredentialRequest{ Issuer: issuerDID.DID.ID, IssuerKID: issuerDID.DID.VerificationMethod[0].ID, Subject: "did:abc:456", + Expiry: time.Now().Add(24 * time.Hour).Format(time.RFC3339), + } + badRequestValue := newRequestValue(tt, badCredRequest) + req := httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/credentials", badRequestValue) + w := httptest.NewRecorder() + + c := newRequestContext(w, req) + credRouter.CreateCredential(c) + assert.Contains(tt, w.Body.String(), "invalid create credential request") + + // reset the http recorder + w = httptest.NewRecorder() + + // missing known issuer request + missingIssuerRequest := router.CreateCredentialRequest{ + Issuer: "did:abc:123", + IssuerKID: "did:abc:123#key-1", + Subject: "did:abc:456", Data: map[string]any{ "firstName": "Jack", "lastName": "Dorsey", }, Expiry: time.Now().Add(24 * time.Hour).Format(time.RFC3339), - }, - { + } + missingIssuerRequestValue := newRequestValue(tt, missingIssuerRequest) + req = httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/credentials", missingIssuerRequestValue) + c = newRequestContext(w, req) + credRouter.CreateCredential(c) + assert.Contains(tt, w.Body.String(), "getting key for signing credential") + + // reset the http recorder + w = httptest.NewRecorder() + + // good request + createCredRequest := router.CreateCredentialRequest{ Issuer: issuerDID.DID.ID, IssuerKID: issuerDID.DID.VerificationMethod[0].ID, - Subject: "did:abc:789", + Subject: "did:abc:456", Data: map[string]any{ - "firstName": "Lemony", - "lastName": "Snickets", + "firstName": "Jack", + "lastName": "Dorsey", }, - Expiry: time.Now().Add(12 * time.Hour).Format(time.RFC3339), - Revocable: true, - }, - }, - } - tt.Run("Returns Many Credentials", func(ttt *testing.T) { - requestValue := newRequestValue(ttt, batchCreateCredentialsRequest) - req := httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/credentials/batchCreate", requestValue) - w := httptest.NewRecorder() - c := newRequestContext(w, req) - credRouter.BatchCreateCredentials(c) - assert.True(ttt, util.Is2xxResponse(w.Code)) - - var resp router.BatchCreateCredentialsResponse - err = json.NewDecoder(w.Body).Decode(&resp) - assert.NoError(ttt, err) - - assert.Len(ttt, resp.Credentials, 2) - - assert.NotEmpty(ttt, resp.Credentials[0].CredentialJWT) - assert.Equal(ttt, resp.Credentials[0].Credential.Issuer, issuerDID.DID.ID) - assert.Equal(ttt, "did:abc:456", resp.Credentials[0].Credential.CredentialSubject.GetID()) - assert.Equal(ttt, "Jack", resp.Credentials[0].Credential.CredentialSubject["firstName"]) - assert.Equal(ttt, "Dorsey", resp.Credentials[0].Credential.CredentialSubject["lastName"]) - assert.Empty(ttt, resp.Credentials[0].Credential.CredentialStatus) - - assert.NotEmpty(ttt, resp.Credentials[1].CredentialJWT) - assert.Equal(ttt, resp.Credentials[1].Credential.Issuer, issuerDID.DID.ID) - assert.Equal(ttt, "did:abc:789", resp.Credentials[1].Credential.CredentialSubject.GetID()) - assert.Equal(ttt, "Lemony", resp.Credentials[1].Credential.CredentialSubject["firstName"]) - assert.Equal(ttt, "Snickets", resp.Credentials[1].Credential.CredentialSubject["lastName"]) - assert.NotEmpty(ttt, resp.Credentials[1].Credential.CredentialStatus) - }) + Expiry: time.Now().Add(24 * time.Hour).Format(time.RFC3339), + } + requestValue := newRequestValue(tt, createCredRequest) + req = httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/credentials", requestValue) + c = newRequestContext(w, req) + credRouter.CreateCredential(c) + assert.True(tt, util.Is2xxResponse(w.Code)) + + var resp router.CreateCredentialResponse + err = json.NewDecoder(w.Body).Decode(&resp) + assert.NoError(tt, err) + + assert.NotEmpty(tt, resp.CredentialJWT) + assert.NoError(tt, err) + assert.Equal(tt, resp.Credential.Issuer, issuerDID.DID.ID) + after, found := strings.CutPrefix(resp.Credential.ID, "https://ssi-service.com/v1/credentials/") + assert.True(tt, found) + assert.NotPanics(tt, func() { + uuid.MustParse(after) + }) + }) + + t.Run("Test Create Credential with Schema", func(tt *testing.T) { + db := test.ServiceStorage(t) + require.NotEmpty(tt, db) + + keyStoreService := testKeyStoreService(tt, db) + didService := testDIDService(tt, db, keyStoreService) + schemaService := testSchemaService(tt, db, keyStoreService, didService) + credRouter := testCredentialRouter(tt, db, keyStoreService, didService, schemaService) + + 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 + simpleSchema := map[string]any{ + "$schema": "https://json-schema.org/draft-07/schema", + "type": "object", + "properties": map[string]any{ + "credentialSubject": map[string]any{ + "type": "object", + "properties": map[string]any{ + "id": map[string]any{ + "type": "string", + }, + "firstName": map[string]any{ + "type": "string", + }, + "lastName": map[string]any{ + "type": "string", + }, + }, + "required": []any{"firstName", "lastName"}, + }, + }, + } + createdSchema, err := schemaService.CreateSchema(context.Background(), schema.CreateSchemaRequest{Issuer: "me", Name: "simple schema", Schema: simpleSchema}) + assert.NoError(tt, err) + assert.NotEmpty(tt, createdSchema) - tt.Run("Fails with malformed request", func(ttt *testing.T) { - batchCreateCredentialsRequest := batchCreateCredentialsRequest - // missing the data field - batchCreateCredentialsRequest.Requests = append(batchCreateCredentialsRequest.Requests, router.CreateCredentialRequest{ - Issuer: issuerDID.DID.ID, - IssuerKID: issuerDID.DID.VerificationMethod[0].ID, - Subject: "did:abc:456", - Expiry: time.Now().Add(24 * time.Hour).Format(time.RFC3339), + w := httptest.NewRecorder() + + createCredRequest := router.CreateCredentialRequest{ + Issuer: issuerDID.DID.ID, + IssuerKID: issuerDID.DID.VerificationMethod[0].ID, + Subject: "did:abc:456", + SchemaID: createdSchema.ID, + Data: map[string]any{ + "firstName": "Jack", + "lastName": "Dorsey", + }, + Expiry: time.Now().Add(24 * time.Hour).Format(time.RFC3339), + } + requestValue := newRequestValue(tt, createCredRequest) + req := httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/credentials", requestValue) + c := newRequestContext(w, req) + credRouter.CreateCredential(c) + assert.True(tt, util.Is2xxResponse(w.Code)) + + var resp router.CreateCredentialResponse + err = json.NewDecoder(w.Body).Decode(&resp) + assert.NoError(tt, err) + assert.NotEmpty(tt, resp.CredentialJWT) + + // reset the http recorder + w = httptest.NewRecorder() + + // get credential by schema + req = httptest.NewRequest(http.MethodGet, fmt.Sprintf("https://ssi-service.com/v1/credential?schema=%s", createdSchema.ID), nil) + c = newRequestContextWithParams(w, req, map[string]string{"schema": createdSchema.ID}) + credRouter.ListCredentials(c) + assert.True(tt, util.Is2xxResponse(w.Code)) + + var getCredsResp router.ListCredentialsResponse + err = json.NewDecoder(w.Body).Decode(&getCredsResp) + assert.NoError(tt, err) + assert.NotEmpty(tt, getCredsResp) + assert.Len(tt, getCredsResp.Credentials, 1) + + assert.Equal(tt, resp.Credential.ID, getCredsResp.Credentials[0].ID) + assert.Equal(tt, resp.Credential.CredentialSchema.ID, getCredsResp.Credentials[0].Credential.CredentialSchema.ID) + + // reset the http recorder + w = httptest.NewRecorder() + + // create cred with unknown schema + missingSchemaCred := router.CreateCredentialRequest{ + Issuer: issuerDID.DID.ID, + IssuerKID: issuerDID.DID.VerificationMethod[0].ID, + Subject: "did:abc:456", + SchemaID: "bad", + Data: map[string]any{ + "firstName": "Jack", + "lastName": "Dorsey", + }, + Expiry: time.Now().Add(24 * time.Hour).Format(time.RFC3339), + } + requestValue = newRequestValue(tt, missingSchemaCred) + req = httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/credentials", requestValue) + c = newRequestContext(w, req) + credRouter.CreateCredential(c) + assert.Contains(tt, w.Body.String(), "schema not found") }) - requestValue := newRequestValue(ttt, batchCreateCredentialsRequest) - req := httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/credentials/batchCreate", requestValue) - w := httptest.NewRecorder() - c := newRequestContext(w, req) - credRouter.BatchCreateCredentials(c) - assert.Equal(ttt, http.StatusBadRequest, w.Code) - assert.Contains(ttt, w.Body.String(), "invalid batch create credential request") - }) + t.Run("Test Get Credential By ID", func(tt *testing.T) { + db := test.ServiceStorage(t) + require.NotEmpty(tt, db) - tt.Run("Fails with more than 1000 requests", func(ttt *testing.T) { - batchCreateCredentialsRequest := batchCreateCredentialsRequest - // missing the data field - for i := 0; i < 1000; i++ { - batchCreateCredentialsRequest.Requests = append(batchCreateCredentialsRequest.Requests, batchCreateCredentialsRequest.Requests[0]) - } - - requestValue := newRequestValue(ttt, batchCreateCredentialsRequest) - req := httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/credentials/batchCreate", requestValue) - w := httptest.NewRecorder() - c := newRequestContext(w, req) - credRouter.BatchCreateCredentials(c) - assert.Equal(ttt, http.StatusBadRequest, w.Code) - assert.Contains(ttt, w.Body.String(), "max number of requests is 1000") - }) - }) - t.Run("Test Create Credential", func(tt *testing.T) { - bolt := setupTestDB(tt) - require.NotEmpty(tt, bolt) - - keyStoreService := testKeyStoreService(tt, bolt) - didService := testDIDService(tt, bolt, keyStoreService) - schemaService := testSchemaService(tt, bolt, keyStoreService, didService) - credRouter := testCredentialRouter(tt, bolt, keyStoreService, didService, schemaService) - - issuerDID, err := didService.CreateDIDByMethod(context.Background(), did.CreateDIDRequest{ - Method: didsdk.KeyMethod, - KeyType: crypto.Ed25519, - }) - assert.NoError(tt, err) - assert.NotEmpty(tt, issuerDID) - - // missing required field: data - badCredRequest := router.CreateCredentialRequest{ - Issuer: issuerDID.DID.ID, - IssuerKID: issuerDID.DID.VerificationMethod[0].ID, - Subject: "did:abc:456", - Expiry: time.Now().Add(24 * time.Hour).Format(time.RFC3339), - } - badRequestValue := newRequestValue(tt, badCredRequest) - req := httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/credentials", badRequestValue) - w := httptest.NewRecorder() - - c := newRequestContext(w, req) - credRouter.CreateCredential(c) - assert.Contains(tt, w.Body.String(), "invalid create credential request") - - // reset the http recorder - w = httptest.NewRecorder() - - // missing known issuer request - missingIssuerRequest := router.CreateCredentialRequest{ - Issuer: "did:abc:123", - IssuerKID: "did:abc:123#key-1", - Subject: "did:abc:456", - Data: map[string]any{ - "firstName": "Jack", - "lastName": "Dorsey", - }, - Expiry: time.Now().Add(24 * time.Hour).Format(time.RFC3339), - } - missingIssuerRequestValue := newRequestValue(tt, missingIssuerRequest) - req = httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/credentials", missingIssuerRequestValue) - c = newRequestContext(w, req) - credRouter.CreateCredential(c) - assert.Contains(tt, w.Body.String(), "getting key for signing credential") - - // reset the http recorder - w = httptest.NewRecorder() - - // good request - createCredRequest := router.CreateCredentialRequest{ - Issuer: issuerDID.DID.ID, - IssuerKID: issuerDID.DID.VerificationMethod[0].ID, - Subject: "did:abc:456", - Data: map[string]any{ - "firstName": "Jack", - "lastName": "Dorsey", - }, - Expiry: time.Now().Add(24 * time.Hour).Format(time.RFC3339), - } - requestValue := newRequestValue(tt, createCredRequest) - req = httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/credentials", requestValue) - c = newRequestContext(w, req) - credRouter.CreateCredential(c) - assert.True(tt, util.Is2xxResponse(w.Code)) - - var resp router.CreateCredentialResponse - err = json.NewDecoder(w.Body).Decode(&resp) - assert.NoError(tt, err) - - assert.NotEmpty(tt, resp.CredentialJWT) - assert.NoError(tt, err) - assert.Equal(tt, resp.Credential.Issuer, issuerDID.DID.ID) - after, found := strings.CutPrefix(resp.Credential.ID, "https://ssi-service.com/v1/credentials/") - assert.True(tt, found) - assert.NotPanics(tt, func() { - uuid.MustParse(after) - }) - }) + keyStoreService := testKeyStoreService(tt, db) + didService := testDIDService(tt, db, keyStoreService) + schemaService := testSchemaService(tt, db, keyStoreService, didService) + credRouter := testCredentialRouter(tt, db, keyStoreService, didService, schemaService) - t.Run("Test Create Credential with Schema", func(tt *testing.T) { - bolt := setupTestDB(tt) - require.NotEmpty(tt, bolt) + w := httptest.NewRecorder() - keyStoreService := testKeyStoreService(tt, bolt) - didService := testDIDService(tt, bolt, keyStoreService) - schemaService := testSchemaService(tt, bolt, keyStoreService, didService) - credRouter := testCredentialRouter(tt, bolt, keyStoreService, didService, schemaService) + // get a cred that doesn't exit + req := httptest.NewRequest(http.MethodGet, "https://ssi-service.com/v1/credentials/bad", nil) + c := newRequestContext(w, req) + credRouter.GetCredential(c) + assert.Contains(tt, w.Body.String(), "cannot get credential without ID parameter") - 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 - simpleSchema := map[string]any{ - "$schema": "https://json-schema.org/draft-07/schema", - "type": "object", - "properties": map[string]any{ - "credentialSubject": map[string]any{ - "type": "object", - "properties": map[string]any{ - "id": map[string]any{ - "type": "string", - }, - "firstName": map[string]any{ - "type": "string", - }, - "lastName": map[string]any{ - "type": "string", - }, + // reset the http recorder + w = httptest.NewRecorder() + + // get a cred with an invalid id parameter + req = httptest.NewRequest(http.MethodGet, "https://ssi-service.com/v1/credentials/bad", nil) + c = newRequestContextWithParams(w, req, map[string]string{"id": "bad"}) + credRouter.GetCredential(c) + assert.Contains(tt, w.Body.String(), "could not get credential with id: bad") + + // reset the http recorder + w = httptest.NewRecorder() + + issuerDID, err := didService.CreateDIDByMethod(context.Background(), did.CreateDIDRequest{ + Method: didsdk.KeyMethod, + KeyType: crypto.Ed25519, + }) + assert.NoError(tt, err) + assert.NotEmpty(tt, issuerDID) + + createCredRequest := router.CreateCredentialRequest{ + Issuer: issuerDID.DID.ID, + IssuerKID: issuerDID.DID.VerificationMethod[0].ID, + Subject: "did:abc:456", + Data: map[string]any{ + "firstName": "Jack", + "lastName": "Dorsey", }, - "required": []any{"firstName", "lastName"}, - }, - }, - } - createdSchema, err := schemaService.CreateSchema(context.Background(), schema.CreateSchemaRequest{Issuer: "me", Name: "simple schema", Schema: simpleSchema}) - assert.NoError(tt, err) - assert.NotEmpty(tt, createdSchema) - - w := httptest.NewRecorder() - - createCredRequest := router.CreateCredentialRequest{ - Issuer: issuerDID.DID.ID, - IssuerKID: issuerDID.DID.VerificationMethod[0].ID, - Subject: "did:abc:456", - SchemaID: createdSchema.ID, - Data: map[string]any{ - "firstName": "Jack", - "lastName": "Dorsey", - }, - Expiry: time.Now().Add(24 * time.Hour).Format(time.RFC3339), - } - requestValue := newRequestValue(tt, createCredRequest) - req := httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/credentials", requestValue) - c := newRequestContext(w, req) - credRouter.CreateCredential(c) - assert.True(tt, util.Is2xxResponse(w.Code)) - - var resp router.CreateCredentialResponse - err = json.NewDecoder(w.Body).Decode(&resp) - assert.NoError(tt, err) - assert.NotEmpty(tt, resp.CredentialJWT) - - // reset the http recorder - w = httptest.NewRecorder() - - // get credential by schema - req = httptest.NewRequest(http.MethodGet, fmt.Sprintf("https://ssi-service.com/v1/credential?schema=%s", createdSchema.ID), nil) - c = newRequestContextWithParams(w, req, map[string]string{"schema": createdSchema.ID}) - credRouter.ListCredentials(c) - assert.True(tt, util.Is2xxResponse(w.Code)) - - var getCredsResp router.ListCredentialsResponse - err = json.NewDecoder(w.Body).Decode(&getCredsResp) - assert.NoError(tt, err) - assert.NotEmpty(tt, getCredsResp) - assert.Len(tt, getCredsResp.Credentials, 1) - - assert.Equal(tt, resp.Credential.ID, getCredsResp.Credentials[0].ID) - assert.Equal(tt, resp.Credential.CredentialSchema.ID, getCredsResp.Credentials[0].Credential.CredentialSchema.ID) - - // reset the http recorder - w = httptest.NewRecorder() - - // create cred with unknown schema - missingSchemaCred := router.CreateCredentialRequest{ - Issuer: issuerDID.DID.ID, - IssuerKID: issuerDID.DID.VerificationMethod[0].ID, - Subject: "did:abc:456", - SchemaID: "bad", - Data: map[string]any{ - "firstName": "Jack", - "lastName": "Dorsey", - }, - Expiry: time.Now().Add(24 * time.Hour).Format(time.RFC3339), - } - requestValue = newRequestValue(tt, missingSchemaCred) - req = httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/credentials", requestValue) - c = newRequestContext(w, req) - credRouter.CreateCredential(c) - assert.Contains(tt, w.Body.String(), "schema not found") - }) - - t.Run("Test Get Credential By ID", func(tt *testing.T) { - bolt := setupTestDB(tt) - require.NotEmpty(tt, bolt) - - keyStoreService := testKeyStoreService(tt, bolt) - didService := testDIDService(tt, bolt, keyStoreService) - schemaService := testSchemaService(tt, bolt, keyStoreService, didService) - credRouter := testCredentialRouter(tt, bolt, keyStoreService, didService, schemaService) - - w := httptest.NewRecorder() - - // get a cred that doesn't exit - req := httptest.NewRequest(http.MethodGet, "https://ssi-service.com/v1/credentials/bad", nil) - c := newRequestContext(w, req) - credRouter.GetCredential(c) - assert.Contains(tt, w.Body.String(), "cannot get credential without ID parameter") - - // reset the http recorder - w = httptest.NewRecorder() - - // get a cred with an invalid id parameter - req = httptest.NewRequest(http.MethodGet, "https://ssi-service.com/v1/credentials/bad", nil) - c = newRequestContextWithParams(w, req, map[string]string{"id": "bad"}) - credRouter.GetCredential(c) - assert.Contains(tt, w.Body.String(), "could not get credential with id: bad") - - // reset the http recorder - w = httptest.NewRecorder() - - issuerDID, err := didService.CreateDIDByMethod(context.Background(), did.CreateDIDRequest{ - Method: didsdk.KeyMethod, - KeyType: crypto.Ed25519, - }) - assert.NoError(tt, err) - assert.NotEmpty(tt, issuerDID) - - createCredRequest := router.CreateCredentialRequest{ - Issuer: issuerDID.DID.ID, - IssuerKID: issuerDID.DID.VerificationMethod[0].ID, - Subject: "did:abc:456", - Data: map[string]any{ - "firstName": "Jack", - "lastName": "Dorsey", - }, - Expiry: time.Now().Add(24 * time.Hour).Format(time.RFC3339), - } - requestValue := newRequestValue(tt, createCredRequest) - req = httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/credentials", requestValue) - c = newRequestContext(w, req) - credRouter.CreateCredential(c) - assert.True(tt, util.Is2xxResponse(w.Code)) - - // We expect a JWT credential - var resp router.CreateCredentialResponse - err = json.NewDecoder(w.Body).Decode(&resp) - assert.NoError(tt, err) - assert.NotEmpty(tt, resp.Credential) - assert.NotEmpty(tt, resp.CredentialJWT) - - // reset the http recorder - w = httptest.NewRecorder() - - // get credential by id - req = httptest.NewRequest(http.MethodGet, resp.Credential.ID, nil) - c = newRequestContextWithParams(w, req, map[string]string{"id": idFromURI(resp.Credential.ID)}) - credRouter.GetCredential(c) - assert.True(tt, util.Is2xxResponse(w.Code)) - - var getCredResp router.GetCredentialResponse - err = json.NewDecoder(w.Body).Decode(&getCredResp) - assert.NoError(tt, err) - assert.NotEmpty(tt, getCredResp) - assert.NotEmpty(tt, getCredResp.CredentialJWT) - assert.Equal(tt, resp.Credential.ID, getCredResp.Credential.ID) - }) - - t.Run("Test Get Credential By Schema", func(tt *testing.T) { - bolt := setupTestDB(tt) - require.NotEmpty(tt, bolt) - - keyStoreService := testKeyStoreService(tt, bolt) - didService := testDIDService(tt, bolt, keyStoreService) - schemaService := testSchemaService(tt, bolt, keyStoreService, didService) - credRouter := testCredentialRouter(tt, bolt, keyStoreService, didService, schemaService) - - w := httptest.NewRecorder() - - 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 - simpleSchema := map[string]any{ - "$schema": "https://json-schema.org/draft-07/schema", - "type": "object", - "properties": map[string]any{ - "credentialSubject": map[string]any{ - "type": "object", + Expiry: time.Now().Add(24 * time.Hour).Format(time.RFC3339), + } + requestValue := newRequestValue(tt, createCredRequest) + req = httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/credentials", requestValue) + c = newRequestContext(w, req) + credRouter.CreateCredential(c) + assert.True(tt, util.Is2xxResponse(w.Code)) + + // We expect a JWT credential + var resp router.CreateCredentialResponse + err = json.NewDecoder(w.Body).Decode(&resp) + assert.NoError(tt, err) + assert.NotEmpty(tt, resp.Credential) + assert.NotEmpty(tt, resp.CredentialJWT) + + // reset the http recorder + w = httptest.NewRecorder() + + // get credential by id + req = httptest.NewRequest(http.MethodGet, resp.Credential.ID, nil) + c = newRequestContextWithParams(w, req, map[string]string{"id": idFromURI(resp.Credential.ID)}) + credRouter.GetCredential(c) + assert.True(tt, util.Is2xxResponse(w.Code)) + + var getCredResp router.GetCredentialResponse + err = json.NewDecoder(w.Body).Decode(&getCredResp) + assert.NoError(tt, err) + assert.NotEmpty(tt, getCredResp) + assert.NotEmpty(tt, getCredResp.CredentialJWT) + assert.Equal(tt, resp.Credential.ID, getCredResp.Credential.ID) + }) + + t.Run("Test Get Credential By Schema", func(tt *testing.T) { + db := test.ServiceStorage(t) + require.NotEmpty(tt, db) + + keyStoreService := testKeyStoreService(tt, db) + didService := testDIDService(tt, db, keyStoreService) + schemaService := testSchemaService(tt, db, keyStoreService, didService) + credRouter := testCredentialRouter(tt, db, keyStoreService, didService, schemaService) + + w := httptest.NewRecorder() + + 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 + simpleSchema := map[string]any{ + "$schema": "https://json-schema.org/draft-07/schema", + "type": "object", "properties": map[string]any{ - "id": map[string]any{ - "type": "string", - }, - "firstName": map[string]any{ - "type": "string", - }, - "lastName": map[string]any{ - "type": "string", + "credentialSubject": map[string]any{ + "type": "object", + "properties": map[string]any{ + "id": map[string]any{ + "type": "string", + }, + "firstName": map[string]any{ + "type": "string", + }, + "lastName": map[string]any{ + "type": "string", + }, + }, + "required": []any{"firstName", "lastName"}, }, }, - "required": []any{"firstName", "lastName"}, - }, - }, - } - createdSchema, err := schemaService.CreateSchema(context.Background(), schema.CreateSchemaRequest{Issuer: "me", Name: "simple schema", Schema: simpleSchema}) - assert.NoError(tt, err) - assert.NotEmpty(tt, createdSchema) - - createCredRequest := router.CreateCredentialRequest{ - Issuer: issuerDID.DID.ID, - IssuerKID: issuerDID.DID.VerificationMethod[0].ID, - Subject: "did:abc:456", - SchemaID: createdSchema.ID, - Data: map[string]any{ - "firstName": "Jack", - "lastName": "Dorsey", - }, - Expiry: time.Now().Add(24 * time.Hour).Format(time.RFC3339), - } - requestValue := newRequestValue(tt, createCredRequest) - req := httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/credentials", requestValue) - c := newRequestContext(w, req) - credRouter.CreateCredential(c) - assert.True(tt, util.Is2xxResponse(w.Code)) - - var resp router.CreateCredentialResponse - err = json.NewDecoder(w.Body).Decode(&resp) - assert.NoError(tt, err) - assert.NotEmpty(tt, resp.CredentialJWT) - - // reset the http recorder - w = httptest.NewRecorder() - - // get credential by schema - req = httptest.NewRequest(http.MethodGet, fmt.Sprintf("https://ssi-service.com/v1/credential?schema=%s", createdSchema.ID), nil) - c = newRequestContext(w, req) - credRouter.ListCredentials(c) - assert.True(tt, util.Is2xxResponse(w.Code)) - - var getCredsResp router.ListCredentialsResponse - err = json.NewDecoder(w.Body).Decode(&getCredsResp) - assert.NoError(tt, err) - assert.NotEmpty(tt, getCredsResp) - assert.Len(tt, getCredsResp.Credentials, 1) - - assert.Equal(tt, resp.Credential.ID, getCredsResp.Credentials[0].ID) - assert.Equal(tt, resp.Credential.CredentialSchema.ID, getCredsResp.Credentials[0].Credential.CredentialSchema.ID) - }) - - t.Run("Get Credential No Param", func(tt *testing.T) { - bolt := setupTestDB(tt) - require.NotEmpty(tt, bolt) - - keyStoreService := testKeyStoreService(tt, bolt) - didService := testDIDService(tt, bolt, keyStoreService) - schemaService := testSchemaService(tt, bolt, keyStoreService, didService) - credRouter := testCredentialRouter(tt, bolt, keyStoreService, didService, schemaService) - - w := httptest.NewRecorder() - - issuerDID, err := didService.CreateDIDByMethod(context.Background(), did.CreateDIDRequest{ - Method: didsdk.KeyMethod, - KeyType: crypto.Ed25519, - }) - assert.NoError(tt, err) - assert.NotEmpty(tt, issuerDID) - - createCredRequest := router.CreateCredentialRequest{ - Issuer: issuerDID.DID.ID, - IssuerKID: issuerDID.DID.VerificationMethod[0].ID, - Subject: "did:abc:456", - Data: map[string]any{ - "firstName": "Jack", - "lastName": "Dorsey", - }, - Expiry: time.Now().Add(24 * time.Hour).Format(time.RFC3339), - } - requestValue := newRequestValue(tt, createCredRequest) - req := httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/credentials", requestValue) - c := newRequestContext(w, req) - credRouter.CreateCredential(c) - assert.True(tt, util.Is2xxResponse(w.Code)) - - var resp router.CreateCredentialResponse - err = json.NewDecoder(w.Body).Decode(&resp) - assert.NoError(tt, err) - assert.NotEmpty(tt, resp.CredentialJWT) - - // reset the http recorder - w = httptest.NewRecorder() - - // get credential by issuer id - req = httptest.NewRequest(http.MethodGet, "https://ssi-service.com/v1/credentials", nil) - c = newRequestContext(w, req) - credRouter.ListCredentials(c) - assert.True(tt, util.Is2xxResponse(w.Code)) - - var getCredsResp router.ListCredentialsResponse - err = json.NewDecoder(w.Body).Decode(&getCredsResp) - assert.NoError(tt, err) - assert.NotEmpty(tt, getCredsResp) - - assert.Len(tt, getCredsResp.Credentials, 1) - assert.Equal(tt, resp.Credential.ID, getCredsResp.Credentials[0].ID) - }) - - t.Run("Test Get Credential By Issuer", func(tt *testing.T) { - bolt := setupTestDB(tt) - require.NotEmpty(tt, bolt) - - keyStoreService := testKeyStoreService(tt, bolt) - didService := testDIDService(tt, bolt, keyStoreService) - schemaService := testSchemaService(tt, bolt, keyStoreService, didService) - credRouter := testCredentialRouter(tt, bolt, keyStoreService, didService, schemaService) - - w := httptest.NewRecorder() - - issuerDID, err := didService.CreateDIDByMethod(context.Background(), did.CreateDIDRequest{ - Method: didsdk.KeyMethod, - KeyType: crypto.Ed25519, - }) - assert.NoError(tt, err) - assert.NotEmpty(tt, issuerDID) - - createCredRequest := router.CreateCredentialRequest{ - Issuer: issuerDID.DID.ID, - IssuerKID: issuerDID.DID.VerificationMethod[0].ID, - Subject: "did:abc:456", - Data: map[string]any{ - "firstName": "Jack", - "lastName": "Dorsey", - }, - Expiry: time.Now().Add(24 * time.Hour).Format(time.RFC3339), - } - requestValue := newRequestValue(tt, createCredRequest) - req := httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/credentials", requestValue) - c := newRequestContext(w, req) - credRouter.CreateCredential(c) - assert.True(tt, util.Is2xxResponse(w.Code)) - - var resp router.CreateCredentialResponse - err = json.NewDecoder(w.Body).Decode(&resp) - assert.NoError(tt, err) - assert.NotEmpty(tt, resp.CredentialJWT) - - // reset the http recorder - w = httptest.NewRecorder() - - // get credential by issuer id - req = httptest.NewRequest(http.MethodGet, fmt.Sprintf("https://ssi-service.com/v1/credential?issuer=%s", issuerDID.DID.ID), nil) - c = newRequestContext(w, req) - credRouter.ListCredentials(c) - assert.True(tt, util.Is2xxResponse(w.Code)) - - var getCredsResp router.ListCredentialsResponse - err = json.NewDecoder(w.Body).Decode(&getCredsResp) - assert.NoError(tt, err) - assert.NotEmpty(tt, getCredsResp) - - assert.Len(tt, getCredsResp.Credentials, 1) - assert.Equal(tt, resp.Credential.ID, getCredsResp.Credentials[0].ID) - }) - - t.Run("Test Get Credential By Subject", func(tt *testing.T) { - bolt := setupTestDB(tt) - require.NotEmpty(tt, bolt) - - keyStoreService := testKeyStoreService(tt, bolt) - didService := testDIDService(tt, bolt, keyStoreService) - schemaService := testSchemaService(tt, bolt, keyStoreService, didService) - credRouter := testCredentialRouter(tt, bolt, keyStoreService, didService, schemaService) - - w := httptest.NewRecorder() - - issuerDID, err := didService.CreateDIDByMethod(context.Background(), did.CreateDIDRequest{ - Method: didsdk.KeyMethod, - KeyType: crypto.Ed25519, - }) - assert.NoError(tt, err) - assert.NotEmpty(tt, issuerDID) - - subjectID := "did:abc:456" - createCredRequest := router.CreateCredentialRequest{ - Issuer: issuerDID.DID.ID, - IssuerKID: issuerDID.DID.VerificationMethod[0].ID, - Subject: subjectID, - Data: map[string]any{ - "firstName": "Jack", - "lastName": "Dorsey", - }, - Expiry: time.Now().Add(24 * time.Hour).Format(time.RFC3339), - } - requestValue := newRequestValue(tt, createCredRequest) - req := httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/credentials", requestValue) - c := newRequestContext(w, req) - credRouter.CreateCredential(c) - assert.True(tt, util.Is2xxResponse(w.Code)) - - var createCredentialResponse router.CreateCredentialResponse - err = json.NewDecoder(w.Body).Decode(&createCredentialResponse) - assert.NoError(tt, err) - assert.NotEmpty(tt, createCredentialResponse.CredentialJWT) - - // reset the http recorder - w = httptest.NewRecorder() - - // get credential by subject id - req = httptest.NewRequest(http.MethodGet, fmt.Sprintf("https://ssi-service.com/v1/credential?subject=%s", subjectID), nil) - c = newRequestContext(w, req) - credRouter.ListCredentials(c) - assert.True(tt, util.Is2xxResponse(w.Code)) - - var listCredentialsResponse router.ListCredentialsResponse - err = json.NewDecoder(w.Body).Decode(&listCredentialsResponse) - assert.NoError(tt, err) - assert.NotEmpty(tt, listCredentialsResponse) - - assert.Len(tt, listCredentialsResponse.Credentials, 1) - assert.Equal(tt, createCredentialResponse.Credential.ID, listCredentialsResponse.Credentials[0].ID) - assert.Equal(tt, createCredentialResponse.Credential.CredentialSubject[credsdk.VerifiableCredentialIDProperty], listCredentialsResponse.Credentials[0].Credential.CredentialSubject[credsdk.VerifiableCredentialIDProperty]) - }) - - t.Run("Test Delete Credential", func(tt *testing.T) { - bolt := setupTestDB(tt) - require.NotEmpty(tt, bolt) - - keyStoreService := testKeyStoreService(tt, bolt) - didService := testDIDService(tt, bolt, keyStoreService) - schemaService := testSchemaService(tt, bolt, keyStoreService, didService) - credRouter := testCredentialRouter(tt, bolt, keyStoreService, didService, schemaService) - - issuerDID, err := didService.CreateDIDByMethod(context.Background(), did.CreateDIDRequest{ - Method: didsdk.KeyMethod, - KeyType: crypto.Ed25519, - }) - assert.NoError(tt, err) - assert.NotEmpty(tt, issuerDID) - - createCredRequest := router.CreateCredentialRequest{ - Issuer: issuerDID.DID.ID, - IssuerKID: issuerDID.DID.VerificationMethod[0].ID, - Subject: "did:abc:456", - Data: map[string]any{ - "firstName": "Jack", - "lastName": "Dorsey", - }, - Expiry: time.Now().Add(24 * time.Hour).Format(time.RFC3339), - } - requestValue := newRequestValue(tt, createCredRequest) - req := httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/credentials", requestValue) - w := httptest.NewRecorder() - c := newRequestContext(w, req) - credRouter.CreateCredential(c) - assert.True(tt, util.Is2xxResponse(w.Code)) - - var resp router.CreateCredentialResponse - err = json.NewDecoder(w.Body).Decode(&resp) - assert.NoError(tt, err) - - // reset the http recorder - w = httptest.NewRecorder() - - // get credential by id - credID := resp.Credential.ID - req = httptest.NewRequest(http.MethodGet, credID, nil) - c = newRequestContextWithParams(w, req, map[string]string{"id": idFromURI(credID)}) - credRouter.GetCredential(c) - assert.True(tt, util.Is2xxResponse(w.Code)) - - var getCredResp router.GetCredentialResponse - err = json.NewDecoder(w.Body).Decode(&getCredResp) - assert.NoError(tt, err) - assert.NotEmpty(tt, getCredResp) - assert.Equal(tt, credID, getCredResp.Credential.ID) - - // reset the http recorder - w = httptest.NewRecorder() - - // delete it - req = httptest.NewRequest(http.MethodDelete, fmt.Sprintf("https://ssi-service.com/v1/credentials/%s", credID), nil) - c = newRequestContextWithParams(w, req, map[string]string{"id": credID}) - credRouter.DeleteCredential(c) - assert.True(tt, util.Is2xxResponse(w.Code)) - - // reset the http recorder - w = httptest.NewRecorder() - - // get it back - req = httptest.NewRequest(http.MethodGet, fmt.Sprintf("https://ssi-service.com/v1/credentials/%s", credID), nil) - c = newRequestContextWithParams(w, req, map[string]string{"id": credID}) - credRouter.GetCredential(c) - assert.Contains(tt, w.Body.String(), fmt.Sprintf("could not get credential with id: %s", credID)) - }) - - t.Run("Test Verifying a Credential", func(tt *testing.T) { - bolt := setupTestDB(tt) - require.NotEmpty(tt, bolt) - - keyStoreService := testKeyStoreService(tt, bolt) - didService := testDIDService(tt, bolt, keyStoreService) - schemaService := testSchemaService(tt, bolt, keyStoreService, didService) - credRouter := testCredentialRouter(tt, bolt, keyStoreService, didService, schemaService) - - issuerDID, err := didService.CreateDIDByMethod(context.Background(), did.CreateDIDRequest{ - Method: didsdk.KeyMethod, - KeyType: crypto.Ed25519, - }) - assert.NoError(tt, err) - assert.NotEmpty(tt, issuerDID) - - // good request - createCredRequest := router.CreateCredentialRequest{ - Issuer: issuerDID.DID.ID, - IssuerKID: issuerDID.DID.VerificationMethod[0].ID, - Subject: "did:abc:456", - Data: map[string]any{ - "firstName": "Jack", - "lastName": "Dorsey", - }, - Expiry: time.Now().Add(24 * time.Hour).Format(time.RFC3339), - } - requestValue := newRequestValue(tt, createCredRequest) - req := httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/credentials", requestValue) - w := httptest.NewRecorder() - c := newRequestContext(w, req) - credRouter.CreateCredential(c) - assert.True(tt, util.Is2xxResponse(w.Code)) - - var resp router.CreateCredentialResponse - err = json.NewDecoder(w.Body).Decode(&resp) - assert.NoError(tt, err) - - assert.NotEmpty(tt, resp.CredentialJWT) - assert.NoError(tt, err) - assert.Equal(tt, resp.Credential.Issuer, issuerDID.DID.ID) - - // reset the http recorder - w = httptest.NewRecorder() - - // verify the credential - requestValue = newRequestValue(tt, router.VerifyCredentialRequest{CredentialJWT: resp.CredentialJWT}) - req = httptest.NewRequest(http.MethodPost, "https://ssi-service.com/v1/credentials/verification", requestValue) - c = newRequestContext(w, req) - credRouter.VerifyCredential(c) - assert.True(tt, util.Is2xxResponse(w.Code)) - - var verifyResp router.VerifyCredentialResponse - err = json.NewDecoder(w.Body).Decode(&verifyResp) - assert.NoError(tt, err) - assert.NotEmpty(tt, verifyResp) - assert.True(tt, verifyResp.Verified) - - // bad credential - requestValue = newRequestValue(tt, router.VerifyCredentialRequest{CredentialJWT: keyaccess.JWTPtr("bad")}) - req = httptest.NewRequest(http.MethodPost, "https://ssi-service.com/v1/credentials/verification", requestValue) - c = newRequestContext(w, req) - credRouter.VerifyCredential(c) - assert.True(tt, util.Is2xxResponse(w.Code)) - - err = json.NewDecoder(w.Body).Decode(&verifyResp) - assert.NoError(tt, err) - assert.NotEmpty(tt, verifyResp) - assert.False(tt, verifyResp.Verified) - assert.Contains(tt, verifyResp.Reason, "could not parse credential from JWT") - }) - - t.Run("Test Create Revocable Credential", func(tt *testing.T) { - bolt := setupTestDB(tt) - require.NotEmpty(tt, bolt) - - keyStoreService := testKeyStoreService(tt, bolt) - didService := testDIDService(tt, bolt, keyStoreService) - schemaService := testSchemaService(tt, bolt, keyStoreService, didService) - credRouter := testCredentialRouter(tt, bolt, keyStoreService, didService, schemaService) - - issuerDID, err := didService.CreateDIDByMethod(context.Background(), did.CreateDIDRequest{ - Method: didsdk.KeyMethod, - KeyType: crypto.Ed25519, - }) - assert.NoError(tt, err) - assert.NotEmpty(tt, issuerDID) + } + createdSchema, err := schemaService.CreateSchema(context.Background(), schema.CreateSchemaRequest{Issuer: "me", Name: "simple schema", Schema: simpleSchema}) + assert.NoError(tt, err) + assert.NotEmpty(tt, createdSchema) - issuerDIDTwo, err := didService.CreateDIDByMethod(context.Background(), did.CreateDIDRequest{ - Method: didsdk.KeyMethod, - KeyType: crypto.Ed25519, - }) - assert.NoError(tt, err) - assert.NotEmpty(tt, issuerDIDTwo) - - w := httptest.NewRecorder() - - // good request One - createCredRequest := router.CreateCredentialRequest{ - Issuer: issuerDID.DID.ID, - IssuerKID: issuerDID.DID.VerificationMethod[0].ID, - Subject: "did:abc:456", - Data: map[string]any{ - "firstName": "Jack", - "lastName": "Dorsey", - }, - Expiry: time.Now().Add(24 * time.Hour).Format(time.RFC3339), - } - requestValue := newRequestValue(tt, createCredRequest) - req := httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/credentials", requestValue) - c := newRequestContext(w, req) - credRouter.CreateCredential(c) - assert.True(tt, util.Is2xxResponse(w.Code)) - - var resp router.CreateCredentialResponse - err = json.NewDecoder(w.Body).Decode(&resp) - assert.NoError(tt, err) - - assert.NotEmpty(tt, resp.CredentialJWT) - assert.NoError(tt, err) - assert.Empty(tt, resp.Credential.CredentialStatus) - assert.Equal(tt, resp.Credential.Issuer, issuerDID.DID.ID) - - // good revocable request One - createRevocableCredRequestOne := router.CreateCredentialRequest{ - Issuer: issuerDID.DID.ID, - IssuerKID: issuerDID.DID.VerificationMethod[0].ID, - Subject: "did:abc:456", - Data: map[string]any{ - "firstName": "Jack", - "lastName": "Dorsey", - }, - Expiry: time.Now().Add(24 * time.Hour).Format(time.RFC3339), - Revocable: true, - } - - requestValue = newRequestValue(tt, createRevocableCredRequestOne) - req = httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/credentials", requestValue) - c = newRequestContext(w, req) - credRouter.CreateCredential(c) - assert.True(tt, util.Is2xxResponse(w.Code)) - - var revocableRespOne router.CreateCredentialResponse - err = json.NewDecoder(w.Body).Decode(&revocableRespOne) - assert.NoError(tt, err) - - assert.NotEmpty(tt, revocableRespOne.CredentialJWT) - assert.NotEmpty(tt, revocableRespOne.Credential.CredentialStatus) - assert.Equal(tt, revocableRespOne.Credential.Issuer, issuerDID.DID.ID) - - credStatusMap, ok := revocableRespOne.Credential.CredentialStatus.(map[string]any) - assert.True(tt, ok) - - assert.NotEmpty(tt, credStatusMap["statusListIndex"]) - - // good revocable request Two - createRevocableCredRequestTwo := router.CreateCredentialRequest{ - Issuer: issuerDID.DID.ID, - IssuerKID: issuerDID.DID.VerificationMethod[0].ID, - Subject: "did:abc:456", - Data: map[string]any{ - "firstName": "Jack", - "lastName": "Dorsey", - }, - Expiry: time.Now().Add(24 * time.Hour).Format(time.RFC3339), - Revocable: true, - } - - requestValue = newRequestValue(tt, createRevocableCredRequestTwo) - req = httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/credentials", requestValue) - c = newRequestContext(w, req) - credRouter.CreateCredential(c) - assert.True(tt, util.Is2xxResponse(w.Code)) - - var revocableRespTwo router.CreateCredentialResponse - err = json.NewDecoder(w.Body).Decode(&revocableRespTwo) - assert.NoError(tt, err) - - assert.NotEmpty(tt, revocableRespTwo.CredentialJWT) - assert.NotEmpty(tt, revocableRespTwo.Credential.CredentialStatus) - assert.Equal(tt, revocableRespTwo.Credential.Issuer, issuerDID.DID.ID) - - credStatusMap, ok = revocableRespTwo.Credential.CredentialStatus.(map[string]any) - assert.True(tt, ok) - - assert.NotEmpty(tt, credStatusMap["statusListIndex"]) - - // good revocable request Three - createRevocableCredRequestThree := router.CreateCredentialRequest{ - Issuer: issuerDID.DID.ID, - IssuerKID: issuerDID.DID.VerificationMethod[0].ID, - Subject: "did:abc:456", - Data: map[string]any{ - "firstName": "Jack", - "lastName": "Dorsey", - }, - Expiry: time.Now().Add(24 * time.Hour).Format(time.RFC3339), - Revocable: true, - } - - requestValue = newRequestValue(tt, createRevocableCredRequestThree) - req = httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/credentials", requestValue) - c = newRequestContext(w, req) - credRouter.CreateCredential(c) - assert.True(tt, util.Is2xxResponse(w.Code)) - - var revocableRespThree router.CreateCredentialResponse - err = json.NewDecoder(w.Body).Decode(&revocableRespThree) - assert.NoError(tt, err) - - assert.NotEmpty(tt, revocableRespThree.CredentialJWT) - assert.NotEmpty(tt, revocableRespThree.Credential.CredentialStatus) - assert.Equal(tt, revocableRespThree.Credential.Issuer, issuerDID.DID.ID) - - credStatusMap, ok = revocableRespThree.Credential.CredentialStatus.(map[string]any) - assert.True(tt, ok) - - assert.NotEmpty(tt, credStatusMap["statusListIndex"]) - - // good revocable request Four (different issuer / schema) - createRevocableCredRequestFour := router.CreateCredentialRequest{ - Issuer: issuerDIDTwo.DID.ID, - IssuerKID: issuerDIDTwo.DID.VerificationMethod[0].ID, - Subject: "did:abc:456", - Data: map[string]any{ - "firstName": "Jack", - "lastName": "Dorsey", - }, - Expiry: time.Now().Add(24 * time.Hour).Format(time.RFC3339), - Revocable: true, - } - - requestValue = newRequestValue(tt, createRevocableCredRequestFour) - req = httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/credentials", requestValue) - c = newRequestContext(w, req) - credRouter.CreateCredential(c) - assert.True(tt, util.Is2xxResponse(w.Code)) - - var revocableRespFour router.CreateCredentialResponse - err = json.NewDecoder(w.Body).Decode(&revocableRespFour) - assert.NoError(tt, err) - - assert.NotEmpty(tt, revocableRespFour.CredentialJWT) - assert.NotEmpty(tt, revocableRespFour.Credential.CredentialStatus) - assert.Equal(tt, revocableRespFour.Credential.Issuer, issuerDIDTwo.DID.ID) - - credStatusMap, ok = revocableRespFour.Credential.CredentialStatus.(map[string]any) - assert.True(tt, ok) - - assert.NotEmpty(tt, credStatusMap["statusListIndex"]) - }) - - t.Run("Test Get Revoked Status Of Credential", func(tt *testing.T) { - bolt := setupTestDB(tt) - require.NotEmpty(tt, bolt) - - keyStoreService := testKeyStoreService(tt, bolt) - didService := testDIDService(tt, bolt, keyStoreService) - schemaService := testSchemaService(tt, bolt, keyStoreService, didService) - credRouter := testCredentialRouter(tt, bolt, keyStoreService, didService, schemaService) - - issuerDID, err := didService.CreateDIDByMethod(context.Background(), did.CreateDIDRequest{ - Method: didsdk.KeyMethod, - KeyType: crypto.Ed25519, - }) - assert.NoError(tt, err) - assert.NotEmpty(tt, issuerDID) - - w := httptest.NewRecorder() - - // good request number one - createCredRequest := router.CreateCredentialRequest{ - Issuer: issuerDID.DID.ID, - IssuerKID: issuerDID.DID.VerificationMethod[0].ID, - Subject: "did:abc:456", - Data: map[string]any{ - "firstName": "Jack", - "lastName": "Dorsey", - }, - Expiry: time.Now().Add(24 * time.Hour).Format(time.RFC3339), - Revocable: true, - } - - requestValue := newRequestValue(tt, createCredRequest) - req := httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/credentials", requestValue) - c := newRequestContext(w, req) - credRouter.CreateCredential(c) - assert.True(tt, util.Is2xxResponse(w.Code)) - - var resp router.CreateCredentialResponse - err = json.NewDecoder(w.Body).Decode(&resp) - assert.NoError(tt, err) - - assert.NotEmpty(tt, resp.CredentialJWT) - assert.NotEmpty(tt, resp.Credential.CredentialStatus) - assert.Equal(tt, resp.Credential.Issuer, issuerDID.DID.ID) - - credStatusMap, ok := resp.Credential.CredentialStatus.(map[string]any) - assert.True(tt, ok) - - assert.NotEmpty(tt, credStatusMap["statusListIndex"]) - - req = httptest.NewRequest(http.MethodGet, fmt.Sprintf("%s/status", resp.Credential.ID), nil) - c = newRequestContextWithParams(w, req, map[string]string{"id": idFromURI(resp.Credential.ID)}) - credRouter.GetCredentialStatus(c) - assert.True(tt, util.Is2xxResponse(w.Code)) - - var credStatusResponse = router.GetCredentialStatusResponse{} - err = json.NewDecoder(w.Body).Decode(&credStatusResponse) - assert.NoError(tt, err) - assert.Equal(tt, false, credStatusResponse.Revoked) - - // good request number one - updateCredStatusRequest := router.UpdateCredentialStatusRequest{Revoked: true} - - requestValue = newRequestValue(tt, updateCredStatusRequest) - req = httptest.NewRequest(http.MethodPut, fmt.Sprintf("%s/status", resp.Credential.ID), requestValue) - c = newRequestContextWithParams(w, req, map[string]string{"id": idFromURI(resp.Credential.ID)}) - credRouter.UpdateCredentialStatus(c) - assert.True(tt, util.Is2xxResponse(w.Code)) - - var credStatusUpdateResponse = router.UpdateCredentialStatusResponse{} - err = json.NewDecoder(w.Body).Decode(&credStatusUpdateResponse) - assert.NoError(tt, err) - assert.Equal(tt, true, credStatusUpdateResponse.Revoked) - - }) - - t.Run("Test Get Status List Credential", func(tt *testing.T) { - bolt := setupTestDB(tt) - require.NotEmpty(tt, bolt) - - keyStoreService := testKeyStoreService(tt, bolt) - didService := testDIDService(tt, bolt, keyStoreService) - schemaService := testSchemaService(tt, bolt, keyStoreService, didService) - credRouter := testCredentialRouter(tt, bolt, keyStoreService, didService, schemaService) - - issuerDID, err := didService.CreateDIDByMethod(context.Background(), did.CreateDIDRequest{ - Method: didsdk.KeyMethod, - KeyType: crypto.Ed25519, + createCredRequest := router.CreateCredentialRequest{ + Issuer: issuerDID.DID.ID, + IssuerKID: issuerDID.DID.VerificationMethod[0].ID, + Subject: "did:abc:456", + SchemaID: createdSchema.ID, + Data: map[string]any{ + "firstName": "Jack", + "lastName": "Dorsey", + }, + Expiry: time.Now().Add(24 * time.Hour).Format(time.RFC3339), + } + requestValue := newRequestValue(tt, createCredRequest) + req := httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/credentials", requestValue) + c := newRequestContext(w, req) + credRouter.CreateCredential(c) + assert.True(tt, util.Is2xxResponse(w.Code)) + + var resp router.CreateCredentialResponse + err = json.NewDecoder(w.Body).Decode(&resp) + assert.NoError(tt, err) + assert.NotEmpty(tt, resp.CredentialJWT) + + // reset the http recorder + w = httptest.NewRecorder() + + // get credential by schema + req = httptest.NewRequest(http.MethodGet, fmt.Sprintf("https://ssi-service.com/v1/credential?schema=%s", createdSchema.ID), nil) + c = newRequestContext(w, req) + credRouter.ListCredentials(c) + assert.True(tt, util.Is2xxResponse(w.Code)) + + var getCredsResp router.ListCredentialsResponse + err = json.NewDecoder(w.Body).Decode(&getCredsResp) + assert.NoError(tt, err) + assert.NotEmpty(tt, getCredsResp) + assert.Len(tt, getCredsResp.Credentials, 1) + + assert.Equal(tt, resp.Credential.ID, getCredsResp.Credentials[0].ID) + assert.Equal(tt, resp.Credential.CredentialSchema.ID, getCredsResp.Credentials[0].Credential.CredentialSchema.ID) + }) + + t.Run("Get Credential No Param", func(tt *testing.T) { + db := test.ServiceStorage(t) + require.NotEmpty(tt, db) + + keyStoreService := testKeyStoreService(tt, db) + didService := testDIDService(tt, db, keyStoreService) + schemaService := testSchemaService(tt, db, keyStoreService, didService) + credRouter := testCredentialRouter(tt, db, keyStoreService, didService, schemaService) + + w := httptest.NewRecorder() + + issuerDID, err := didService.CreateDIDByMethod(context.Background(), did.CreateDIDRequest{ + Method: didsdk.KeyMethod, + KeyType: crypto.Ed25519, + }) + assert.NoError(tt, err) + assert.NotEmpty(tt, issuerDID) + + createCredRequest := router.CreateCredentialRequest{ + Issuer: issuerDID.DID.ID, + IssuerKID: issuerDID.DID.VerificationMethod[0].ID, + Subject: "did:abc:456", + Data: map[string]any{ + "firstName": "Jack", + "lastName": "Dorsey", + }, + Expiry: time.Now().Add(24 * time.Hour).Format(time.RFC3339), + } + requestValue := newRequestValue(tt, createCredRequest) + req := httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/credentials", requestValue) + c := newRequestContext(w, req) + credRouter.CreateCredential(c) + assert.True(tt, util.Is2xxResponse(w.Code)) + + var resp router.CreateCredentialResponse + err = json.NewDecoder(w.Body).Decode(&resp) + assert.NoError(tt, err) + assert.NotEmpty(tt, resp.CredentialJWT) + + // reset the http recorder + w = httptest.NewRecorder() + + // get credential by issuer id + req = httptest.NewRequest(http.MethodGet, "https://ssi-service.com/v1/credentials", nil) + c = newRequestContext(w, req) + credRouter.ListCredentials(c) + assert.True(tt, util.Is2xxResponse(w.Code)) + + var getCredsResp router.ListCredentialsResponse + err = json.NewDecoder(w.Body).Decode(&getCredsResp) + assert.NoError(tt, err) + assert.NotEmpty(tt, getCredsResp) + + assert.Len(tt, getCredsResp.Credentials, 1) + assert.Equal(tt, resp.Credential.ID, getCredsResp.Credentials[0].ID) + }) + + t.Run("Test Get Credential By Issuer", func(tt *testing.T) { + db := test.ServiceStorage(t) + require.NotEmpty(tt, db) + + keyStoreService := testKeyStoreService(tt, db) + didService := testDIDService(tt, db, keyStoreService) + schemaService := testSchemaService(tt, db, keyStoreService, didService) + credRouter := testCredentialRouter(tt, db, keyStoreService, didService, schemaService) + + w := httptest.NewRecorder() + + issuerDID, err := didService.CreateDIDByMethod(context.Background(), did.CreateDIDRequest{ + Method: didsdk.KeyMethod, + KeyType: crypto.Ed25519, + }) + assert.NoError(tt, err) + assert.NotEmpty(tt, issuerDID) + + createCredRequest := router.CreateCredentialRequest{ + Issuer: issuerDID.DID.ID, + IssuerKID: issuerDID.DID.VerificationMethod[0].ID, + Subject: "did:abc:456", + Data: map[string]any{ + "firstName": "Jack", + "lastName": "Dorsey", + }, + Expiry: time.Now().Add(24 * time.Hour).Format(time.RFC3339), + } + requestValue := newRequestValue(tt, createCredRequest) + req := httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/credentials", requestValue) + c := newRequestContext(w, req) + credRouter.CreateCredential(c) + assert.True(tt, util.Is2xxResponse(w.Code)) + + var resp router.CreateCredentialResponse + err = json.NewDecoder(w.Body).Decode(&resp) + assert.NoError(tt, err) + assert.NotEmpty(tt, resp.CredentialJWT) + + // reset the http recorder + w = httptest.NewRecorder() + + // get credential by issuer id + req = httptest.NewRequest(http.MethodGet, fmt.Sprintf("https://ssi-service.com/v1/credential?issuer=%s", issuerDID.DID.ID), nil) + c = newRequestContext(w, req) + credRouter.ListCredentials(c) + assert.True(tt, util.Is2xxResponse(w.Code)) + + var getCredsResp router.ListCredentialsResponse + err = json.NewDecoder(w.Body).Decode(&getCredsResp) + assert.NoError(tt, err) + assert.NotEmpty(tt, getCredsResp) + + assert.Len(tt, getCredsResp.Credentials, 1) + assert.Equal(tt, resp.Credential.ID, getCredsResp.Credentials[0].ID) + }) + + t.Run("Test Get Credential By Subject", func(tt *testing.T) { + db := test.ServiceStorage(t) + require.NotEmpty(tt, db) + + keyStoreService := testKeyStoreService(tt, db) + didService := testDIDService(tt, db, keyStoreService) + schemaService := testSchemaService(tt, db, keyStoreService, didService) + credRouter := testCredentialRouter(tt, db, keyStoreService, didService, schemaService) + + w := httptest.NewRecorder() + + issuerDID, err := didService.CreateDIDByMethod(context.Background(), did.CreateDIDRequest{ + Method: didsdk.KeyMethod, + KeyType: crypto.Ed25519, + }) + assert.NoError(tt, err) + assert.NotEmpty(tt, issuerDID) + + subjectID := "did:abc:456" + createCredRequest := router.CreateCredentialRequest{ + Issuer: issuerDID.DID.ID, + IssuerKID: issuerDID.DID.VerificationMethod[0].ID, + Subject: subjectID, + Data: map[string]any{ + "firstName": "Jack", + "lastName": "Dorsey", + }, + Expiry: time.Now().Add(24 * time.Hour).Format(time.RFC3339), + } + requestValue := newRequestValue(tt, createCredRequest) + req := httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/credentials", requestValue) + c := newRequestContext(w, req) + credRouter.CreateCredential(c) + assert.True(tt, util.Is2xxResponse(w.Code)) + + var createCredentialResponse router.CreateCredentialResponse + err = json.NewDecoder(w.Body).Decode(&createCredentialResponse) + assert.NoError(tt, err) + assert.NotEmpty(tt, createCredentialResponse.CredentialJWT) + + // reset the http recorder + w = httptest.NewRecorder() + + // get credential by subject id + req = httptest.NewRequest(http.MethodGet, fmt.Sprintf("https://ssi-service.com/v1/credential?subject=%s", subjectID), nil) + c = newRequestContext(w, req) + credRouter.ListCredentials(c) + assert.True(tt, util.Is2xxResponse(w.Code)) + + var listCredentialsResponse router.ListCredentialsResponse + err = json.NewDecoder(w.Body).Decode(&listCredentialsResponse) + assert.NoError(tt, err) + assert.NotEmpty(tt, listCredentialsResponse) + + assert.Len(tt, listCredentialsResponse.Credentials, 1) + assert.Equal(tt, createCredentialResponse.Credential.ID, listCredentialsResponse.Credentials[0].ID) + assert.Equal(tt, createCredentialResponse.Credential.CredentialSubject[credsdk.VerifiableCredentialIDProperty], listCredentialsResponse.Credentials[0].Credential.CredentialSubject[credsdk.VerifiableCredentialIDProperty]) + }) + + t.Run("Test Delete Credential", func(tt *testing.T) { + db := test.ServiceStorage(t) + require.NotEmpty(tt, db) + + keyStoreService := testKeyStoreService(tt, db) + didService := testDIDService(tt, db, keyStoreService) + schemaService := testSchemaService(tt, db, keyStoreService, didService) + credRouter := testCredentialRouter(tt, db, keyStoreService, didService, schemaService) + + issuerDID, err := didService.CreateDIDByMethod(context.Background(), did.CreateDIDRequest{ + Method: didsdk.KeyMethod, + KeyType: crypto.Ed25519, + }) + assert.NoError(tt, err) + assert.NotEmpty(tt, issuerDID) + + createCredRequest := router.CreateCredentialRequest{ + Issuer: issuerDID.DID.ID, + IssuerKID: issuerDID.DID.VerificationMethod[0].ID, + Subject: "did:abc:456", + Data: map[string]any{ + "firstName": "Jack", + "lastName": "Dorsey", + }, + Expiry: time.Now().Add(24 * time.Hour).Format(time.RFC3339), + } + requestValue := newRequestValue(tt, createCredRequest) + req := httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/credentials", requestValue) + w := httptest.NewRecorder() + c := newRequestContext(w, req) + credRouter.CreateCredential(c) + assert.True(tt, util.Is2xxResponse(w.Code)) + + var resp router.CreateCredentialResponse + err = json.NewDecoder(w.Body).Decode(&resp) + assert.NoError(tt, err) + + // reset the http recorder + w = httptest.NewRecorder() + + // get credential by id + credID := resp.Credential.ID + req = httptest.NewRequest(http.MethodGet, credID, nil) + c = newRequestContextWithParams(w, req, map[string]string{"id": idFromURI(credID)}) + credRouter.GetCredential(c) + assert.True(tt, util.Is2xxResponse(w.Code)) + + var getCredResp router.GetCredentialResponse + err = json.NewDecoder(w.Body).Decode(&getCredResp) + assert.NoError(tt, err) + assert.NotEmpty(tt, getCredResp) + assert.Equal(tt, credID, getCredResp.Credential.ID) + + // reset the http recorder + w = httptest.NewRecorder() + + // delete it + req = httptest.NewRequest(http.MethodDelete, fmt.Sprintf("https://ssi-service.com/v1/credentials/%s", credID), nil) + c = newRequestContextWithParams(w, req, map[string]string{"id": credID}) + credRouter.DeleteCredential(c) + assert.True(tt, util.Is2xxResponse(w.Code)) + + // reset the http recorder + w = httptest.NewRecorder() + + // get it back + req = httptest.NewRequest(http.MethodGet, fmt.Sprintf("https://ssi-service.com/v1/credentials/%s", credID), nil) + c = newRequestContextWithParams(w, req, map[string]string{"id": credID}) + credRouter.GetCredential(c) + assert.Contains(tt, w.Body.String(), fmt.Sprintf("could not get credential with id: %s", credID)) + }) + + t.Run("Test Verifying a Credential", func(tt *testing.T) { + db := test.ServiceStorage(t) + require.NotEmpty(tt, db) + + keyStoreService := testKeyStoreService(tt, db) + didService := testDIDService(tt, db, keyStoreService) + schemaService := testSchemaService(tt, db, keyStoreService, didService) + credRouter := testCredentialRouter(tt, db, keyStoreService, didService, schemaService) + + issuerDID, err := didService.CreateDIDByMethod(context.Background(), did.CreateDIDRequest{ + Method: didsdk.KeyMethod, + KeyType: crypto.Ed25519, + }) + assert.NoError(tt, err) + assert.NotEmpty(tt, issuerDID) + + // good request + createCredRequest := router.CreateCredentialRequest{ + Issuer: issuerDID.DID.ID, + IssuerKID: issuerDID.DID.VerificationMethod[0].ID, + Subject: "did:abc:456", + Data: map[string]any{ + "firstName": "Jack", + "lastName": "Dorsey", + }, + Expiry: time.Now().Add(24 * time.Hour).Format(time.RFC3339), + } + requestValue := newRequestValue(tt, createCredRequest) + req := httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/credentials", requestValue) + w := httptest.NewRecorder() + c := newRequestContext(w, req) + credRouter.CreateCredential(c) + assert.True(tt, util.Is2xxResponse(w.Code)) + + var resp router.CreateCredentialResponse + err = json.NewDecoder(w.Body).Decode(&resp) + assert.NoError(tt, err) + + assert.NotEmpty(tt, resp.CredentialJWT) + assert.NoError(tt, err) + assert.Equal(tt, resp.Credential.Issuer, issuerDID.DID.ID) + + // reset the http recorder + w = httptest.NewRecorder() + + // verify the credential + requestValue = newRequestValue(tt, router.VerifyCredentialRequest{CredentialJWT: resp.CredentialJWT}) + req = httptest.NewRequest(http.MethodPost, "https://ssi-service.com/v1/credentials/verification", requestValue) + c = newRequestContext(w, req) + credRouter.VerifyCredential(c) + assert.True(tt, util.Is2xxResponse(w.Code)) + + var verifyResp router.VerifyCredentialResponse + err = json.NewDecoder(w.Body).Decode(&verifyResp) + assert.NoError(tt, err) + assert.NotEmpty(tt, verifyResp) + assert.True(tt, verifyResp.Verified) + + // bad credential + requestValue = newRequestValue(tt, router.VerifyCredentialRequest{CredentialJWT: keyaccess.JWTPtr("bad")}) + req = httptest.NewRequest(http.MethodPost, "https://ssi-service.com/v1/credentials/verification", requestValue) + c = newRequestContext(w, req) + credRouter.VerifyCredential(c) + assert.True(tt, util.Is2xxResponse(w.Code)) + + err = json.NewDecoder(w.Body).Decode(&verifyResp) + assert.NoError(tt, err) + assert.NotEmpty(tt, verifyResp) + assert.False(tt, verifyResp.Verified) + assert.Contains(tt, verifyResp.Reason, "could not parse credential from JWT") + }) + + t.Run("Test Create Revocable Credential", func(tt *testing.T) { + db := test.ServiceStorage(t) + require.NotEmpty(tt, db) + + keyStoreService := testKeyStoreService(tt, db) + didService := testDIDService(tt, db, keyStoreService) + schemaService := testSchemaService(tt, db, keyStoreService, didService) + credRouter := testCredentialRouter(tt, db, keyStoreService, didService, schemaService) + + issuerDID, err := didService.CreateDIDByMethod(context.Background(), did.CreateDIDRequest{ + Method: didsdk.KeyMethod, + KeyType: crypto.Ed25519, + }) + assert.NoError(tt, err) + assert.NotEmpty(tt, issuerDID) + + issuerDIDTwo, err := didService.CreateDIDByMethod(context.Background(), did.CreateDIDRequest{ + Method: didsdk.KeyMethod, + KeyType: crypto.Ed25519, + }) + assert.NoError(tt, err) + assert.NotEmpty(tt, issuerDIDTwo) + + w := httptest.NewRecorder() + + // good request One + createCredRequest := router.CreateCredentialRequest{ + Issuer: issuerDID.DID.ID, + IssuerKID: issuerDID.DID.VerificationMethod[0].ID, + Subject: "did:abc:456", + Data: map[string]any{ + "firstName": "Jack", + "lastName": "Dorsey", + }, + Expiry: time.Now().Add(24 * time.Hour).Format(time.RFC3339), + } + requestValue := newRequestValue(tt, createCredRequest) + req := httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/credentials", requestValue) + c := newRequestContext(w, req) + credRouter.CreateCredential(c) + assert.True(tt, util.Is2xxResponse(w.Code)) + + var resp router.CreateCredentialResponse + err = json.NewDecoder(w.Body).Decode(&resp) + assert.NoError(tt, err) + + assert.NotEmpty(tt, resp.CredentialJWT) + assert.NoError(tt, err) + assert.Empty(tt, resp.Credential.CredentialStatus) + assert.Equal(tt, resp.Credential.Issuer, issuerDID.DID.ID) + + // good revocable request One + createRevocableCredRequestOne := router.CreateCredentialRequest{ + Issuer: issuerDID.DID.ID, + IssuerKID: issuerDID.DID.VerificationMethod[0].ID, + Subject: "did:abc:456", + Data: map[string]any{ + "firstName": "Jack", + "lastName": "Dorsey", + }, + Expiry: time.Now().Add(24 * time.Hour).Format(time.RFC3339), + Revocable: true, + } + + requestValue = newRequestValue(tt, createRevocableCredRequestOne) + req = httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/credentials", requestValue) + c = newRequestContext(w, req) + credRouter.CreateCredential(c) + assert.True(tt, util.Is2xxResponse(w.Code)) + + var revocableRespOne router.CreateCredentialResponse + err = json.NewDecoder(w.Body).Decode(&revocableRespOne) + assert.NoError(tt, err) + + assert.NotEmpty(tt, revocableRespOne.CredentialJWT) + assert.NotEmpty(tt, revocableRespOne.Credential.CredentialStatus) + assert.Equal(tt, revocableRespOne.Credential.Issuer, issuerDID.DID.ID) + + credStatusMap, ok := revocableRespOne.Credential.CredentialStatus.(map[string]any) + assert.True(tt, ok) + + assert.NotEmpty(tt, credStatusMap["statusListIndex"]) + + // good revocable request Two + createRevocableCredRequestTwo := router.CreateCredentialRequest{ + Issuer: issuerDID.DID.ID, + IssuerKID: issuerDID.DID.VerificationMethod[0].ID, + Subject: "did:abc:456", + Data: map[string]any{ + "firstName": "Jack", + "lastName": "Dorsey", + }, + Expiry: time.Now().Add(24 * time.Hour).Format(time.RFC3339), + Revocable: true, + } + + requestValue = newRequestValue(tt, createRevocableCredRequestTwo) + req = httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/credentials", requestValue) + c = newRequestContext(w, req) + credRouter.CreateCredential(c) + assert.True(tt, util.Is2xxResponse(w.Code)) + + var revocableRespTwo router.CreateCredentialResponse + err = json.NewDecoder(w.Body).Decode(&revocableRespTwo) + assert.NoError(tt, err) + + assert.NotEmpty(tt, revocableRespTwo.CredentialJWT) + assert.NotEmpty(tt, revocableRespTwo.Credential.CredentialStatus) + assert.Equal(tt, revocableRespTwo.Credential.Issuer, issuerDID.DID.ID) + + credStatusMap, ok = revocableRespTwo.Credential.CredentialStatus.(map[string]any) + assert.True(tt, ok) + + assert.NotEmpty(tt, credStatusMap["statusListIndex"]) + + // good revocable request Three + createRevocableCredRequestThree := router.CreateCredentialRequest{ + Issuer: issuerDID.DID.ID, + IssuerKID: issuerDID.DID.VerificationMethod[0].ID, + Subject: "did:abc:456", + Data: map[string]any{ + "firstName": "Jack", + "lastName": "Dorsey", + }, + Expiry: time.Now().Add(24 * time.Hour).Format(time.RFC3339), + Revocable: true, + } + + requestValue = newRequestValue(tt, createRevocableCredRequestThree) + req = httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/credentials", requestValue) + c = newRequestContext(w, req) + credRouter.CreateCredential(c) + assert.True(tt, util.Is2xxResponse(w.Code)) + + var revocableRespThree router.CreateCredentialResponse + err = json.NewDecoder(w.Body).Decode(&revocableRespThree) + assert.NoError(tt, err) + + assert.NotEmpty(tt, revocableRespThree.CredentialJWT) + assert.NotEmpty(tt, revocableRespThree.Credential.CredentialStatus) + assert.Equal(tt, revocableRespThree.Credential.Issuer, issuerDID.DID.ID) + + credStatusMap, ok = revocableRespThree.Credential.CredentialStatus.(map[string]any) + assert.True(tt, ok) + + assert.NotEmpty(tt, credStatusMap["statusListIndex"]) + + // good revocable request Four (different issuer / schema) + createRevocableCredRequestFour := router.CreateCredentialRequest{ + Issuer: issuerDIDTwo.DID.ID, + IssuerKID: issuerDIDTwo.DID.VerificationMethod[0].ID, + Subject: "did:abc:456", + Data: map[string]any{ + "firstName": "Jack", + "lastName": "Dorsey", + }, + Expiry: time.Now().Add(24 * time.Hour).Format(time.RFC3339), + Revocable: true, + } + + requestValue = newRequestValue(tt, createRevocableCredRequestFour) + req = httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/credentials", requestValue) + c = newRequestContext(w, req) + credRouter.CreateCredential(c) + assert.True(tt, util.Is2xxResponse(w.Code)) + + var revocableRespFour router.CreateCredentialResponse + err = json.NewDecoder(w.Body).Decode(&revocableRespFour) + assert.NoError(tt, err) + + assert.NotEmpty(tt, revocableRespFour.CredentialJWT) + assert.NotEmpty(tt, revocableRespFour.Credential.CredentialStatus) + assert.Equal(tt, revocableRespFour.Credential.Issuer, issuerDIDTwo.DID.ID) + + credStatusMap, ok = revocableRespFour.Credential.CredentialStatus.(map[string]any) + assert.True(tt, ok) + + assert.NotEmpty(tt, credStatusMap["statusListIndex"]) + }) + + t.Run("Test Get Revoked Status Of Credential", func(tt *testing.T) { + db := test.ServiceStorage(t) + require.NotEmpty(tt, db) + + keyStoreService := testKeyStoreService(tt, db) + didService := testDIDService(tt, db, keyStoreService) + schemaService := testSchemaService(tt, db, keyStoreService, didService) + credRouter := testCredentialRouter(tt, db, keyStoreService, didService, schemaService) + + issuerDID, err := didService.CreateDIDByMethod(context.Background(), did.CreateDIDRequest{ + Method: didsdk.KeyMethod, + KeyType: crypto.Ed25519, + }) + assert.NoError(tt, err) + assert.NotEmpty(tt, issuerDID) + + w := httptest.NewRecorder() + + // good request number one + createCredRequest := router.CreateCredentialRequest{ + Issuer: issuerDID.DID.ID, + IssuerKID: issuerDID.DID.VerificationMethod[0].ID, + Subject: "did:abc:456", + Data: map[string]any{ + "firstName": "Jack", + "lastName": "Dorsey", + }, + Expiry: time.Now().Add(24 * time.Hour).Format(time.RFC3339), + Revocable: true, + } + + requestValue := newRequestValue(tt, createCredRequest) + req := httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/credentials", requestValue) + c := newRequestContext(w, req) + credRouter.CreateCredential(c) + assert.True(tt, util.Is2xxResponse(w.Code)) + + var resp router.CreateCredentialResponse + err = json.NewDecoder(w.Body).Decode(&resp) + assert.NoError(tt, err) + + assert.NotEmpty(tt, resp.CredentialJWT) + assert.NotEmpty(tt, resp.Credential.CredentialStatus) + assert.Equal(tt, resp.Credential.Issuer, issuerDID.DID.ID) + + credStatusMap, ok := resp.Credential.CredentialStatus.(map[string]any) + assert.True(tt, ok) + + assert.NotEmpty(tt, credStatusMap["statusListIndex"]) + + req = httptest.NewRequest(http.MethodGet, fmt.Sprintf("%s/status", resp.Credential.ID), nil) + c = newRequestContextWithParams(w, req, map[string]string{"id": idFromURI(resp.Credential.ID)}) + credRouter.GetCredentialStatus(c) + assert.True(tt, util.Is2xxResponse(w.Code)) + + var credStatusResponse = router.GetCredentialStatusResponse{} + err = json.NewDecoder(w.Body).Decode(&credStatusResponse) + assert.NoError(tt, err) + assert.Equal(tt, false, credStatusResponse.Revoked) + + // good request number one + updateCredStatusRequest := router.UpdateCredentialStatusRequest{Revoked: true} + + requestValue = newRequestValue(tt, updateCredStatusRequest) + req = httptest.NewRequest(http.MethodPut, fmt.Sprintf("%s/status", resp.Credential.ID), requestValue) + c = newRequestContextWithParams(w, req, map[string]string{"id": idFromURI(resp.Credential.ID)}) + credRouter.UpdateCredentialStatus(c) + assert.True(tt, util.Is2xxResponse(w.Code)) + + var credStatusUpdateResponse = router.UpdateCredentialStatusResponse{} + err = json.NewDecoder(w.Body).Decode(&credStatusUpdateResponse) + assert.NoError(tt, err) + assert.Equal(tt, true, credStatusUpdateResponse.Revoked) + + }) + + t.Run("Test Get Status List Credential", func(tt *testing.T) { + db := test.ServiceStorage(t) + require.NotEmpty(tt, db) + + keyStoreService := testKeyStoreService(tt, db) + didService := testDIDService(tt, db, keyStoreService) + schemaService := testSchemaService(tt, db, keyStoreService, didService) + credRouter := testCredentialRouter(tt, db, keyStoreService, didService, schemaService) + + issuerDID, err := didService.CreateDIDByMethod(context.Background(), did.CreateDIDRequest{ + Method: didsdk.KeyMethod, + KeyType: crypto.Ed25519, + }) + assert.NoError(tt, err) + assert.NotEmpty(tt, issuerDID) + + w := httptest.NewRecorder() + + // good request number one + createCredRequest := router.CreateCredentialRequest{ + Issuer: issuerDID.DID.ID, + IssuerKID: issuerDID.DID.VerificationMethod[0].ID, + Subject: "did:abc:456", + Data: map[string]any{ + "firstName": "Jack", + "lastName": "Dorsey", + }, + Expiry: time.Now().Add(24 * time.Hour).Format(time.RFC3339), + Revocable: true, + } + + requestValue := newRequestValue(tt, createCredRequest) + req := httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/credentials", requestValue) + c := newRequestContext(w, req) + credRouter.CreateCredential(c) + assert.True(tt, util.Is2xxResponse(w.Code)) + + var resp router.CreateCredentialResponse + err = json.NewDecoder(w.Body).Decode(&resp) + assert.NoError(tt, err) + + assert.NotEmpty(tt, resp.CredentialJWT) + assert.NotEmpty(tt, resp.Credential.CredentialStatus) + assert.Equal(tt, resp.Credential.Issuer, issuerDID.DID.ID) + + credStatusMap, ok := resp.Credential.CredentialStatus.(map[string]any) + assert.True(tt, ok) + + assert.NotEmpty(tt, credStatusMap["statusListIndex"]) + + credStatusListID := (credStatusMap["statusListCredential"]).(string) + + assert.NotEmpty(tt, credStatusListID) + + i := strings.LastIndex(credStatusListID, "/") + uuidStringUUID := credStatusListID[i+1:] + + req = httptest.NewRequest(http.MethodGet, fmt.Sprintf("https://localhost:8080/%s", credStatusListID), nil) + c = newRequestContextWithParams(w, req, map[string]string{"id": uuidStringUUID}) + credRouter.GetCredentialStatusList(c) + assert.True(tt, util.Is2xxResponse(w.Code)) + + var credListResp router.GetCredentialStatusListResponse + err = json.NewDecoder(w.Body).Decode(&credListResp) + assert.NoError(tt, err) + + assert.NotEmpty(tt, credListResp.CredentialJWT) + assert.Empty(tt, credListResp.Credential.CredentialStatus) + assert.Equal(tt, credListResp.Credential.ID, credStatusListID) + }) }) - assert.NoError(tt, err) - assert.NotEmpty(tt, issuerDID) - - w := httptest.NewRecorder() - - // good request number one - createCredRequest := router.CreateCredentialRequest{ - Issuer: issuerDID.DID.ID, - IssuerKID: issuerDID.DID.VerificationMethod[0].ID, - Subject: "did:abc:456", - Data: map[string]any{ - "firstName": "Jack", - "lastName": "Dorsey", - }, - Expiry: time.Now().Add(24 * time.Hour).Format(time.RFC3339), - Revocable: true, - } - - requestValue := newRequestValue(tt, createCredRequest) - req := httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/credentials", requestValue) - c := newRequestContext(w, req) - credRouter.CreateCredential(c) - assert.True(tt, util.Is2xxResponse(w.Code)) - - var resp router.CreateCredentialResponse - err = json.NewDecoder(w.Body).Decode(&resp) - assert.NoError(tt, err) - - assert.NotEmpty(tt, resp.CredentialJWT) - assert.NotEmpty(tt, resp.Credential.CredentialStatus) - assert.Equal(tt, resp.Credential.Issuer, issuerDID.DID.ID) - - credStatusMap, ok := resp.Credential.CredentialStatus.(map[string]any) - assert.True(tt, ok) - - assert.NotEmpty(tt, credStatusMap["statusListIndex"]) - - credStatusListID := (credStatusMap["statusListCredential"]).(string) - - assert.NotEmpty(tt, credStatusListID) - - i := strings.LastIndex(credStatusListID, "/") - uuidStringUUID := credStatusListID[i+1:] - - req = httptest.NewRequest(http.MethodGet, fmt.Sprintf("https://localhost:8080/%s", credStatusListID), nil) - c = newRequestContextWithParams(w, req, map[string]string{"id": uuidStringUUID}) - credRouter.GetCredentialStatusList(c) - assert.True(tt, util.Is2xxResponse(w.Code)) - - var credListResp router.GetCredentialStatusListResponse - err = json.NewDecoder(w.Body).Decode(&credListResp) - assert.NoError(tt, err) - - assert.NotEmpty(tt, credListResp.CredentialJWT) - assert.Empty(tt, credListResp.Credential.CredentialStatus) - assert.Equal(tt, credListResp.Credential.ID, credStatusListID) - }) + } } diff --git a/pkg/server/server_did_test.go b/pkg/server/server_did_test.go index 34527f259..6e5252256 100644 --- a/pkg/server/server_did_test.go +++ b/pkg/server/server_did_test.go @@ -5,6 +5,7 @@ import ( "net/http" "net/http/httptest" "net/url" + "strings" "testing" "github.com/TBD54566975/ssi-sdk/crypto" @@ -12,6 +13,7 @@ import ( "github.com/goccy/go-json" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/tbd54566975/ssi-service/pkg/testutil" "gopkg.in/h2non/gock.v1" "github.com/tbd54566975/ssi-service/internal/util" @@ -20,643 +22,653 @@ import ( ) func TestDIDAPI(t *testing.T) { - t.Run("Test Get DID Methods", func(tt *testing.T) { - bolt := setupTestDB(tt) - require.NotEmpty(tt, bolt) - - _, keyStoreService := testKeyStore(tt, bolt) - didService := testDIDRouter(tt, bolt, keyStoreService, []string{"key", "web", "ion"}) - - // get DID method - req := httptest.NewRequest(http.MethodGet, "https://ssi-service.com/v1/dids", nil) - w := httptest.NewRecorder() - - c := newRequestContext(w, req) - didService.ListDIDMethods(c) - assert.Equal(tt, http.StatusOK, w.Result().StatusCode) - - var resp router.ListDIDMethodsResponse - err := json.NewDecoder(w.Body).Decode(&resp) - assert.NoError(tt, err) - - assert.Len(tt, resp.DIDMethods, 3) - assert.Contains(tt, resp.DIDMethods, didsdk.KeyMethod) - assert.Contains(tt, resp.DIDMethods, didsdk.WebMethod) - assert.Contains(tt, resp.DIDMethods, didsdk.IONMethod) - }) - - t.Run("Test Create DID By Method: Key", func(tt *testing.T) { - bolt := setupTestDB(tt) - require.NotEmpty(tt, bolt) - - _, keyStoreService := testKeyStore(tt, bolt) - didService := testDIDRouter(tt, bolt, keyStoreService, []string{"key"}) - - // create DID by method - key - missing body - req := httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/dids/key", nil) - w := httptest.NewRecorder() - params := map[string]string{"method": "key"} - c := newRequestContextWithParams(w, req, params) - didService.CreateDIDByMethod(c) - assert.Contains(tt, w.Body.String(), "invalid create DID request") - - // reset recorder between calls - w = httptest.NewRecorder() - - // with body, bad key type - createDIDRequest := router.CreateDIDByMethodRequest{KeyType: "bad"} - requestReader := newRequestValue(tt, createDIDRequest) - req = httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/dids/key", requestReader) - - c = newRequestContextWithParams(w, req, params) - didService.CreateDIDByMethod(c) - assert.Contains(tt, w.Body.String(), "could not create DID for method with key type: bad") - - // reset recorder between calls - w = httptest.NewRecorder() - - // with body, good key type - createDIDRequest = router.CreateDIDByMethodRequest{KeyType: crypto.Ed25519} - requestReader = newRequestValue(tt, createDIDRequest) - req = httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/dids/key", requestReader) - - c = newRequestContextWithParams(w, req, params) - didService.CreateDIDByMethod(c) - assert.True(tt, util.Is2xxResponse(w.Code)) - - var resp router.CreateDIDByMethodResponse - err := json.NewDecoder(w.Body).Decode(&resp) - assert.NoError(tt, err) - assert.Contains(tt, resp.DID.ID, didsdk.KeyMethod) - }) - - t.Run("Test Create DID By Method: Web", func(tt *testing.T) { - bolt := setupTestDB(tt) - require.NotEmpty(tt, bolt) - - _, keyStoreService := testKeyStore(tt, bolt) - didService := testDIDRouter(tt, bolt, keyStoreService, []string{"web"}) - - // create DID by method - web - missing body - req := httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/dids/web", nil) - w := httptest.NewRecorder() - params := map[string]string{ - "method": "web", - } - - c := newRequestContextWithParams(w, req, params) - didService.CreateDIDByMethod(c) - assert.Contains(tt, w.Body.String(), "invalid create DID request") - - // reset recorder between calls - w = httptest.NewRecorder() - - // with body, good key type, missing options - createDIDRequest := router.CreateDIDByMethodRequest{KeyType: crypto.Ed25519} - requestReader := newRequestValue(tt, createDIDRequest) - req = httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/dids/web", requestReader) - - c = newRequestContextWithParams(w, req, params) - didService.CreateDIDByMethod(c) - assert.Contains(tt, w.Body.String(), "could not create DID for method with key type: Ed25519: options cannot be empty") - - // reset recorder between calls - w = httptest.NewRecorder() - - // good options - options := did.CreateWebDIDOptions{DIDWebID: "did:web:example.com"} - - // with body, bad key type - createDIDRequest = router.CreateDIDByMethodRequest{KeyType: "bad", Options: options} - requestReader = newRequestValue(tt, createDIDRequest) - req = httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/dids/web", requestReader) - - c = newRequestContextWithParams(w, req, params) - didService.CreateDIDByMethod(c) - assert.Contains(tt, w.Body.String(), "could not create DID for method with key type: bad") - - // reset recorder between calls - w = httptest.NewRecorder() - - // with body, good key type with options - createDIDRequest = router.CreateDIDByMethodRequest{ - KeyType: crypto.Ed25519, - Options: options, - } - - requestReader = newRequestValue(tt, createDIDRequest) - req = httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/dids/web", requestReader) - - gock.New("https://example.com"). - Get("/.well-known/did.json"). - Reply(200). - BodyString(`{"didDocument": {"id": "did:web:example.com"}}`) - defer gock.Off() - - c = newRequestContextWithParams(w, req, params) - didService.CreateDIDByMethod(c) - assert.True(tt, util.Is2xxResponse(w.Code)) - - var resp router.CreateDIDByMethodResponse - err := json.NewDecoder(w.Body).Decode(&resp) - assert.NoError(tt, err) - assert.Contains(tt, resp.DID.ID, didsdk.WebMethod) - }) - - t.Run("Test Create DID By Method: ION", func(tt *testing.T) { - bolt := setupTestDB(tt) - require.NotEmpty(tt, bolt) - - _, keyStoreService := testKeyStore(tt, bolt) - didService := testDIDRouter(tt, bolt, keyStoreService, []string{"ion"}) - - // create DID by method - ion - missing body - req := httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/dids/ion", nil) - w := httptest.NewRecorder() - params := map[string]string{ - "method": "ion", - } - - c := newRequestContextWithParams(w, req, params) - didService.CreateDIDByMethod(c) - assert.Contains(tt, w.Body.String(), "invalid create DID request") - - // reset recorder between calls - w = httptest.NewRecorder() - - gock.New(testIONResolverURL). - Post("/operations"). - Reply(200) - defer gock.Off() - - // with body, good key type, no options - createDIDRequest := router.CreateDIDByMethodRequest{KeyType: crypto.Ed25519} - requestReader := newRequestValue(tt, createDIDRequest) - req = httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/dids/ion", requestReader) - - c = newRequestContextWithParams(w, req, params) - didService.CreateDIDByMethod(c) - assert.True(tt, util.Is2xxResponse(w.Code)) - - // reset recorder between calls - w = httptest.NewRecorder() - - // good options - options := did.CreateIONDIDOptions{ServiceEndpoints: []didsdk.Service{{ID: "test", Type: "test", ServiceEndpoint: "test"}}} - - // with body, bad key type - createDIDRequest = router.CreateDIDByMethodRequest{KeyType: "bad", Options: options} - requestReader = newRequestValue(tt, createDIDRequest) - req = httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/dids/ion", requestReader) - - c = newRequestContextWithParams(w, req, params) - didService.CreateDIDByMethod(c) - assert.Contains(tt, w.Body.String(), "could not create DID for method with key type: bad") - - // reset recorder between calls - w = httptest.NewRecorder() - - // with body, good key type with options - createDIDRequest = router.CreateDIDByMethodRequest{ - KeyType: crypto.Ed25519, - Options: options, - } - requestReader = newRequestValue(tt, createDIDRequest) - req = httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/dids/ion", requestReader) - - gock.New(testIONResolverURL). - Post("/operations"). - Reply(200) - defer gock.Off() - - c = newRequestContextWithParams(w, req, params) - didService.CreateDIDByMethod(c) - assert.True(tt, util.Is2xxResponse(w.Code)) - - var resp router.CreateDIDByMethodResponse - err := json.NewDecoder(w.Body).Decode(&resp) - assert.NoError(tt, err) - assert.Contains(tt, resp.DID.ID, didsdk.IONMethod) - }) - - t.Run("Test Get DID By Method", func(tt *testing.T) { - bolt := setupTestDB(tt) - require.NotEmpty(tt, bolt) - - _, keyStore := testKeyStore(tt, bolt) - didService := testDIDRouter(tt, bolt, keyStore, []string{"key"}) - - // get DID by method - req := httptest.NewRequest(http.MethodGet, "https://ssi-service.com/v1/dids/bad/worse", nil) - w := httptest.NewRecorder() - - // bad params - badParams := map[string]string{ - "method": "bad", - "id": "worse", - } - c := newRequestContextWithParams(w, req, badParams) - didService.GetDIDByMethod(c) - assert.Contains(tt, w.Body.String(), "could not get DID for method") - - // reset recorder between calls - w = httptest.NewRecorder() - - // good method, bad id - badParams1 := map[string]string{ - "method": "key", - "id": "worse", - } - c = newRequestContextWithParams(w, req, badParams1) - didService.GetDIDByMethod(c) - assert.Contains(tt, w.Body.String(), "could not get DID for method with id: worse") - - // reset recorder between calls - w = httptest.NewRecorder() - - // store a DID - createDIDRequest := router.CreateDIDByMethodRequest{KeyType: crypto.Ed25519} - requestReader := newRequestValue(tt, createDIDRequest) - params := map[string]string{"method": "key"} - req = httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/dids/key", requestReader) - - c = newRequestContextWithParams(w, req, params) - didService.CreateDIDByMethod(c) - assert.True(tt, util.Is2xxResponse(w.Code)) - - var createdDID router.CreateDIDByMethodResponse - err := json.NewDecoder(w.Body).Decode(&createdDID) - assert.NoError(tt, err) - - // reset recorder between calls - w = httptest.NewRecorder() - - // get it back - createdID := createdDID.DID.ID - getDIDPath := fmt.Sprintf("https://ssi-service.com/v1/dids/key/%s", createdID) - req = httptest.NewRequest(http.MethodGet, getDIDPath, nil) - - // good params - goodParams := map[string]string{ - "method": "key", - "id": createdID, - } - c = newRequestContextWithParams(w, req, goodParams) - didService.GetDIDByMethod(c) - assert.True(tt, util.Is2xxResponse(w.Code)) - - var resp router.GetDIDByMethodResponse - err = json.NewDecoder(w.Body).Decode(&resp) - assert.NoError(tt, err) - assert.Equal(tt, createdID, resp.DID.ID) - }) - - t.Run("Test Soft Delete DID By Method", func(tt *testing.T) { - bolt := setupTestDB(tt) - require.NotEmpty(tt, bolt) - - _, keyStore := testKeyStore(tt, bolt) - didService := testDIDRouter(tt, bolt, keyStore, []string{"key"}) - - // soft delete DID by method - req := httptest.NewRequest(http.MethodDelete, "https://ssi-service.com/v1/dids/bad/worse", nil) - w := httptest.NewRecorder() - - // bad params - badParams := map[string]string{ - "method": "bad", - "id": "worse", - } - - c := newRequestContextWithParams(w, req, badParams) - didService.SoftDeleteDIDByMethod(c) - assert.Contains(tt, w.Body.String(), "could not soft delete DID") - - // good method, bad id - badParams1 := map[string]string{ - "method": "key", - "id": "worse", - } - w = httptest.NewRecorder() - c = newRequestContextWithParams(w, req, badParams1) - didService.SoftDeleteDIDByMethod(c) - assert.Contains(tt, w.Body.String(), "could not soft delete DID with id: worse: error getting DID: worse") - - // store a DID - createDIDRequest := router.CreateDIDByMethodRequest{KeyType: crypto.Ed25519} - requestReader := newRequestValue(tt, createDIDRequest) - params := map[string]string{"method": "key"} - req = httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/dids/key", requestReader) - - w = httptest.NewRecorder() - c = newRequestContextWithParams(w, req, params) - didService.CreateDIDByMethod(c) - assert.True(tt, util.Is2xxResponse(w.Code)) - - var createdDID router.CreateDIDByMethodResponse - err := json.NewDecoder(w.Body).Decode(&createdDID) - assert.NoError(tt, err) - - // get all dids for method - req = httptest.NewRequest(http.MethodGet, "https://ssi-service.com/v1/dids/key", requestReader) - w = httptest.NewRecorder() - c = newRequestContextWithParams(w, req, params) - didService.ListDIDsByMethod(c) - assert.True(tt, util.Is2xxResponse(w.Code)) - - var gotDIDsResponse router.ListDIDsByMethodResponse - err = json.NewDecoder(w.Body).Decode(&gotDIDsResponse) - assert.NoError(tt, err) - assert.Len(tt, gotDIDsResponse.DIDs, 1) - - // get it back - createdID := createdDID.DID.ID - getDIDPath := fmt.Sprintf("https://ssi-service.com/v1/dids/key/%s", createdID) - req = httptest.NewRequest(http.MethodGet, getDIDPath, nil) - - // good params - goodParams := map[string]string{ - "method": "key", - "id": createdID, - } - w = httptest.NewRecorder() - c = newRequestContextWithParams(w, req, goodParams) - didService.GetDIDByMethod(c) - assert.NoError(tt, err) - - var resp router.GetDIDByMethodResponse - err = json.NewDecoder(w.Body).Decode(&resp) - assert.NoError(tt, err) - assert.Equal(tt, createdID, resp.DID.ID) - - // delete it - deleteDIDPath := fmt.Sprintf("https://ssi-service.com/v1/dids/key/%s", createdID) - req = httptest.NewRequest(http.MethodDelete, deleteDIDPath, nil) - - w = httptest.NewRecorder() - c = newRequestContextWithParams(w, req, goodParams) - didService.SoftDeleteDIDByMethod(c) - assert.True(tt, util.Is2xxResponse(w.Code)) - - // get it back - req = httptest.NewRequest(http.MethodGet, getDIDPath, nil) - - w = httptest.NewRecorder() - c = newRequestContextWithParams(w, req, goodParams) - didService.GetDIDByMethod(c) - assert.True(tt, util.Is2xxResponse(w.Code)) - - var deletedGetResp router.GetDIDByMethodResponse - err = json.NewDecoder(w.Body).Decode(&deletedGetResp) - assert.NoError(tt, err) - assert.Equal(tt, createdID, deletedGetResp.DID.ID) - - // get all dids for method - req = httptest.NewRequest(http.MethodGet, "https://ssi-service.com/v1/dids/key", requestReader) - w = httptest.NewRecorder() - c = newRequestContextWithParams(w, req, params) - didService.ListDIDsByMethod(c) - assert.True(tt, util.Is2xxResponse(w.Code)) - - var gotDIDsResponseAfterDelete router.ListDIDsByMethodResponse - err = json.NewDecoder(w.Body).Decode(&gotDIDsResponseAfterDelete) - assert.NoError(tt, err) - assert.Len(tt, gotDIDsResponseAfterDelete.DIDs, 0) - - // get all deleted dids for method - req = httptest.NewRequest(http.MethodGet, "https://ssi-service.com/v1/dids/key?deleted=true", requestReader) - w = httptest.NewRecorder() - c = newRequestContextWithParams(w, req, params) - didService.ListDIDsByMethod(c) - assert.True(tt, util.Is2xxResponse(w.Code)) - - var gotDeletedDIDsResponseAfterDelete router.ListDIDsByMethodResponse - err = json.NewDecoder(w.Body).Decode(&gotDeletedDIDsResponseAfterDelete) - assert.NoError(tt, err) - assert.Len(tt, gotDeletedDIDsResponseAfterDelete.DIDs, 1) - }) - - t.Run("List DIDs made up token fails", func(tt *testing.T) { - db := setupTestDB(tt) - require.NotEmpty(tt, db) - _, keyStore := testKeyStore(tt, db) - didService := testDIDRouter(tt, db, keyStore, []string{"key", "web"}) - - w := httptest.NewRecorder() - badParams := url.Values{ - "method": []string{"key"}, - "pageSize": []string{"1"}, - "pageToken": []string{"made up token"}, - } - req := httptest.NewRequest(http.MethodGet, "https://ssi-service.com/v1/dids/key?"+badParams.Encode(), nil) - c := newRequestContextWithURLValues(w, req, badParams) - didService.ListDIDsByMethod(c) - assert.Contains(tt, w.Body.String(), "token value cannot be decoded") - }) - - t.Run("List DIDs pagination", func(tt *testing.T) { - db := setupTestDB(tt) - require.NotEmpty(tt, db) - _, keyStore := testKeyStore(tt, db) - didRouter := testDIDRouter(tt, db, keyStore, []string{"key", "web"}) - - createDIDWithRouter(tt, didRouter) - createDIDWithRouter(tt, didRouter) - - w := httptest.NewRecorder() - params := url.Values{ - "method": []string{"key"}, - "pageSize": []string{"1"}, - } - req := httptest.NewRequest(http.MethodGet, "https://ssi-service.com/v1/dids/key?"+params.Encode(), nil) - c := newRequestContextWithURLValues(w, req, params) - - didRouter.ListDIDsByMethod(c) - - var listDIDsByMethodResponse router.ListDIDsByMethodResponse - err := json.NewDecoder(w.Body).Decode(&listDIDsByMethodResponse) - assert.NoError(tt, err) - assert.NotEmpty(tt, listDIDsByMethodResponse.NextPageToken) - assert.Len(tt, listDIDsByMethodResponse.DIDs, 1) - - w = httptest.NewRecorder() - params["pageToken"] = []string{listDIDsByMethodResponse.NextPageToken} - req = httptest.NewRequest(http.MethodGet, "https://ssi-service.com/v1/dids/key?"+params.Encode(), nil) - c = newRequestContextWithURLValues(w, req, params) - - didRouter.ListDIDsByMethod(c) - - var listDIDsByMethodResponse2 router.ListDIDsByMethodResponse - err = json.NewDecoder(w.Body).Decode(&listDIDsByMethodResponse2) - assert.NoError(tt, err) - assert.Empty(tt, listDIDsByMethodResponse2.NextPageToken) - assert.Len(tt, listDIDsByMethodResponse2.DIDs, 1) - }) - - t.Run("List DIDs pagination change query between calls returns error", func(tt *testing.T) { - db := setupTestDB(tt) - require.NotEmpty(tt, db) - _, keyStore := testKeyStore(tt, db) - didRouter := testDIDRouter(tt, db, keyStore, []string{"key", "web"}) - createDIDWithRouter(tt, didRouter) - createDIDWithRouter(tt, didRouter) - - w := httptest.NewRecorder() - params := url.Values{ - "method": []string{"key"}, - "pageSize": []string{"1"}, - } - req := httptest.NewRequest(http.MethodGet, "https://ssi-service.com/v1/dids/key?"+params.Encode(), nil) - - c := newRequestContextWithURLValues(w, req, params) - didRouter.ListDIDsByMethod(c) - assert.True(tt, util.Is2xxResponse(w.Result().StatusCode)) - - var listDIDsByMethodResponse router.ListDIDsByMethodResponse - err := json.NewDecoder(w.Body).Decode(&listDIDsByMethodResponse) - assert.NoError(tt, err) - assert.NotEmpty(tt, listDIDsByMethodResponse.NextPageToken) - assert.Len(tt, listDIDsByMethodResponse.DIDs, 1) - - w = httptest.NewRecorder() - params["pageToken"] = []string{listDIDsByMethodResponse.NextPageToken} - params["deleted"] = []string{"true"} - req = httptest.NewRequest(http.MethodGet, "https://ssi-service.com/v1/dids/key?"+params.Encode(), nil) - c = newRequestContextWithURLValues(w, req, params) - didRouter.ListDIDsByMethod(c) - assert.Equal(tt, http.StatusBadRequest, w.Result().StatusCode) - assert.Contains(tt, w.Body.String(), "page token must be for the same query") - }) - - t.Run("Test Get DIDs By Method", func(tt *testing.T) { - bolt := setupTestDB(tt) - require.NotEmpty(tt, bolt) - _, keyStore := testKeyStore(tt, bolt) - didService := testDIDRouter(tt, bolt, keyStore, []string{"key", "web"}) - - // get DIDs by method - req := httptest.NewRequest(http.MethodGet, "https://ssi-service.com/v1/dids/bad", nil) - w := httptest.NewRecorder() - - // bad params - badParams := map[string]string{ - "method": "bad", - } - c := newRequestContextWithParams(w, req, badParams) - didService.ListDIDsByMethod(c) - assert.Contains(tt, w.Body.String(), "could not get DIDs for method: bad") - - w = httptest.NewRecorder() - - // good method - goodParams := map[string]string{"method": "key"} - c = newRequestContextWithParams(w, req, goodParams) - didService.ListDIDsByMethod(c) - assert.True(tt, util.Is2xxResponse(w.Code)) - var gotDIDs router.GetDIDByMethodResponse - err := json.NewDecoder(w.Body).Decode(&gotDIDs) - assert.NoError(tt, err) - assert.Empty(tt, gotDIDs) - - // reset recorder between calls - w = httptest.NewRecorder() - - // store two DIDs - createDIDRequest := router.CreateDIDByMethodRequest{KeyType: crypto.Ed25519} - requestReader := newRequestValue(tt, createDIDRequest) - params := map[string]string{"method": "key"} - req = httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/dids/key", requestReader) - - c = newRequestContextWithParams(w, req, params) - didService.CreateDIDByMethod(c) - assert.True(tt, util.Is2xxResponse(w.Code)) - - var createdDID router.CreateDIDByMethodResponse - err = json.NewDecoder(w.Body).Decode(&createdDID) - assert.NoError(tt, err) - - // reset recorder between calls - w = httptest.NewRecorder() - - requestReader = newRequestValue(tt, createDIDRequest) - req = httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/dids/key", requestReader) - - c = newRequestContextWithParams(w, req, params) - didService.CreateDIDByMethod(c) - assert.True(tt, util.Is2xxResponse(w.Code)) - - var createdDID2 router.CreateDIDByMethodResponse - err = json.NewDecoder(w.Body).Decode(&createdDID2) - assert.NoError(tt, err) - - // reset recorder between calls - w = httptest.NewRecorder() - - // get all dids for method - req = httptest.NewRequest(http.MethodGet, "https://ssi-service.com/v1/dids/key", requestReader) - c = newRequestContextWithParams(w, req, params) - didService.ListDIDsByMethod(c) - assert.True(tt, util.Is2xxResponse(w.Code)) - - var gotDIDsResponse router.ListDIDsByMethodResponse - err = json.NewDecoder(w.Body).Decode(&gotDIDsResponse) - assert.NoError(tt, err) - - knownDIDs := map[string]bool{createdDID.DID.ID: true, createdDID2.DID.ID: true} - for _, id := range gotDIDsResponse.DIDs { - if _, ok := knownDIDs[id.ID]; !ok { - tt.Error("got unknown DID") - } else { - delete(knownDIDs, id.ID) - } - } - assert.Len(tt, knownDIDs, 0) - }) - - t.Run("Test Resolve DIDs", func(tt *testing.T) { - bolt := setupTestDB(tt) - require.NotEmpty(tt, bolt) - - _, keyStore := testKeyStore(tt, bolt) - didService := testDIDRouter(tt, bolt, keyStore, []string{"key", "web"}) - - // bad resolution request - req := httptest.NewRequest(http.MethodGet, "https://ssi-service.com/v1/dids/resolver/bad", nil) - w := httptest.NewRecorder() - - badParams := map[string]string{"id": "bad"} - c := newRequestContextWithParams(w, req, badParams) - didService.ResolveDID(c) - assert.Contains(tt, w.Body.String(), "malformed did") - - w = httptest.NewRecorder() - - // known method, bad did - req = httptest.NewRequest(http.MethodGet, "https://ssi-service.com/v1/dids/resolver/did:key:abcd", nil) - badParams = map[string]string{ - "id": "did:key:abcd", - } - c = newRequestContextWithParams(w, req, badParams) - didService.ResolveDID(c) - assert.Contains(tt, w.Body.String(), "unable to resolve DID did:key:abcd") - - w = httptest.NewRecorder() - - // known method, good did - req = httptest.NewRequest(http.MethodGet, "https://ssi-service.com/v1/dids/resolver/did:key:z6MkiTBz1ymuepAQ4HEHYSF1H8quG5GLVVQR3djdX3mDooWp", nil) - goodParams := map[string]string{ - "id": "did:key:z6MkiTBz1ymuepAQ4HEHYSF1H8quG5GLVVQR3djdX3mDooWp", - } - c = newRequestContextWithParams(w, req, goodParams) - didService.ResolveDID(c) - assert.True(tt, util.Is2xxResponse(w.Code)) - - var resolutionResponse router.ResolveDIDResponse - err := json.NewDecoder(w.Body).Decode(&resolutionResponse) - assert.NoError(tt, err) - assert.NotEmpty(tt, resolutionResponse.DIDDocument) - assert.Equal(tt, "did:key:z6MkiTBz1ymuepAQ4HEHYSF1H8quG5GLVVQR3djdX3mDooWp", resolutionResponse.DIDDocument.ID) - }) + + for _, test := range testutil.TestDatabases { + t.Run(test.Name, func(t *testing.T) { + + t.Run("Test Get DID Methods", func(tt *testing.T) { + db := test.ServiceStorage(t) + require.NotEmpty(tt, db) + + _, keyStoreService := testKeyStore(tt, db) + didService := testDIDRouter(tt, db, keyStoreService, []string{"key", "web", "ion"}) + + // get DID method + req := httptest.NewRequest(http.MethodGet, "https://ssi-service.com/v1/dids", nil) + w := httptest.NewRecorder() + + c := newRequestContext(w, req) + didService.ListDIDMethods(c) + assert.Equal(tt, http.StatusOK, w.Result().StatusCode) + + var resp router.ListDIDMethodsResponse + err := json.NewDecoder(w.Body).Decode(&resp) + assert.NoError(tt, err) + + assert.Len(tt, resp.DIDMethods, 3) + assert.Contains(tt, resp.DIDMethods, didsdk.KeyMethod) + assert.Contains(tt, resp.DIDMethods, didsdk.WebMethod) + assert.Contains(tt, resp.DIDMethods, didsdk.IONMethod) + }) + + t.Run("Test Create DID By Method: Key", func(tt *testing.T) { + db := test.ServiceStorage(t) + require.NotEmpty(tt, db) + + _, keyStoreService := testKeyStore(tt, db) + didService := testDIDRouter(tt, db, keyStoreService, []string{"key"}) + + // create DID by method - key - missing body + req := httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/dids/key", nil) + w := httptest.NewRecorder() + params := map[string]string{"method": "key"} + c := newRequestContextWithParams(w, req, params) + didService.CreateDIDByMethod(c) + assert.Contains(tt, w.Body.String(), "invalid create DID request") + + // reset recorder between calls + w = httptest.NewRecorder() + + // with body, bad key type + createDIDRequest := router.CreateDIDByMethodRequest{KeyType: "bad"} + requestReader := newRequestValue(tt, createDIDRequest) + req = httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/dids/key", requestReader) + + c = newRequestContextWithParams(w, req, params) + didService.CreateDIDByMethod(c) + assert.Contains(tt, w.Body.String(), "could not create DID for method with key type: bad") + + // reset recorder between calls + w = httptest.NewRecorder() + + // with body, good key type + createDIDRequest = router.CreateDIDByMethodRequest{KeyType: crypto.Ed25519} + requestReader = newRequestValue(tt, createDIDRequest) + req = httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/dids/key", requestReader) + + c = newRequestContextWithParams(w, req, params) + didService.CreateDIDByMethod(c) + assert.True(tt, util.Is2xxResponse(w.Code)) + + var resp router.CreateDIDByMethodResponse + err := json.NewDecoder(w.Body).Decode(&resp) + assert.NoError(tt, err) + assert.Contains(tt, resp.DID.ID, didsdk.KeyMethod) + }) + + t.Run("Test Create DID By Method: Web", func(tt *testing.T) { + db := test.ServiceStorage(t) + require.NotEmpty(tt, db) + + _, keyStoreService := testKeyStore(tt, db) + didService := testDIDRouter(tt, db, keyStoreService, []string{"web"}) + + // create DID by method - web - missing body + req := httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/dids/web", nil) + w := httptest.NewRecorder() + params := map[string]string{ + "method": "web", + } + + c := newRequestContextWithParams(w, req, params) + didService.CreateDIDByMethod(c) + assert.Contains(tt, w.Body.String(), "invalid create DID request") + + // reset recorder between calls + w = httptest.NewRecorder() + + // with body, good key type, missing options + createDIDRequest := router.CreateDIDByMethodRequest{KeyType: crypto.Ed25519} + requestReader := newRequestValue(tt, createDIDRequest) + req = httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/dids/web", requestReader) + + c = newRequestContextWithParams(w, req, params) + didService.CreateDIDByMethod(c) + assert.Contains(tt, w.Body.String(), "could not create DID for method with key type: Ed25519: options cannot be empty") + + // reset recorder between calls + w = httptest.NewRecorder() + + // good options + options := did.CreateWebDIDOptions{DIDWebID: "did:web:example.com"} + + // with body, bad key type + createDIDRequest = router.CreateDIDByMethodRequest{KeyType: "bad", Options: options} + requestReader = newRequestValue(tt, createDIDRequest) + req = httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/dids/web", requestReader) + + c = newRequestContextWithParams(w, req, params) + didService.CreateDIDByMethod(c) + assert.Contains(tt, w.Body.String(), "could not create DID for method with key type: bad") + + // reset recorder between calls + w = httptest.NewRecorder() + + // with body, good key type with options + createDIDRequest = router.CreateDIDByMethodRequest{ + KeyType: crypto.Ed25519, + Options: options, + } + + requestReader = newRequestValue(tt, createDIDRequest) + req = httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/dids/web", requestReader) + + gock.New("https://example.com"). + Get("/.well-known/did.json"). + Reply(200). + BodyString(`{"didDocument": {"id": "did:web:example.com"}}`) + defer gock.Off() + + c = newRequestContextWithParams(w, req, params) + didService.CreateDIDByMethod(c) + assert.True(tt, util.Is2xxResponse(w.Code)) + + var resp router.CreateDIDByMethodResponse + err := json.NewDecoder(w.Body).Decode(&resp) + assert.NoError(tt, err) + assert.Contains(tt, resp.DID.ID, didsdk.WebMethod) + }) + + t.Run("Test Create DID By Method: ION", func(tt *testing.T) { + db := test.ServiceStorage(t) + require.NotEmpty(tt, db) + + _, keyStoreService := testKeyStore(tt, db) + didService := testDIDRouter(tt, db, keyStoreService, []string{"ion"}) + + // create DID by method - ion - missing body + req := httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/dids/ion", nil) + w := httptest.NewRecorder() + params := map[string]string{ + "method": "ion", + } + + c := newRequestContextWithParams(w, req, params) + didService.CreateDIDByMethod(c) + assert.Contains(tt, w.Body.String(), "invalid create DID request") + + // reset recorder between calls + w = httptest.NewRecorder() + + gock.New(testIONResolverURL). + Post("/operations"). + Reply(200) + defer gock.Off() + + // with body, good key type, no options + createDIDRequest := router.CreateDIDByMethodRequest{KeyType: crypto.Ed25519} + requestReader := newRequestValue(tt, createDIDRequest) + req = httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/dids/ion", requestReader) + + c = newRequestContextWithParams(w, req, params) + didService.CreateDIDByMethod(c) + assert.True(tt, util.Is2xxResponse(w.Code)) + + // reset recorder between calls + w = httptest.NewRecorder() + + // good options + options := did.CreateIONDIDOptions{ServiceEndpoints: []didsdk.Service{{ID: "test", Type: "test", ServiceEndpoint: "test"}}} + + // with body, bad key type + createDIDRequest = router.CreateDIDByMethodRequest{KeyType: "bad", Options: options} + requestReader = newRequestValue(tt, createDIDRequest) + req = httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/dids/ion", requestReader) + + c = newRequestContextWithParams(w, req, params) + didService.CreateDIDByMethod(c) + assert.Contains(tt, w.Body.String(), "could not create DID for method with key type: bad") + + // reset recorder between calls + w = httptest.NewRecorder() + + // with body, good key type with options + createDIDRequest = router.CreateDIDByMethodRequest{ + KeyType: crypto.Ed25519, + Options: options, + } + requestReader = newRequestValue(tt, createDIDRequest) + req = httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/dids/ion", requestReader) + + gock.New(testIONResolverURL). + Post("/operations"). + Reply(200) + defer gock.Off() + + c = newRequestContextWithParams(w, req, params) + didService.CreateDIDByMethod(c) + assert.True(tt, util.Is2xxResponse(w.Code)) + + var resp router.CreateDIDByMethodResponse + err := json.NewDecoder(w.Body).Decode(&resp) + assert.NoError(tt, err) + assert.Contains(tt, resp.DID.ID, didsdk.IONMethod) + }) + + t.Run("Test Get DID By Method", func(tt *testing.T) { + db := test.ServiceStorage(t) + require.NotEmpty(tt, db) + + _, keyStore := testKeyStore(tt, db) + didService := testDIDRouter(tt, db, keyStore, []string{"key"}) + + // get DID by method + req := httptest.NewRequest(http.MethodGet, "https://ssi-service.com/v1/dids/bad/worse", nil) + w := httptest.NewRecorder() + + // bad params + badParams := map[string]string{ + "method": "bad", + "id": "worse", + } + c := newRequestContextWithParams(w, req, badParams) + didService.GetDIDByMethod(c) + assert.Contains(tt, w.Body.String(), "could not get DID for method") + + // reset recorder between calls + w = httptest.NewRecorder() + + // good method, bad id + badParams1 := map[string]string{ + "method": "key", + "id": "worse", + } + c = newRequestContextWithParams(w, req, badParams1) + didService.GetDIDByMethod(c) + assert.Contains(tt, w.Body.String(), "could not get DID for method with id: worse") + + // reset recorder between calls + w = httptest.NewRecorder() + + // store a DID + createDIDRequest := router.CreateDIDByMethodRequest{KeyType: crypto.Ed25519} + requestReader := newRequestValue(tt, createDIDRequest) + params := map[string]string{"method": "key"} + req = httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/dids/key", requestReader) + + c = newRequestContextWithParams(w, req, params) + didService.CreateDIDByMethod(c) + assert.True(tt, util.Is2xxResponse(w.Code)) + + var createdDID router.CreateDIDByMethodResponse + err := json.NewDecoder(w.Body).Decode(&createdDID) + assert.NoError(tt, err) + + // reset recorder between calls + w = httptest.NewRecorder() + + // get it back + createdID := createdDID.DID.ID + getDIDPath := fmt.Sprintf("https://ssi-service.com/v1/dids/key/%s", createdID) + req = httptest.NewRequest(http.MethodGet, getDIDPath, nil) + + // good params + goodParams := map[string]string{ + "method": "key", + "id": createdID, + } + c = newRequestContextWithParams(w, req, goodParams) + didService.GetDIDByMethod(c) + assert.True(tt, util.Is2xxResponse(w.Code)) + + var resp router.GetDIDByMethodResponse + err = json.NewDecoder(w.Body).Decode(&resp) + assert.NoError(tt, err) + assert.Equal(tt, createdID, resp.DID.ID) + }) + + t.Run("Test Soft Delete DID By Method", func(tt *testing.T) { + db := test.ServiceStorage(t) + require.NotEmpty(tt, db) + + _, keyStore := testKeyStore(tt, db) + didService := testDIDRouter(tt, db, keyStore, []string{"key"}) + + // soft delete DID by method + req := httptest.NewRequest(http.MethodDelete, "https://ssi-service.com/v1/dids/bad/worse", nil) + w := httptest.NewRecorder() + + // bad params + badParams := map[string]string{ + "method": "bad", + "id": "worse", + } + + c := newRequestContextWithParams(w, req, badParams) + didService.SoftDeleteDIDByMethod(c) + assert.Contains(tt, w.Body.String(), "could not soft delete DID") + + // good method, bad id + badParams1 := map[string]string{ + "method": "key", + "id": "worse", + } + w = httptest.NewRecorder() + c = newRequestContextWithParams(w, req, badParams1) + didService.SoftDeleteDIDByMethod(c) + assert.Contains(tt, w.Body.String(), "could not soft delete DID with id: worse: error getting DID: worse") + + // store a DID + createDIDRequest := router.CreateDIDByMethodRequest{KeyType: crypto.Ed25519} + requestReader := newRequestValue(tt, createDIDRequest) + params := map[string]string{"method": "key"} + req = httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/dids/key", requestReader) + + w = httptest.NewRecorder() + c = newRequestContextWithParams(w, req, params) + didService.CreateDIDByMethod(c) + assert.True(tt, util.Is2xxResponse(w.Code)) + + var createdDID router.CreateDIDByMethodResponse + err := json.NewDecoder(w.Body).Decode(&createdDID) + assert.NoError(tt, err) + + // get all dids for method + req = httptest.NewRequest(http.MethodGet, "https://ssi-service.com/v1/dids/key", requestReader) + w = httptest.NewRecorder() + c = newRequestContextWithParams(w, req, params) + didService.ListDIDsByMethod(c) + assert.True(tt, util.Is2xxResponse(w.Code)) + + var gotDIDsResponse router.ListDIDsByMethodResponse + err = json.NewDecoder(w.Body).Decode(&gotDIDsResponse) + assert.NoError(tt, err) + assert.Len(tt, gotDIDsResponse.DIDs, 1) + + // get it back + createdID := createdDID.DID.ID + getDIDPath := fmt.Sprintf("https://ssi-service.com/v1/dids/key/%s", createdID) + req = httptest.NewRequest(http.MethodGet, getDIDPath, nil) + + // good params + goodParams := map[string]string{ + "method": "key", + "id": createdID, + } + w = httptest.NewRecorder() + c = newRequestContextWithParams(w, req, goodParams) + didService.GetDIDByMethod(c) + assert.NoError(tt, err) + + var resp router.GetDIDByMethodResponse + err = json.NewDecoder(w.Body).Decode(&resp) + assert.NoError(tt, err) + assert.Equal(tt, createdID, resp.DID.ID) + + // delete it + deleteDIDPath := fmt.Sprintf("https://ssi-service.com/v1/dids/key/%s", createdID) + req = httptest.NewRequest(http.MethodDelete, deleteDIDPath, nil) + + w = httptest.NewRecorder() + c = newRequestContextWithParams(w, req, goodParams) + didService.SoftDeleteDIDByMethod(c) + assert.True(tt, util.Is2xxResponse(w.Code)) + + // get it back + req = httptest.NewRequest(http.MethodGet, getDIDPath, nil) + + w = httptest.NewRecorder() + c = newRequestContextWithParams(w, req, goodParams) + didService.GetDIDByMethod(c) + assert.True(tt, util.Is2xxResponse(w.Code)) + + var deletedGetResp router.GetDIDByMethodResponse + err = json.NewDecoder(w.Body).Decode(&deletedGetResp) + assert.NoError(tt, err) + assert.Equal(tt, createdID, deletedGetResp.DID.ID) + + // get all dids for method + req = httptest.NewRequest(http.MethodGet, "https://ssi-service.com/v1/dids/key", requestReader) + w = httptest.NewRecorder() + c = newRequestContextWithParams(w, req, params) + didService.ListDIDsByMethod(c) + assert.True(tt, util.Is2xxResponse(w.Code)) + + var gotDIDsResponseAfterDelete router.ListDIDsByMethodResponse + err = json.NewDecoder(w.Body).Decode(&gotDIDsResponseAfterDelete) + assert.NoError(tt, err) + assert.Len(tt, gotDIDsResponseAfterDelete.DIDs, 0) + + // get all deleted dids for method + req = httptest.NewRequest(http.MethodGet, "https://ssi-service.com/v1/dids/key?deleted=true", requestReader) + w = httptest.NewRecorder() + c = newRequestContextWithParams(w, req, params) + didService.ListDIDsByMethod(c) + assert.True(tt, util.Is2xxResponse(w.Code)) + + var gotDeletedDIDsResponseAfterDelete router.ListDIDsByMethodResponse + err = json.NewDecoder(w.Body).Decode(&gotDeletedDIDsResponseAfterDelete) + assert.NoError(tt, err) + assert.Len(tt, gotDeletedDIDsResponseAfterDelete.DIDs, 1) + }) + + t.Run("List DIDs made up token fails", func(tt *testing.T) { + db := test.ServiceStorage(t) + require.NotEmpty(tt, db) + _, keyStore := testKeyStore(tt, db) + didService := testDIDRouter(tt, db, keyStore, []string{"key", "web"}) + + w := httptest.NewRecorder() + badParams := url.Values{ + "method": []string{"key"}, + "pageSize": []string{"1"}, + "pageToken": []string{"made up token"}, + } + req := httptest.NewRequest(http.MethodGet, "https://ssi-service.com/v1/dids/key?"+badParams.Encode(), nil) + c := newRequestContextWithURLValues(w, req, badParams) + didService.ListDIDsByMethod(c) + assert.Contains(tt, w.Body.String(), "token value cannot be decoded") + }) + + t.Run("List DIDs pagination", func(tt *testing.T) { + if !strings.Contains(test.Name, "Redis") { + db := test.ServiceStorage(t) + require.NotEmpty(tt, db) + _, keyStore := testKeyStore(tt, db) + didRouter := testDIDRouter(tt, db, keyStore, []string{"key", "web"}) + + createDIDWithRouter(tt, didRouter) + createDIDWithRouter(tt, didRouter) + + w := httptest.NewRecorder() + params := url.Values{ + "method": []string{"key"}, + "pageSize": []string{"1"}, + } + req := httptest.NewRequest(http.MethodGet, "https://ssi-service.com/v1/dids/key?"+params.Encode(), nil) + c := newRequestContextWithURLValues(w, req, params) + + didRouter.ListDIDsByMethod(c) + + var listDIDsByMethodResponse router.ListDIDsByMethodResponse + err := json.NewDecoder(w.Body).Decode(&listDIDsByMethodResponse) + assert.NoError(tt, err) + assert.NotEmpty(tt, listDIDsByMethodResponse.NextPageToken) + assert.Len(tt, listDIDsByMethodResponse.DIDs, 1) + + w = httptest.NewRecorder() + params["pageToken"] = []string{listDIDsByMethodResponse.NextPageToken} + req = httptest.NewRequest(http.MethodGet, "https://ssi-service.com/v1/dids/key?"+params.Encode(), nil) + c = newRequestContextWithURLValues(w, req, params) + + didRouter.ListDIDsByMethod(c) + + var listDIDsByMethodResponse2 router.ListDIDsByMethodResponse + err = json.NewDecoder(w.Body).Decode(&listDIDsByMethodResponse2) + assert.NoError(tt, err) + assert.Empty(tt, listDIDsByMethodResponse2.NextPageToken) + assert.Len(tt, listDIDsByMethodResponse2.DIDs, 1) + } + }) + + t.Run("List DIDs pagination change query between calls returns error", func(tt *testing.T) { + if !strings.Contains(test.Name, "Redis") { + db := test.ServiceStorage(t) + require.NotEmpty(tt, db) + _, keyStore := testKeyStore(tt, db) + didRouter := testDIDRouter(tt, db, keyStore, []string{"key", "web"}) + createDIDWithRouter(tt, didRouter) + createDIDWithRouter(tt, didRouter) + + w := httptest.NewRecorder() + params := url.Values{ + "method": []string{"key"}, + "pageSize": []string{"1"}, + } + req := httptest.NewRequest(http.MethodGet, "https://ssi-service.com/v1/dids/key?"+params.Encode(), nil) + + c := newRequestContextWithURLValues(w, req, params) + didRouter.ListDIDsByMethod(c) + assert.True(tt, util.Is2xxResponse(w.Result().StatusCode)) + + var listDIDsByMethodResponse router.ListDIDsByMethodResponse + err := json.NewDecoder(w.Body).Decode(&listDIDsByMethodResponse) + assert.NoError(tt, err) + assert.NotEmpty(tt, listDIDsByMethodResponse.NextPageToken) + assert.Len(tt, listDIDsByMethodResponse.DIDs, 1) + + w = httptest.NewRecorder() + params["pageToken"] = []string{listDIDsByMethodResponse.NextPageToken} + params["deleted"] = []string{"true"} + req = httptest.NewRequest(http.MethodGet, "https://ssi-service.com/v1/dids/key?"+params.Encode(), nil) + c = newRequestContextWithURLValues(w, req, params) + didRouter.ListDIDsByMethod(c) + assert.Equal(tt, http.StatusBadRequest, w.Result().StatusCode) + assert.Contains(tt, w.Body.String(), "page token must be for the same query") + } + }) + + t.Run("Test Get DIDs By Method", func(tt *testing.T) { + db := test.ServiceStorage(t) + require.NotEmpty(tt, db) + _, keyStore := testKeyStore(tt, db) + didService := testDIDRouter(tt, db, keyStore, []string{"key", "web"}) + + // get DIDs by method + req := httptest.NewRequest(http.MethodGet, "https://ssi-service.com/v1/dids/bad", nil) + w := httptest.NewRecorder() + + // bad params + badParams := map[string]string{ + "method": "bad", + } + c := newRequestContextWithParams(w, req, badParams) + didService.ListDIDsByMethod(c) + assert.Contains(tt, w.Body.String(), "could not get DIDs for method: bad") + + w = httptest.NewRecorder() + + // good method + goodParams := map[string]string{"method": "key"} + c = newRequestContextWithParams(w, req, goodParams) + didService.ListDIDsByMethod(c) + assert.True(tt, util.Is2xxResponse(w.Code)) + var gotDIDs router.GetDIDByMethodResponse + err := json.NewDecoder(w.Body).Decode(&gotDIDs) + assert.NoError(tt, err) + assert.Empty(tt, gotDIDs) + + // reset recorder between calls + w = httptest.NewRecorder() + + // store two DIDs + createDIDRequest := router.CreateDIDByMethodRequest{KeyType: crypto.Ed25519} + requestReader := newRequestValue(tt, createDIDRequest) + params := map[string]string{"method": "key"} + req = httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/dids/key", requestReader) + + c = newRequestContextWithParams(w, req, params) + didService.CreateDIDByMethod(c) + assert.True(tt, util.Is2xxResponse(w.Code)) + + var createdDID router.CreateDIDByMethodResponse + err = json.NewDecoder(w.Body).Decode(&createdDID) + assert.NoError(tt, err) + + // reset recorder between calls + w = httptest.NewRecorder() + + requestReader = newRequestValue(tt, createDIDRequest) + req = httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/dids/key", requestReader) + + c = newRequestContextWithParams(w, req, params) + didService.CreateDIDByMethod(c) + assert.True(tt, util.Is2xxResponse(w.Code)) + + var createdDID2 router.CreateDIDByMethodResponse + err = json.NewDecoder(w.Body).Decode(&createdDID2) + assert.NoError(tt, err) + + // reset recorder between calls + w = httptest.NewRecorder() + + // get all dids for method + req = httptest.NewRequest(http.MethodGet, "https://ssi-service.com/v1/dids/key", requestReader) + c = newRequestContextWithParams(w, req, params) + didService.ListDIDsByMethod(c) + assert.True(tt, util.Is2xxResponse(w.Code)) + + var gotDIDsResponse router.ListDIDsByMethodResponse + err = json.NewDecoder(w.Body).Decode(&gotDIDsResponse) + assert.NoError(tt, err) + + knownDIDs := map[string]bool{createdDID.DID.ID: true, createdDID2.DID.ID: true} + for _, id := range gotDIDsResponse.DIDs { + if _, ok := knownDIDs[id.ID]; !ok { + tt.Error("got unknown DID") + } else { + delete(knownDIDs, id.ID) + } + } + assert.Len(tt, knownDIDs, 0) + }) + + t.Run("Test Resolve DIDs", func(tt *testing.T) { + db := test.ServiceStorage(t) + require.NotEmpty(tt, db) + + _, keyStore := testKeyStore(tt, db) + didService := testDIDRouter(tt, db, keyStore, []string{"key", "web"}) + + // bad resolution request + req := httptest.NewRequest(http.MethodGet, "https://ssi-service.com/v1/dids/resolver/bad", nil) + w := httptest.NewRecorder() + + badParams := map[string]string{"id": "bad"} + c := newRequestContextWithParams(w, req, badParams) + didService.ResolveDID(c) + assert.Contains(tt, w.Body.String(), "malformed did") + + w = httptest.NewRecorder() + + // known method, bad did + req = httptest.NewRequest(http.MethodGet, "https://ssi-service.com/v1/dids/resolver/did:key:abcd", nil) + badParams = map[string]string{ + "id": "did:key:abcd", + } + c = newRequestContextWithParams(w, req, badParams) + didService.ResolveDID(c) + assert.Contains(tt, w.Body.String(), "unable to resolve DID did:key:abcd") + + w = httptest.NewRecorder() + + // known method, good did + req = httptest.NewRequest(http.MethodGet, "https://ssi-service.com/v1/dids/resolver/did:key:z6MkiTBz1ymuepAQ4HEHYSF1H8quG5GLVVQR3djdX3mDooWp", nil) + goodParams := map[string]string{ + "id": "did:key:z6MkiTBz1ymuepAQ4HEHYSF1H8quG5GLVVQR3djdX3mDooWp", + } + c = newRequestContextWithParams(w, req, goodParams) + didService.ResolveDID(c) + assert.True(tt, util.Is2xxResponse(w.Code)) + + var resolutionResponse router.ResolveDIDResponse + err := json.NewDecoder(w.Body).Decode(&resolutionResponse) + assert.NoError(tt, err) + assert.NotEmpty(tt, resolutionResponse.DIDDocument) + assert.Equal(tt, "did:key:z6MkiTBz1ymuepAQ4HEHYSF1H8quG5GLVVQR3djdX3mDooWp", resolutionResponse.DIDDocument.ID) + }) + }) + } } func createDIDWithRouter(tt *testing.T, didService *router.DIDRouter) { diff --git a/pkg/server/server_issuance_test.go b/pkg/server/server_issuance_test.go index 210eb10bb..9a7bc8b40 100644 --- a/pkg/server/server_issuance_test.go +++ b/pkg/server/server_issuance_test.go @@ -24,340 +24,346 @@ import ( "github.com/tbd54566975/ssi-service/pkg/service/manifest/model" "github.com/tbd54566975/ssi-service/pkg/service/schema" "github.com/tbd54566975/ssi-service/pkg/storage" + "github.com/tbd54566975/ssi-service/pkg/testutil" ) func TestIssuanceRouter(t *testing.T) { now := time.Now() duration := 10 * time.Second - t.Run("CreateIssuanceTemplate", func(tt *testing.T) { - issuerResp, createdSchema, manifest, r := setupAllThings(tt) - for _, tc := range []struct { - name string - request router.CreateIssuanceTemplateRequest - }{ - { - name: "returns a template with ID", - request: router.CreateIssuanceTemplateRequest{ - Template: issuance.Template{ - CredentialManifest: manifest.Manifest.ID, - Issuer: issuerResp.DID.ID, - IssuerKID: issuerResp.DID.VerificationMethod[0].ID, - Credentials: []issuance.CredentialTemplate{ - { - ID: "output_descriptor_1", - Schema: createdSchema.ID, - Data: issuance.ClaimTemplates{ - "foo": "bar", - "hello": "$.vcsomething.something", - }, - Expiry: issuance.TimeLike{ - Time: &now, + + for _, test := range testutil.TestDatabases { + t.Run(test.Name, func(t *testing.T) { + t.Run("CreateIssuanceTemplate", func(tt *testing.T) { + issuerResp, createdSchema, manifest, r := setupAllThings(tt, test.ServiceStorage(t)) + for _, tc := range []struct { + name string + request router.CreateIssuanceTemplateRequest + }{ + { + name: "returns a template with ID", + request: router.CreateIssuanceTemplateRequest{ + Template: issuance.Template{ + CredentialManifest: manifest.Manifest.ID, + Issuer: issuerResp.DID.ID, + IssuerKID: issuerResp.DID.VerificationMethod[0].ID, + Credentials: []issuance.CredentialTemplate{ + { + ID: "output_descriptor_1", + Schema: createdSchema.ID, + Data: issuance.ClaimTemplates{ + "foo": "bar", + "hello": "$.vcsomething.something", + }, + Expiry: issuance.TimeLike{ + Time: &now, + }, + }, }, }, }, }, - }, - }, - { - name: "returns a template with ID when schema is empty", - request: router.CreateIssuanceTemplateRequest{ - Template: issuance.Template{ - CredentialManifest: manifest.Manifest.ID, - Issuer: issuerResp.DID.ID, - IssuerKID: issuerResp.DID.VerificationMethod[0].ID, - Credentials: []issuance.CredentialTemplate{ - { - ID: "output_descriptor_1", - Schema: "", - Data: issuance.ClaimTemplates{ - "foo": "bar", - "hello": "$.vcsomething.something", - }, - Expiry: issuance.TimeLike{ - Time: &now, + { + name: "returns a template with ID when schema is empty", + request: router.CreateIssuanceTemplateRequest{ + Template: issuance.Template{ + CredentialManifest: manifest.Manifest.ID, + Issuer: issuerResp.DID.ID, + IssuerKID: issuerResp.DID.VerificationMethod[0].ID, + Credentials: []issuance.CredentialTemplate{ + { + ID: "output_descriptor_1", + Schema: "", + Data: issuance.ClaimTemplates{ + "foo": "bar", + "hello": "$.vcsomething.something", + }, + Expiry: issuance.TimeLike{ + Time: &now, + }, + }, }, }, }, }, - }, - }, - } { - tt.Run(tc.name, func(t *testing.T) { - value := newRequestValue(t, tc.request) - req := httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/issuancetemplates", value) - w := httptest.NewRecorder() - - c := newRequestContext(w, req) - r.CreateIssuanceTemplate(c) - assert.True(t, util.Is2xxResponse(w.Code)) - - var resp issuance.Template - assert.NoError(t, json.NewDecoder(w.Body).Decode(&resp)) - assert.NotEmpty(t, resp.ID) + } { + tt.Run(tc.name, func(t *testing.T) { + value := newRequestValue(t, tc.request) + req := httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/issuancetemplates", value) + w := httptest.NewRecorder() + + c := newRequestContext(w, req) + r.CreateIssuanceTemplate(c) + assert.True(t, util.Is2xxResponse(w.Code)) + + var resp issuance.Template + assert.NoError(t, json.NewDecoder(w.Body).Decode(&resp)) + assert.NotEmpty(t, resp.ID) + }) + } }) - } - }) - t.Run("CreateIssuanceTemplate returns error", func(tt *testing.T) { - issuerResp, createdSchema, manifest, r := setupAllThings(tt) + t.Run("CreateIssuanceTemplate returns error", func(tt *testing.T) { + issuerResp, createdSchema, manifest, r := setupAllThings(tt, test.ServiceStorage(t)) - for _, tc := range []struct { - name string - request router.CreateIssuanceTemplateRequest - expectedError string - }{ - { - name: "when missing output_descriptor_id", - request: router.CreateIssuanceTemplateRequest{ - Template: issuance.Template{ - CredentialManifest: manifest.Manifest.ID, - Issuer: issuerResp.DID.ID, - IssuerKID: issuerResp.DID.VerificationMethod[0].ID, - Credentials: []issuance.CredentialTemplate{ - { - ID: "", - Schema: createdSchema.ID, - Data: issuance.ClaimTemplates{ - "foo": "bar", - "hello": "$.vcsomething.something", - }, - Expiry: issuance.TimeLike{ - Time: &now, + for _, tc := range []struct { + name string + request router.CreateIssuanceTemplateRequest + expectedError string + }{ + { + name: "when missing output_descriptor_id", + request: router.CreateIssuanceTemplateRequest{ + Template: issuance.Template{ + CredentialManifest: manifest.Manifest.ID, + Issuer: issuerResp.DID.ID, + IssuerKID: issuerResp.DID.VerificationMethod[0].ID, + Credentials: []issuance.CredentialTemplate{ + { + ID: "", + Schema: createdSchema.ID, + Data: issuance.ClaimTemplates{ + "foo": "bar", + "hello": "$.vcsomething.something", + }, + Expiry: issuance.TimeLike{ + Time: &now, + }, + }, }, }, }, + expectedError: "ID cannot be empty", }, - }, - expectedError: "ID cannot be empty", - }, - { - name: "when both times are set", - request: router.CreateIssuanceTemplateRequest{ - Template: issuance.Template{ - CredentialManifest: manifest.Manifest.ID, - Issuer: issuerResp.DID.ID, - IssuerKID: issuerResp.DID.VerificationMethod[0].ID, - Credentials: []issuance.CredentialTemplate{ - { - ID: "output_descriptor_1", - Schema: createdSchema.ID, - Data: issuance.ClaimTemplates{ - "foo": "bar", - "hello": "$.vcsomething.something", - }, - Expiry: issuance.TimeLike{ - Time: &now, - Duration: &duration, + { + name: "when both times are set", + request: router.CreateIssuanceTemplateRequest{ + Template: issuance.Template{ + CredentialManifest: manifest.Manifest.ID, + Issuer: issuerResp.DID.ID, + IssuerKID: issuerResp.DID.VerificationMethod[0].ID, + Credentials: []issuance.CredentialTemplate{ + { + ID: "output_descriptor_1", + Schema: createdSchema.ID, + Data: issuance.ClaimTemplates{ + "foo": "bar", + "hello": "$.vcsomething.something", + }, + Expiry: issuance.TimeLike{ + Time: &now, + Duration: &duration, + }, + }, }, }, }, + expectedError: "Time and Duration cannot be both set simultaneously", }, - }, - expectedError: "Time and Duration cannot be both set simultaneously", - }, - { - name: "when credential schema does not exist", - request: router.CreateIssuanceTemplateRequest{ - Template: issuance.Template{ - CredentialManifest: manifest.Manifest.ID, - Issuer: issuerResp.DID.ID, - IssuerKID: issuerResp.DID.VerificationMethod[0].ID, - Credentials: []issuance.CredentialTemplate{ - { - ID: "output_descriptor_1", - Schema: "fake schema", - Data: issuance.ClaimTemplates{ - "foo": "bar", - "hello": "$.vcsomething.something", - }, - Expiry: issuance.TimeLike{ - Time: &now, + { + name: "when credential schema does not exist", + request: router.CreateIssuanceTemplateRequest{ + Template: issuance.Template{ + CredentialManifest: manifest.Manifest.ID, + Issuer: issuerResp.DID.ID, + IssuerKID: issuerResp.DID.VerificationMethod[0].ID, + Credentials: []issuance.CredentialTemplate{ + { + ID: "output_descriptor_1", + Schema: "fake schema", + Data: issuance.ClaimTemplates{ + "foo": "bar", + "hello": "$.vcsomething.something", + }, + Expiry: issuance.TimeLike{ + Time: &now, + }, + }, }, }, }, + expectedError: "schema not found", }, - }, - expectedError: "schema not found", - }, - { - name: "when credential manifest ID is does not exist", - request: router.CreateIssuanceTemplateRequest{ - Template: issuance.Template{ - CredentialManifest: "fake manifest id", - Issuer: issuerResp.DID.ID, - IssuerKID: issuerResp.DID.VerificationMethod[0].ID, - Credentials: []issuance.CredentialTemplate{ - { - ID: "output_descriptor_1", - Schema: createdSchema.ID, - Data: issuance.ClaimTemplates{ - "foo": "bar", - "hello": "$.vcsomething.something", - }, - Expiry: issuance.TimeLike{ - Time: &now, + { + name: "when credential manifest ID is does not exist", + request: router.CreateIssuanceTemplateRequest{ + Template: issuance.Template{ + CredentialManifest: "fake manifest id", + Issuer: issuerResp.DID.ID, + IssuerKID: issuerResp.DID.VerificationMethod[0].ID, + Credentials: []issuance.CredentialTemplate{ + { + ID: "output_descriptor_1", + Schema: createdSchema.ID, + Data: issuance.ClaimTemplates{ + "foo": "bar", + "hello": "$.vcsomething.something", + }, + Expiry: issuance.TimeLike{ + Time: &now, + }, + }, }, }, }, + expectedError: "manifest not found", }, - }, - expectedError: "manifest not found", - }, - { - name: "when issuer is empty", - request: router.CreateIssuanceTemplateRequest{ - Template: issuance.Template{ - CredentialManifest: manifest.Manifest.ID, - Credentials: []issuance.CredentialTemplate{ - { - ID: "output_descriptor_1", - Schema: createdSchema.ID, - Data: issuance.ClaimTemplates{ - "foo": "bar", - "hello": "$.vcsomething.something", - }, - Expiry: issuance.TimeLike{ - Time: &now, + { + name: "when issuer is empty", + request: router.CreateIssuanceTemplateRequest{ + Template: issuance.Template{ + CredentialManifest: manifest.Manifest.ID, + Credentials: []issuance.CredentialTemplate{ + { + ID: "output_descriptor_1", + Schema: createdSchema.ID, + Data: issuance.ClaimTemplates{ + "foo": "bar", + "hello": "$.vcsomething.something", + }, + Expiry: issuance.TimeLike{ + Time: &now, + }, + }, }, }, }, + expectedError: "field validation error", }, - }, - expectedError: "field validation error", - }, - } { - tt.Run(tc.name, func(ttt *testing.T) { - value := newRequestValue(ttt, tc.request) - req := httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/issuancetemplates", value) - w := httptest.NewRecorder() - - c := newRequestContext(w, req) - r.CreateIssuanceTemplate(c) - assert.Contains(ttt, w.Body.String(), tc.expectedError) + } { + tt.Run(tc.name, func(ttt *testing.T) { + value := newRequestValue(ttt, tc.request) + req := httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/issuancetemplates", value) + w := httptest.NewRecorder() + + c := newRequestContext(w, req) + r.CreateIssuanceTemplate(c) + assert.Contains(ttt, w.Body.String(), tc.expectedError) + }) + + } }) - } - }) - - t.Run("Create, Get, Delete work as expected", func(tt *testing.T) { - issuerResp, createdSchema, manifest, r := setupAllThings(tt) + t.Run("Create, Get, Delete work as expected", func(tt *testing.T) { + issuerResp, createdSchema, manifest, r := setupAllThings(tt, test.ServiceStorage(t)) + + inputTemplate := issuance.Template{ + CredentialManifest: manifest.Manifest.ID, + Issuer: issuerResp.DID.ID, + IssuerKID: issuerResp.DID.VerificationMethod[0].ID, + Credentials: []issuance.CredentialTemplate{ + { + ID: "output_descriptor_1", + Schema: createdSchema.ID, + Data: issuance.ClaimTemplates{ + "foo": "bar", + "hello": "$.vcsomething.something", + }, + Expiry: issuance.TimeLike{ + Time: &now, + }, + }, + }, + } + var issuanceTemplate issuance.Template - inputTemplate := issuance.Template{ - CredentialManifest: manifest.Manifest.ID, - Issuer: issuerResp.DID.ID, - IssuerKID: issuerResp.DID.VerificationMethod[0].ID, - Credentials: []issuance.CredentialTemplate{ { - ID: "output_descriptor_1", - Schema: createdSchema.ID, - Data: issuance.ClaimTemplates{ - "foo": "bar", - "hello": "$.vcsomething.something", - }, - Expiry: issuance.TimeLike{ - Time: &now, - }, - }, - }, - } - var issuanceTemplate issuance.Template - - { - request := router.CreateIssuanceTemplateRequest{Template: inputTemplate} - value := newRequestValue(t, request) - req := httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/issuancetemplates", value) - w := httptest.NewRecorder() - - c := newRequestContext(w, req) - r.CreateIssuanceTemplate(c) - assert.True(tt, util.Is2xxResponse(w.Code)) - - assert.NoError(t, json.NewDecoder(w.Body).Decode(&issuanceTemplate)) - if diff := cmp.Diff(inputTemplate, issuanceTemplate, cmpopts.IgnoreFields(issuance.Template{}, "ID")); diff != "" { - t.Errorf("IssuanceTemplate mismatch (-want +got):\n%s", diff) - } - } + request := router.CreateIssuanceTemplateRequest{Template: inputTemplate} + value := newRequestValue(t, request) + req := httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/issuancetemplates", value) + w := httptest.NewRecorder() - { - value := newRequestValue(tt, nil) - req := httptest.NewRequest(http.MethodGet, "https://ssi-service.com/v1/issuancetemplates/"+issuanceTemplate.ID, value) - w := httptest.NewRecorder() + c := newRequestContext(w, req) + r.CreateIssuanceTemplate(c) + assert.True(tt, util.Is2xxResponse(w.Code)) - c := newRequestContextWithParams(w, req, map[string]string{"id": issuanceTemplate.ID}) - r.GetIssuanceTemplate(c) - assert.True(tt, util.Is2xxResponse(w.Code)) + assert.NoError(t, json.NewDecoder(w.Body).Decode(&issuanceTemplate)) + if diff := cmp.Diff(inputTemplate, issuanceTemplate, cmpopts.IgnoreFields(issuance.Template{}, "ID")); diff != "" { + t.Errorf("IssuanceTemplate mismatch (-want +got):\n%s", diff) + } + } - var getIssuanceTemplate issuance.Template - assert.NoError(t, json.NewDecoder(w.Body).Decode(&getIssuanceTemplate)) - if diff := cmp.Diff(issuanceTemplate, getIssuanceTemplate); diff != "" { - tt.Errorf("Template mismatch (-want +got):\n%s", diff) - } - } + { + value := newRequestValue(tt, nil) + req := httptest.NewRequest(http.MethodGet, "https://ssi-service.com/v1/issuancetemplates/"+issuanceTemplate.ID, value) + w := httptest.NewRecorder() - { - value := newRequestValue(tt, nil) - req := httptest.NewRequest(http.MethodGet, "https://ssi-service.com/v1/issuancetemplates/"+issuanceTemplate.ID, value) - w := httptest.NewRecorder() - c := newRequestContextWithParams(w, req, map[string]string{"id": issuanceTemplate.ID}) - r.DeleteIssuanceTemplate(c) - assert.True(tt, util.Is2xxResponse(w.Code)) - } + c := newRequestContextWithParams(w, req, map[string]string{"id": issuanceTemplate.ID}) + r.GetIssuanceTemplate(c) + assert.True(tt, util.Is2xxResponse(w.Code)) - { - value := newRequestValue(tt, nil) - req := httptest.NewRequest(http.MethodGet, "https://ssi-service.com/v1/issuancetemplates/"+issuanceTemplate.ID, value) - w := httptest.NewRecorder() - c := newRequestContextWithParams(w, req, map[string]string{"id": issuanceTemplate.ID}) - r.GetIssuanceTemplate(c) - assert.Contains(tt, w.Body.String(), "issuance template not found") - } - }) + var getIssuanceTemplate issuance.Template + assert.NoError(t, json.NewDecoder(w.Body).Decode(&getIssuanceTemplate)) + if diff := cmp.Diff(issuanceTemplate, getIssuanceTemplate); diff != "" { + tt.Errorf("Template mismatch (-want +got):\n%s", diff) + } + } - t.Run("GetIssuanceTemplate returns error for unknown ID", func(tt *testing.T) { - s := setupTestDB(tt) - r := testIssuanceRouter(tt, s) + { + value := newRequestValue(tt, nil) + req := httptest.NewRequest(http.MethodGet, "https://ssi-service.com/v1/issuancetemplates/"+issuanceTemplate.ID, value) + w := httptest.NewRecorder() + c := newRequestContextWithParams(w, req, map[string]string{"id": issuanceTemplate.ID}) + r.DeleteIssuanceTemplate(c) + assert.True(tt, util.Is2xxResponse(w.Code)) + } - value := newRequestValue(tt, nil) - req := httptest.NewRequest(http.MethodGet, "https://ssi-service.com/v1/issuancetemplates/where-is-it", value) - w := httptest.NewRecorder() - c := newRequestContextWithParams(w, req, map[string]string{"id": "where-is-it"}) - r.GetIssuanceTemplate(c) - assert.Contains(tt, w.Body.String(), "issuance template not found") - }) + { + value := newRequestValue(tt, nil) + req := httptest.NewRequest(http.MethodGet, "https://ssi-service.com/v1/issuancetemplates/"+issuanceTemplate.ID, value) + w := httptest.NewRecorder() + c := newRequestContextWithParams(w, req, map[string]string{"id": issuanceTemplate.ID}) + r.GetIssuanceTemplate(c) + assert.Contains(tt, w.Body.String(), "issuance template not found") + } + }) - t.Run("ListIssuanceTemplates returns empty when there aren't templates", func(tt *testing.T) { - s := setupTestDB(tt) - r := testIssuanceRouter(tt, s) + t.Run("GetIssuanceTemplate returns error for unknown ID", func(tt *testing.T) { + s := test.ServiceStorage(t) + r := testIssuanceRouter(tt, s) - value := newRequestValue(tt, nil) - req := httptest.NewRequest(http.MethodGet, "https://ssi-service.com/v1/issuancetemplates", value) - w := httptest.NewRecorder() - c := newRequestContext(w, req) - r.ListIssuanceTemplates(c) - assert.True(tt, util.Is2xxResponse(w.Code)) + value := newRequestValue(tt, nil) + req := httptest.NewRequest(http.MethodGet, "https://ssi-service.com/v1/issuancetemplates/where-is-it", value) + w := httptest.NewRecorder() + c := newRequestContextWithParams(w, req, map[string]string{"id": "where-is-it"}) + r.GetIssuanceTemplate(c) + assert.Contains(tt, w.Body.String(), "issuance template not found") + }) - var getIssuanceTemplate router.ListIssuanceTemplatesResponse - assert.NoError(tt, json.NewDecoder(w.Body).Decode(&getIssuanceTemplate)) - assert.Empty(tt, getIssuanceTemplate.IssuanceTemplates) - }) + t.Run("ListIssuanceTemplates returns empty when there aren't templates", func(tt *testing.T) { + s := test.ServiceStorage(t) + r := testIssuanceRouter(tt, s) - t.Run("ListIssuanceTemplates returns all created templates", func(tt *testing.T) { - issuerResp, createdSchema, manifest, r := setupAllThings(tt) + value := newRequestValue(tt, nil) + req := httptest.NewRequest(http.MethodGet, "https://ssi-service.com/v1/issuancetemplates", value) + w := httptest.NewRecorder() + c := newRequestContext(w, req) + r.ListIssuanceTemplates(c) + assert.True(tt, util.Is2xxResponse(w.Code)) - createSimpleTemplate(tt, manifest, issuerResp, createdSchema, now, r) - createSimpleTemplate(tt, manifest, issuerResp, createdSchema, now, r) + var getIssuanceTemplate router.ListIssuanceTemplatesResponse + assert.NoError(tt, json.NewDecoder(w.Body).Decode(&getIssuanceTemplate)) + assert.Empty(tt, getIssuanceTemplate.IssuanceTemplates) + }) - value := newRequestValue(tt, nil) - req := httptest.NewRequest(http.MethodGet, "https://ssi-service.com/v1/issuancetemplates", value) - w := httptest.NewRecorder() - c := newRequestContext(w, req) - r.ListIssuanceTemplates(c) - assert.True(tt, util.Is2xxResponse(w.Code)) + t.Run("ListIssuanceTemplates returns all created templates", func(tt *testing.T) { + issuerResp, createdSchema, manifest, r := setupAllThings(tt, test.ServiceStorage(t)) - var getIssuanceTemplate router.ListIssuanceTemplatesResponse - assert.NoError(tt, json.NewDecoder(w.Body).Decode(&getIssuanceTemplate)) - assert.Len(tt, getIssuanceTemplate.IssuanceTemplates, 2) - }) + createSimpleTemplate(tt, manifest, issuerResp, createdSchema, now, r) + createSimpleTemplate(tt, manifest, issuerResp, createdSchema, now, r) + + value := newRequestValue(tt, nil) + req := httptest.NewRequest(http.MethodGet, "https://ssi-service.com/v1/issuancetemplates", value) + w := httptest.NewRecorder() + c := newRequestContext(w, req) + r.ListIssuanceTemplates(c) + assert.True(tt, util.Is2xxResponse(w.Code)) + + var getIssuanceTemplate router.ListIssuanceTemplatesResponse + assert.NoError(tt, json.NewDecoder(w.Body).Decode(&getIssuanceTemplate)) + assert.Len(tt, getIssuanceTemplate.IssuanceTemplates, 2) + }) + }) + } } func createSimpleTemplate(t *testing.T, manifest *model.CreateManifestResponse, issuerResp *did.CreateDIDResponse, @@ -393,9 +399,7 @@ func createSimpleTemplate(t *testing.T, manifest *model.CreateManifestResponse, } } -func setupAllThings(t *testing.T) (*did.CreateDIDResponse, *schema.CreateSchemaResponse, *model.CreateManifestResponse, *router.IssuanceRouter) { - s := setupTestDB(t) - +func setupAllThings(t *testing.T, s storage.ServiceStorage) (*did.CreateDIDResponse, *schema.CreateSchemaResponse, *model.CreateManifestResponse, *router.IssuanceRouter) { _, keyStoreSvc := testKeyStore(t, s) didSvc := testDIDService(t, s, keyStoreSvc) schemaSvc := testSchemaService(t, s, keyStoreSvc, didSvc) diff --git a/pkg/server/server_keystore_test.go b/pkg/server/server_keystore_test.go index 77a3aa7dc..3dd8484c1 100644 --- a/pkg/server/server_keystore_test.go +++ b/pkg/server/server_keystore_test.go @@ -14,101 +14,107 @@ import ( "github.com/tbd54566975/ssi-service/internal/util" "github.com/tbd54566975/ssi-service/pkg/server/router" + "github.com/tbd54566975/ssi-service/pkg/testutil" ) func TestKeyStoreAPI(t *testing.T) { - t.Run("Test Store Key", func(tt *testing.T) { - bolt := setupTestDB(tt) - require.NotEmpty(tt, bolt) - - keyStoreRouter, _ := testKeyStore(tt, bolt) - w := httptest.NewRecorder() - - // bad key type - badKeyStoreRequest := router.StoreKeyRequest{ - ID: "test-kid", - Type: "bad", - Controller: "me", - PrivateKeyBase58: "bad", - } - badRequestValue := newRequestValue(tt, badKeyStoreRequest) - req := httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/keys", badRequestValue) - - c := newRequestContext(w, req) - keyStoreRouter.StoreKey(c) - assert.Contains(tt, w.Body.String(), "unsupported key type: bad") - - // store a valid key - _, privKey, err := crypto.GenerateKeyByKeyType(crypto.Ed25519) - assert.NoError(tt, err) - assert.NotEmpty(tt, privKey) - - privKeyBytes, err := crypto.PrivKeyToBytes(privKey) - assert.NoError(tt, err) - - // good request - storeKeyRequest := router.StoreKeyRequest{ - ID: "did:test:me#key-1", - Type: crypto.Ed25519, - Controller: "did:test:me", - PrivateKeyBase58: base58.Encode(privKeyBytes), - } - requestValue := newRequestValue(tt, storeKeyRequest) - req = httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/keys", requestValue) - w = httptest.NewRecorder() - c = newRequestContext(w, req) - keyStoreRouter.StoreKey(c) - assert.True(tt, util.Is2xxResponse(w.Code)) - }) - - t.Run("Test Get Key Details", func(tt *testing.T) { - bolt := setupTestDB(tt) - require.NotEmpty(tt, bolt) - - keyStoreService, _ := testKeyStore(tt, bolt) - w := httptest.NewRecorder() - - // store a valid key - pubKey, privKey, err := crypto.GenerateKeyByKeyType(crypto.Ed25519) - assert.NoError(tt, err) - assert.NotEmpty(tt, privKey) - - privKeyBytes, err := crypto.PrivKeyToBytes(privKey) - assert.NoError(tt, err) - - // good request - keyID := "did:test:me#key-2" - controller := "did:test:me" - storeKeyRequest := router.StoreKeyRequest{ - ID: keyID, - Type: crypto.Ed25519, - Controller: controller, - PrivateKeyBase58: base58.Encode(privKeyBytes), - } - requestValue := newRequestValue(tt, storeKeyRequest) - req := httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/keys", requestValue) - c := newRequestContext(w, req) - keyStoreService.StoreKey(c) - assert.True(tt, util.Is2xxResponse(w.Code)) - - // get it back - w = httptest.NewRecorder() - getReq := httptest.NewRequest(http.MethodGet, fmt.Sprintf("https://ssi-service.com/v1/keys/%s", keyID), nil) - c = newRequestContextWithParams(w, getReq, map[string]string{"id": keyID}) - keyStoreService.GetKeyDetails(c) - assert.True(tt, util.Is2xxResponse(w.Code)) - - var resp router.GetKeyDetailsResponse - err = json.NewDecoder(w.Body).Decode(&resp) - assert.NoError(tt, err) - assert.Equal(tt, keyID, resp.ID) - assert.Equal(tt, controller, resp.Controller) - assert.Equal(tt, crypto.Ed25519, resp.Type) - - gotPubKey, err := resp.PublicKeyJWK.ToPublicKey() - assert.NoError(tt, err) - wantPubKey, err := crypto.PubKeyToBytes(pubKey) - assert.NoError(tt, err) - assert.NotEmpty(tt, wantPubKey, gotPubKey) - }) + + for _, test := range testutil.TestDatabases { + t.Run(test.Name, func(t *testing.T) { + t.Run("Test Store Key", func(tt *testing.T) { + db := test.ServiceStorage(t) + require.NotEmpty(tt, db) + + keyStoreRouter, _ := testKeyStore(tt, db) + w := httptest.NewRecorder() + + // bad key type + badKeyStoreRequest := router.StoreKeyRequest{ + ID: "test-kid", + Type: "bad", + Controller: "me", + PrivateKeyBase58: "bad", + } + badRequestValue := newRequestValue(tt, badKeyStoreRequest) + req := httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/keys", badRequestValue) + + c := newRequestContext(w, req) + keyStoreRouter.StoreKey(c) + assert.Contains(tt, w.Body.String(), "unsupported key type: bad") + + // store a valid key + _, privKey, err := crypto.GenerateKeyByKeyType(crypto.Ed25519) + assert.NoError(tt, err) + assert.NotEmpty(tt, privKey) + + privKeyBytes, err := crypto.PrivKeyToBytes(privKey) + assert.NoError(tt, err) + + // good request + storeKeyRequest := router.StoreKeyRequest{ + ID: "did:test:me#key-1", + Type: crypto.Ed25519, + Controller: "did:test:me", + PrivateKeyBase58: base58.Encode(privKeyBytes), + } + requestValue := newRequestValue(tt, storeKeyRequest) + req = httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/keys", requestValue) + w = httptest.NewRecorder() + c = newRequestContext(w, req) + keyStoreRouter.StoreKey(c) + assert.True(tt, util.Is2xxResponse(w.Code)) + }) + + t.Run("Test Get Key Details", func(tt *testing.T) { + db := test.ServiceStorage(t) + require.NotEmpty(tt, db) + + keyStoreService, _ := testKeyStore(tt, db) + w := httptest.NewRecorder() + + // store a valid key + pubKey, privKey, err := crypto.GenerateKeyByKeyType(crypto.Ed25519) + assert.NoError(tt, err) + assert.NotEmpty(tt, privKey) + + privKeyBytes, err := crypto.PrivKeyToBytes(privKey) + assert.NoError(tt, err) + + // good request + keyID := "did:test:me#key-2" + controller := "did:test:me" + storeKeyRequest := router.StoreKeyRequest{ + ID: keyID, + Type: crypto.Ed25519, + Controller: controller, + PrivateKeyBase58: base58.Encode(privKeyBytes), + } + requestValue := newRequestValue(tt, storeKeyRequest) + req := httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/keys", requestValue) + c := newRequestContext(w, req) + keyStoreService.StoreKey(c) + assert.True(tt, util.Is2xxResponse(w.Code)) + + // get it back + w = httptest.NewRecorder() + getReq := httptest.NewRequest(http.MethodGet, fmt.Sprintf("https://ssi-service.com/v1/keys/%s", keyID), nil) + c = newRequestContextWithParams(w, getReq, map[string]string{"id": keyID}) + keyStoreService.GetKeyDetails(c) + assert.True(tt, util.Is2xxResponse(w.Code)) + + var resp router.GetKeyDetailsResponse + err = json.NewDecoder(w.Body).Decode(&resp) + assert.NoError(tt, err) + assert.Equal(tt, keyID, resp.ID) + assert.Equal(tt, controller, resp.Controller) + assert.Equal(tt, crypto.Ed25519, resp.Type) + + gotPubKey, err := resp.PublicKeyJWK.ToPublicKey() + assert.NoError(tt, err) + wantPubKey, err := crypto.PubKeyToBytes(pubKey) + assert.NoError(tt, err) + assert.NotEmpty(tt, wantPubKey, gotPubKey) + }) + }) + } } diff --git a/pkg/server/server_manifest_test.go b/pkg/server/server_manifest_test.go index 04c153ce8..2d6e3e5dd 100644 --- a/pkg/server/server_manifest_test.go +++ b/pkg/server/server_manifest_test.go @@ -29,1083 +29,1088 @@ import ( manifestsvc "github.com/tbd54566975/ssi-service/pkg/service/manifest/model" "github.com/tbd54566975/ssi-service/pkg/service/operation/storage" "github.com/tbd54566975/ssi-service/pkg/service/schema" + "github.com/tbd54566975/ssi-service/pkg/testutil" ) func TestManifestAPI(t *testing.T) { - t.Run("Test Create Manifest", func(tt *testing.T) { - bolt := setupTestDB(tt) - require.NotEmpty(tt, bolt) - - keyStoreService := testKeyStoreService(tt, bolt) - didService := testDIDService(tt, bolt, keyStoreService) - schemaService := testSchemaService(tt, bolt, keyStoreService, didService) - credentialService := testCredentialService(tt, bolt, keyStoreService, didService, schemaService) - manifestRouter, manifestService := testManifest(tt, bolt, keyStoreService, didService, credentialService) - - // missing required field: Manifest - var badManifestRequest router.CreateManifestRequest - badRequestValue := newRequestValue(tt, badManifestRequest) - req := httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/manifests", badRequestValue) - w := httptest.NewRecorder() - - c := newRequestContext(w, req) - manifestRouter.CreateManifest(c) - assert.Contains(tt, w.Body.String(), "invalid create manifest request") - - // reset the http recorder - w = httptest.NewRecorder() - - // create an issuer - 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 for the creds to be issued against - kid := issuerDID.DID.VerificationMethod[0].ID - createdSchema, err := schemaService.CreateSchema(context.Background(), schema.CreateSchemaRequest{Issuer: issuerDID.DID.ID, IssuerKID: kid, Name: "license schema", Schema: getLicenseApplicationSchema()}) - assert.NoError(tt, err) - assert.NotEmpty(tt, createdSchema) - - // good request - createManifestRequest := getValidCreateManifestRequest(issuerDID.DID.ID, issuerDID.DID.VerificationMethod[0].ID, createdSchema.ID) - - requestValue := newRequestValue(tt, createManifestRequest) - req = httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/manifests", requestValue) - c = newRequestContext(w, req) - manifestRouter.CreateManifest(c) - assert.True(tt, util.Is2xxResponse(w.Code)) - - var resp router.CreateManifestResponse - err = json.NewDecoder(w.Body).Decode(&resp) - assert.NoError(tt, err) - assert.NotEmpty(tt, resp.Manifest) - assert.Equal(tt, resp.Manifest.Issuer.ID, issuerDID.DID.ID) - - // create a credential manifest request - manifestRequestRequest := getValidManifestRequestRequest(issuerDID, kid, resp.Manifest) - requestValue = newRequestValue(tt, manifestRequestRequest) - req = httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/manifests/requests", requestValue) - c = newRequestContext(w, req) - manifestRouter.CreateRequest(c) - assert.True(tt, util.Is2xxResponse(w.Code)) - - var reqResp router.CreateManifestRequestResponse - err = json.NewDecoder(w.Body).Decode(&reqResp) - assert.NoError(tt, err) - assert.NotEmpty(tt, reqResp.Request) - - // verify the manifest - verificationResponse, err := manifestService.VerifyManifest(context.Background(), manifestsvc.VerifyManifestRequest{ManifestJWT: reqResp.Request.CredentialManifestJWT}) - assert.NoError(tt, err) - assert.NotEmpty(tt, verificationResponse) - assert.True(tt, verificationResponse.Verified) - }) - - t.Run("Test Get Manifest By ID", func(tt *testing.T) { - bolt := setupTestDB(tt) - require.NotEmpty(tt, bolt) - - keyStoreService := testKeyStoreService(tt, bolt) - didService := testDIDService(tt, bolt, keyStoreService) - schemaService := testSchemaService(tt, bolt, keyStoreService, didService) - credentialService := testCredentialService(tt, bolt, keyStoreService, didService, schemaService) - manifestRouter, _ := testManifest(tt, bolt, keyStoreService, didService, credentialService) - - w := httptest.NewRecorder() - - // get a manifest that doesn't exit - req := httptest.NewRequest(http.MethodGet, "https://ssi-service.com/v1/manifests/bad", nil) - c := newRequestContext(w, req) - manifestRouter.GetManifest(c) - assert.Contains(tt, w.Body.String(), "cannot get manifest without ID parameter") - - // reset recorder between calls - w = httptest.NewRecorder() - - // get a manifest with an invalid id parameter - req = httptest.NewRequest(http.MethodGet, "https://ssi-service.com/v1/manifests/bad", nil) - c = newRequestContextWithParams(w, req, map[string]string{"id": "bad"}) - manifestRouter.GetManifest(c) - assert.Contains(tt, w.Body.String(), "could not get manifest with id: bad") - - // reset recorder between calls - w = httptest.NewRecorder() - - // create an issuer - 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 for the creds to be issued against - kid := issuerDID.DID.VerificationMethod[0].ID - createdSchema, err := schemaService.CreateSchema(context.Background(), schema.CreateSchemaRequest{Issuer: issuerDID.DID.ID, IssuerKID: kid, Name: "license schema", Schema: getLicenseApplicationSchema()}) - assert.NoError(tt, err) - assert.NotEmpty(tt, createdSchema) - - // good request - createManifestRequest := getValidCreateManifestRequest(issuerDID.DID.ID, issuerDID.DID.VerificationMethod[0].ID, createdSchema.ID) - - requestValue := newRequestValue(tt, createManifestRequest) - req = httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/manifests", requestValue) - c = newRequestContext(w, req) - manifestRouter.CreateManifest(c) - assert.True(tt, util.Is2xxResponse(w.Code)) - - var resp router.CreateManifestResponse - err = json.NewDecoder(w.Body).Decode(&resp) - assert.NoError(tt, err) - - // get manifest by id - req = httptest.NewRequest(http.MethodGet, fmt.Sprintf("https://ssi-service.com/v1/manifests/%s", resp.Manifest.ID), nil) - c = newRequestContextWithParams(w, req, map[string]string{"id": resp.Manifest.ID}) - manifestRouter.GetManifest(c) - assert.True(tt, util.Is2xxResponse(w.Code)) - - var getManifestResp router.ListManifestResponse - err = json.NewDecoder(w.Body).Decode(&getManifestResp) - assert.NoError(tt, err) - assert.NotEmpty(tt, getManifestResp) - assert.Equal(tt, resp.Manifest.ID, getManifestResp.ID) - }) - - t.Run("Test Get Manifests", func(tt *testing.T) { - bolt := setupTestDB(tt) - require.NotEmpty(tt, bolt) - - keyStoreService := testKeyStoreService(tt, bolt) - didService := testDIDService(tt, bolt, keyStoreService) - schemaService := testSchemaService(tt, bolt, keyStoreService, didService) - credentialService := testCredentialService(tt, bolt, keyStoreService, didService, schemaService) - manifestRouter, manifestService := testManifest(tt, bolt, keyStoreService, didService, credentialService) - - w := httptest.NewRecorder() - - // create an issuer - 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 for the creds to be issued against - kid := issuerDID.DID.VerificationMethod[0].ID - createdSchema, err := schemaService.CreateSchema(context.Background(), schema.CreateSchemaRequest{Issuer: issuerDID.DID.ID, IssuerKID: kid, Name: "license schema", Schema: getLicenseApplicationSchema()}) - assert.NoError(tt, err) - assert.NotEmpty(tt, createdSchema) - - // good request - createManifestRequest := getValidCreateManifestRequest(issuerDID.DID.ID, issuerDID.DID.VerificationMethod[0].ID, createdSchema.ID) - - requestValue := newRequestValue(tt, createManifestRequest) - req := httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/manifests", requestValue) - c := newRequestContext(w, req) - manifestRouter.CreateManifest(c) - assert.True(tt, util.Is2xxResponse(w.Code)) - - var resp router.CreateManifestResponse - err = json.NewDecoder(w.Body).Decode(&resp) - assert.NoError(tt, err) - - // list all manifests - req = httptest.NewRequest(http.MethodGet, "https://ssi-service.com/v1/manifests", nil) - c = newRequestContext(w, req) - manifestRouter.ListManifests(c) - assert.True(tt, util.Is2xxResponse(w.Code)) - - var getManifestsResp router.ListManifestsResponse - err = json.NewDecoder(w.Body).Decode(&getManifestsResp) - assert.NoError(tt, err) - assert.NotEmpty(tt, getManifestsResp) - assert.Len(tt, getManifestsResp.Manifests, 1) - assert.Equal(tt, resp.Manifest.ID, getManifestsResp.Manifests[0].ID) - - // create a credential manifest request - manifestRequestRequest := getValidManifestRequestRequest(issuerDID, kid, resp.Manifest) - requestValue = newRequestValue(tt, manifestRequestRequest) - w = httptest.NewRecorder() - req = httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/manifests/requests", requestValue) - c = newRequestContext(w, req) - manifestRouter.CreateRequest(c) - assert.True(tt, util.Is2xxResponse(w.Code)) - - var reqResp router.CreateManifestRequestResponse - err = json.NewDecoder(w.Body).Decode(&reqResp) - assert.NoError(tt, err) - assert.NotEmpty(tt, reqResp.Request) - - // list the manifest requests - req = httptest.NewRequest(http.MethodGet, "https://ssi-service.com/v1/manifests/requests", nil) - c = newRequestContext(w, req) - manifestRouter.ListRequests(c) - assert.True(tt, util.Is2xxResponse(w.Code)) - - var getManifestReqsResp router.ListManifestRequestsResponse - err = json.NewDecoder(w.Body).Decode(&getManifestReqsResp) - assert.NoError(tt, err) - assert.NotEmpty(tt, getManifestReqsResp) - assert.Len(tt, getManifestReqsResp.Requests, 1) - assert.Equal(tt, reqResp.Request.ID, getManifestReqsResp.Requests[0].ID) - - // verify each manifest request - for _, m := range getManifestReqsResp.Requests { - verificationResponse, err := manifestService.VerifyManifest(context.Background(), manifestsvc.VerifyManifestRequest{ManifestJWT: m.CredentialManifestJWT}) - assert.NoError(tt, err) - assert.NotEmpty(tt, verificationResponse) - assert.True(tt, verificationResponse.Verified) - } - }) - - t.Run("Test Delete Manifest", func(tt *testing.T) { - bolt := setupTestDB(tt) - require.NotEmpty(tt, bolt) - - keyStoreService := testKeyStoreService(tt, bolt) - didService := testDIDService(tt, bolt, keyStoreService) - schemaService := testSchemaService(tt, bolt, keyStoreService, didService) - credentialService := testCredentialService(tt, bolt, keyStoreService, didService, schemaService) - manifestRouter, _ := testManifest(tt, bolt, keyStoreService, didService, credentialService) - - // create an issuer - 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 for the creds to be issued against - kid := issuerDID.DID.VerificationMethod[0].ID - createdSchema, err := schemaService.CreateSchema(context.Background(), schema.CreateSchemaRequest{Issuer: issuerDID.DID.ID, IssuerKID: kid, Name: "license schema", Schema: getLicenseApplicationSchema()}) - assert.NoError(tt, err) - assert.NotEmpty(tt, createdSchema) - - // good request - createManifestRequest := getValidCreateManifestRequest(issuerDID.DID.ID, issuerDID.DID.VerificationMethod[0].ID, createdSchema.ID) - - requestValue := newRequestValue(tt, createManifestRequest) - req := httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/manifests", requestValue) - w := httptest.NewRecorder() - c := newRequestContext(w, req) - manifestRouter.CreateManifest(c) - assert.True(tt, util.Is2xxResponse(w.Code)) - - var resp router.CreateManifestResponse - err = json.NewDecoder(w.Body).Decode(&resp) - assert.NoError(tt, err) - - w = httptest.NewRecorder() - - // get credential by id - req = httptest.NewRequest(http.MethodGet, fmt.Sprintf("https://ssi-service.com/v1/manifests/%s", resp.Manifest.ID), nil) - c = newRequestContextWithParams(w, req, map[string]string{"id": resp.Manifest.ID}) - manifestRouter.GetManifest(c) - assert.True(tt, util.Is2xxResponse(w.Code)) - - var getManifestResp router.GetCredentialResponse - err = json.NewDecoder(w.Body).Decode(&getManifestResp) - assert.NoError(tt, err) - assert.NotEmpty(tt, getManifestResp) - assert.Equal(tt, resp.Manifest.ID, getManifestResp.ID) - - w = httptest.NewRecorder() - - // delete it - req = httptest.NewRequest(http.MethodDelete, fmt.Sprintf("https://ssi-service.com/v1/manifests/%s", resp.Manifest.ID), nil) - c = newRequestContextWithParams(w, req, map[string]string{"id": resp.Manifest.ID}) - manifestRouter.DeleteManifest(c) - assert.True(tt, util.Is2xxResponse(w.Code)) - - w = httptest.NewRecorder() - - // get it back - req = httptest.NewRequest(http.MethodGet, fmt.Sprintf("https://ssi-service.com/v1/manifests/%s", resp.Manifest.ID), nil) - c = newRequestContextWithParams(w, req, map[string]string{"id": resp.Manifest.ID}) - manifestRouter.GetManifest(c) - assert.Contains(tt, w.Body.String(), fmt.Sprintf("could not get manifest with id: %s", resp.Manifest.ID)) - }) - - t.Run("Submit Application With Issuance Template", func(tt *testing.T) { - bolt := setupTestDB(tt) - require.NotEmpty(tt, bolt) - - keyStoreService := testKeyStoreService(tt, bolt) - issuanceService := testIssuanceService(tt, bolt) - didService := testDIDService(tt, bolt, keyStoreService) - schemaService := testSchemaService(tt, bolt, keyStoreService, didService) - credentialService := testCredentialService(tt, bolt, keyStoreService, didService, schemaService) - manifestRouter, manifestSvc := testManifest(tt, bolt, keyStoreService, didService, credentialService) - - // create an issuer - issuerDID, err := didService.CreateDIDByMethod(context.Background(), did.CreateDIDRequest{ - Method: didsdk.KeyMethod, - KeyType: crypto.Ed25519, - }) - assert.NoError(tt, err) - assert.NotEmpty(tt, issuerDID) - - // create an applicant - applicantPrivKey, applicantDIDKey, err := key.GenerateDIDKey(crypto.Ed25519) - assert.NoError(tt, err) - assert.NotEmpty(tt, applicantPrivKey) - assert.NotEmpty(tt, applicantDIDKey) - - applicantDID, err := applicantDIDKey.Expand() - assert.NoError(tt, err) - assert.NotEmpty(tt, applicantDID) - - // create a schema for the creds to be issued against, needed for the application - kid := issuerDID.DID.VerificationMethod[0].ID - licenseApplicationSchema, err := schemaService.CreateSchema( - context.Background(), - schema.CreateSchemaRequest{Issuer: issuerDID.DID.ID, IssuerKID: kid, Name: "license application schema", Schema: getLicenseApplicationSchema()}) - assert.NoError(tt, err) - assert.NotEmpty(tt, licenseApplicationSchema) - - // create a second schema for the creds to be issued after the application is approved - licenseSchema, err := schemaService.CreateSchema( - context.Background(), - schema.CreateSchemaRequest{Issuer: issuerDID.DID.ID, IssuerKID: kid, Name: "license schema", Schema: getLicenseSchema()}) - assert.NoError(tt, err) - assert.NotEmpty(tt, licenseSchema) - - // issue a credential against the schema to the subject, from the issuer - createdCred, err := credentialService.CreateCredential( - context.Background(), - credential.CreateCredentialRequest{ - Issuer: issuerDID.DID.ID, - IssuerKID: kid, - Subject: applicantDID.ID, - SchemaID: licenseApplicationSchema.ID, - Data: map[string]any{ - "licenseType": "Class D", - "firstName": "Tester", - "lastName": "McTest", - }, + for _, test := range testutil.TestDatabases { + t.Run(test.Name, func(t *testing.T) { + t.Run("Test Create Manifest", func(tt *testing.T) { + db := test.ServiceStorage(t) + require.NotEmpty(tt, db) + + keyStoreService := testKeyStoreService(tt, db) + didService := testDIDService(tt, db, keyStoreService) + schemaService := testSchemaService(tt, db, keyStoreService, didService) + credentialService := testCredentialService(tt, db, keyStoreService, didService, schemaService) + manifestRouter, manifestService := testManifest(tt, db, keyStoreService, didService, credentialService) + + // missing required field: Manifest + var badManifestRequest router.CreateManifestRequest + badRequestValue := newRequestValue(tt, badManifestRequest) + req := httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/manifests", badRequestValue) + w := httptest.NewRecorder() + + c := newRequestContext(w, req) + manifestRouter.CreateManifest(c) + assert.Contains(tt, w.Body.String(), "invalid create manifest request") + + // reset the http recorder + w = httptest.NewRecorder() + + // create an issuer + 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 for the creds to be issued against + kid := issuerDID.DID.VerificationMethod[0].ID + createdSchema, err := schemaService.CreateSchema(context.Background(), schema.CreateSchemaRequest{Issuer: issuerDID.DID.ID, IssuerKID: kid, Name: "license schema", Schema: getLicenseApplicationSchema()}) + assert.NoError(tt, err) + assert.NotEmpty(tt, createdSchema) + + // good request + createManifestRequest := getValidCreateManifestRequest(issuerDID.DID.ID, issuerDID.DID.VerificationMethod[0].ID, createdSchema.ID) + + requestValue := newRequestValue(tt, createManifestRequest) + req = httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/manifests", requestValue) + c = newRequestContext(w, req) + manifestRouter.CreateManifest(c) + assert.True(tt, util.Is2xxResponse(w.Code)) + + var resp router.CreateManifestResponse + err = json.NewDecoder(w.Body).Decode(&resp) + assert.NoError(tt, err) + assert.NotEmpty(tt, resp.Manifest) + assert.Equal(tt, resp.Manifest.Issuer.ID, issuerDID.DID.ID) + + // create a credential manifest request + manifestRequestRequest := getValidManifestRequestRequest(issuerDID, kid, resp.Manifest) + requestValue = newRequestValue(tt, manifestRequestRequest) + req = httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/manifests/requests", requestValue) + c = newRequestContext(w, req) + manifestRouter.CreateRequest(c) + assert.True(tt, util.Is2xxResponse(w.Code)) + + var reqResp router.CreateManifestRequestResponse + err = json.NewDecoder(w.Body).Decode(&reqResp) + assert.NoError(tt, err) + assert.NotEmpty(tt, reqResp.Request) + + // verify the manifest + verificationResponse, err := manifestService.VerifyManifest(context.Background(), manifestsvc.VerifyManifestRequest{ManifestJWT: reqResp.Request.CredentialManifestJWT}) + assert.NoError(tt, err) + assert.NotEmpty(tt, verificationResponse) + assert.True(tt, verificationResponse.Verified) }) - assert.NoError(tt, err) - assert.NotEmpty(tt, createdCred) - - // create a manifest with the schema we'll be issuing against after reviewing applications - createManifestRequest := getValidCreateManifestRequest(issuerDID.DID.ID, issuerDID.DID.VerificationMethod[0].ID, licenseSchema.ID) - requestValue := newRequestValue(tt, createManifestRequest) - req := httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/manifests", requestValue) - w := httptest.NewRecorder() - c := newRequestContext(w, req) - manifestRouter.CreateManifest(c) - assert.True(tt, util.Is2xxResponse(w.Code)) - - var resp router.CreateManifestResponse - err = json.NewDecoder(w.Body).Decode(&resp) - assert.NoError(tt, err) - - m := resp.Manifest - assert.NotEmpty(tt, m) - assert.Equal(tt, m.Issuer.ID, issuerDID.DID.ID) - - // good application request - container := []credmodel.Container{{CredentialJWT: createdCred.CredentialJWT}} - applicationRequest := getValidApplicationRequest(m.ID, m.PresentationDefinition.ID, m.PresentationDefinition.InputDescriptors[0].ID, container) - - // sign application - signer, err := keyaccess.NewJWKKeyAccess(applicantDID.ID, applicantDID.VerificationMethod[0].ID, applicantPrivKey) - assert.NoError(tt, err) - signed, err := signer.SignJSON(applicationRequest) - assert.NoError(tt, err) - - expiryDateTime := time.Date(2022, 10, 31, 0, 0, 0, 0, time.UTC) - mockClock := clock.NewMock() - manifestSvc.Clock = mockClock - mockClock.Set(expiryDateTime) - expiryDuration := 5 * time.Second - issuanceTemplate, err := issuanceService.CreateIssuanceTemplate(context.Background(), - getValidIssuanceTemplateRequest(m, issuerDID, licenseSchema.ID, expiryDateTime, expiryDuration)) - assert.NoError(tt, err) - assert.NotEmpty(tt, issuanceTemplate) - - applicationRequestValue := newRequestValue(tt, router.SubmitApplicationRequest{ApplicationJWT: *signed}) - req = httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/manifests/applications", applicationRequestValue) - c = newRequestContext(w, req) - manifestRouter.SubmitApplication(c) - assert.True(tt, util.Is2xxResponse(w.Code)) - - var op router.Operation - err = json.NewDecoder(w.Body).Decode(&op) - assert.NoError(tt, err) - assert.True(tt, op.Done) - - var appResp router.SubmitApplicationResponse - respData, err := json.Marshal(op.Result.Response) - assert.NoError(tt, err) - err = json.Unmarshal(respData, &appResp) - assert.NoError(tt, err) - assert.Len(tt, appResp.Credentials, 2, "each output_descriptor in the definition should result in a credential") - - _, _, vc, err := credsdk.ToCredential(appResp.Credentials[0]) - assert.NoError(tt, err) - expectedSubject := credsdk.CredentialSubject{ - "id": applicantDID.ID, - "state": "CA", - "firstName": "Tester", - "lastName": "McTest", - } - assert.Equal(tt, expectedSubject, vc.CredentialSubject) - assert.Equal(tt, time.Date(2022, 10, 31, 0, 0, 0, 0, time.UTC).Format(time.RFC3339), vc.ExpirationDate) - assert.Equal(tt, licenseSchema.ID, vc.CredentialSchema.ID) - assert.Empty(tt, vc.CredentialStatus) - - _, _, vc2, err := credsdk.ToCredential(appResp.Credentials[1]) - assert.NoError(tt, err) - expectedSubject = credsdk.CredentialSubject{ - "id": applicantDID.ID, - "firstName": "Tester", - "lastName": "McTest", - "state": "NY", - "someCrazyObject": map[string]any{ - "foo": 123., - "bar": false, - "baz": []any{ - "yay", 123., nil, - }, - }, - } - assert.Equal(tt, expectedSubject, vc2.CredentialSubject) - assert.Equal(tt, - time.Date(2022, 10, 31, 0, 0, 5, 0, time.UTC).Format(time.RFC3339), - vc2.ExpirationDate, - ) - assert.Equal(tt, licenseSchema.ID, vc2.CredentialSchema.ID) - assert.NotEmpty(tt, vc2.CredentialStatus) - }) - - t.Run("Test Submit Application with multiple outputs and overrides", func(tt *testing.T) { - bolt := setupTestDB(tt) - require.NotEmpty(tt, bolt) - - keyStoreService := testKeyStoreService(tt, bolt) - didService := testDIDService(tt, bolt, keyStoreService) - schemaService := testSchemaService(tt, bolt, keyStoreService, didService) - credentialService := testCredentialService(tt, bolt, keyStoreService, didService, schemaService) - manifestRouter, _ := testManifest(tt, bolt, keyStoreService, didService, credentialService) - - // missing required field: Application - badManifestRequest := router.SubmitApplicationRequest{ApplicationJWT: "bad"} - badRequestValue := newRequestValue(tt, badManifestRequest) - req := httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/manifests/applications", badRequestValue) - w := httptest.NewRecorder() - - c := newRequestContext(w, req) - manifestRouter.SubmitApplication(c) - assert.Contains(tt, w.Body.String(), "invalid submit application request") - - // create an issuer - issuerDID, err := didService.CreateDIDByMethod(context.Background(), did.CreateDIDRequest{ - Method: didsdk.KeyMethod, - KeyType: crypto.Ed25519, - }) - assert.NoError(tt, err) - assert.NotEmpty(tt, issuerDID) - - // create an applicant - applicantPrivKey, applicantDIDKey, err := key.GenerateDIDKey(crypto.Ed25519) - assert.NoError(tt, err) - assert.NotEmpty(tt, applicantPrivKey) - assert.NotEmpty(tt, applicantDIDKey) - - applicantDID, err := applicantDIDKey.Expand() - assert.NoError(tt, err) - assert.NotEmpty(tt, applicantDID) - - // create a schema for the creds to be issued against, needed for the application - kid := issuerDID.DID.VerificationMethod[0].ID - licenseApplicationSchema, err := schemaService.CreateSchema( - context.Background(), - schema.CreateSchemaRequest{Issuer: issuerDID.DID.ID, IssuerKID: kid, Name: "license application schema", Schema: getLicenseApplicationSchema()}) - assert.NoError(tt, err) - assert.NotEmpty(tt, licenseApplicationSchema) - - // create a second schema for the creds to be issued after the application is approved - licenseSchema, err := schemaService.CreateSchema( - context.Background(), - schema.CreateSchemaRequest{Issuer: issuerDID.DID.ID, IssuerKID: kid, Name: "license schema", Schema: getLicenseSchema()}) - assert.NoError(tt, err) - assert.NotEmpty(tt, licenseSchema) - - // issue a credential against the schema to the subject, from the issuer - createdCred, err := credentialService.CreateCredential(context.Background(), credential.CreateCredentialRequest{ - Issuer: issuerDID.DID.ID, - IssuerKID: kid, - Subject: applicantDID.ID, - SchemaID: licenseApplicationSchema.ID, - Data: map[string]any{"licenseType": "Class D"}, - }) - assert.NoError(tt, err) - assert.NotEmpty(tt, createdCred) - - // good request to create a manifest - createManifestRequest := getValidCreateManifestRequest(issuerDID.DID.ID, issuerDID.DID.VerificationMethod[0].ID, licenseSchema.ID) - w = httptest.NewRecorder() - requestValue := newRequestValue(tt, createManifestRequest) - req = httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/manifests", requestValue) - c = newRequestContext(w, req) - manifestRouter.CreateManifest(c) - assert.True(tt, util.Is2xxResponse(w.Code)) - - var resp router.CreateManifestResponse - err = json.NewDecoder(w.Body).Decode(&resp) - assert.NoError(tt, err) - - m := resp.Manifest - assert.NotEmpty(tt, m) - assert.Equal(tt, m.Issuer.ID, issuerDID.DID.ID) - - // good application request - container := []credmodel.Container{{CredentialJWT: createdCred.CredentialJWT}} - applicationRequest := getValidApplicationRequest(m.ID, m.PresentationDefinition.ID, m.PresentationDefinition.InputDescriptors[0].ID, container) - - // sign application - signer, err := keyaccess.NewJWKKeyAccess(applicantDID.ID, applicantDID.VerificationMethod[0].ID, applicantPrivKey) - assert.NoError(tt, err) - signed, err := signer.SignJSON(applicationRequest) - assert.NoError(tt, err) - - w = httptest.NewRecorder() - - applicationRequestValue := newRequestValue(tt, router.SubmitApplicationRequest{ApplicationJWT: *signed}) - req = httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/manifests/applications", applicationRequestValue) - c = newRequestContext(w, req) - manifestRouter.SubmitApplication(c) - assert.NoError(tt, err) - - var op router.Operation - err = json.NewDecoder(w.Body).Decode(&op) - assert.NoError(tt, err) - - assert.False(tt, op.Done) - assert.Contains(tt, op.ID, "credentials/responses/") - - // review application - expireAt := time.Date(2025, 10, 32, 0, 0, 0, 0, time.UTC) - reviewApplicationRequestValue := newRequestValue(tt, router.ReviewApplicationRequest{ - Approved: true, - Reason: "I'm the almighty approver", - CredentialOverrides: map[string]manifestsvc.CredentialOverride{ - "drivers-license-ca": { - Data: map[string]any{ - "firstName": "John", - "lastName": "Doe", - "state": "CA", - "looks": "pretty darn handsome", - }, - Expiry: &expireAt, - Revocable: true, - }, - "drivers-license-ny": { - Data: map[string]any{ - "firstName": "John", - "lastName": "Doe", - "state": "NY", - "looks": "even handsomer", + + t.Run("Test Get Manifest By ID", func(tt *testing.T) { + db := test.ServiceStorage(t) + require.NotEmpty(tt, db) + + keyStoreService := testKeyStoreService(tt, db) + didService := testDIDService(tt, db, keyStoreService) + schemaService := testSchemaService(tt, db, keyStoreService, didService) + credentialService := testCredentialService(tt, db, keyStoreService, didService, schemaService) + manifestRouter, _ := testManifest(tt, db, keyStoreService, didService, credentialService) + + w := httptest.NewRecorder() + + // get a manifest that doesn't exit + req := httptest.NewRequest(http.MethodGet, "https://ssi-service.com/v1/manifests/bad", nil) + c := newRequestContext(w, req) + manifestRouter.GetManifest(c) + assert.Contains(tt, w.Body.String(), "cannot get manifest without ID parameter") + + // reset recorder between calls + w = httptest.NewRecorder() + + // get a manifest with an invalid id parameter + req = httptest.NewRequest(http.MethodGet, "https://ssi-service.com/v1/manifests/bad", nil) + c = newRequestContextWithParams(w, req, map[string]string{"id": "bad"}) + manifestRouter.GetManifest(c) + assert.Contains(tt, w.Body.String(), "could not get manifest with id: bad") + + // reset recorder between calls + w = httptest.NewRecorder() + + // create an issuer + 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 for the creds to be issued against + kid := issuerDID.DID.VerificationMethod[0].ID + createdSchema, err := schemaService.CreateSchema(context.Background(), schema.CreateSchemaRequest{Issuer: issuerDID.DID.ID, IssuerKID: kid, Name: "license schema", Schema: getLicenseApplicationSchema()}) + assert.NoError(tt, err) + assert.NotEmpty(tt, createdSchema) + + // good request + createManifestRequest := getValidCreateManifestRequest(issuerDID.DID.ID, issuerDID.DID.VerificationMethod[0].ID, createdSchema.ID) + + requestValue := newRequestValue(tt, createManifestRequest) + req = httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/manifests", requestValue) + c = newRequestContext(w, req) + manifestRouter.CreateManifest(c) + assert.True(tt, util.Is2xxResponse(w.Code)) + + var resp router.CreateManifestResponse + err = json.NewDecoder(w.Body).Decode(&resp) + assert.NoError(tt, err) + + // get manifest by id + req = httptest.NewRequest(http.MethodGet, fmt.Sprintf("https://ssi-service.com/v1/manifests/%s", resp.Manifest.ID), nil) + c = newRequestContextWithParams(w, req, map[string]string{"id": resp.Manifest.ID}) + manifestRouter.GetManifest(c) + assert.True(tt, util.Is2xxResponse(w.Code)) + + var getManifestResp router.ListManifestResponse + err = json.NewDecoder(w.Body).Decode(&getManifestResp) + assert.NoError(tt, err) + assert.NotEmpty(tt, getManifestResp) + assert.Equal(tt, resp.Manifest.ID, getManifestResp.ID) + }) + + t.Run("Test Get Manifests", func(tt *testing.T) { + db := test.ServiceStorage(t) + require.NotEmpty(tt, db) + + keyStoreService := testKeyStoreService(tt, db) + didService := testDIDService(tt, db, keyStoreService) + schemaService := testSchemaService(tt, db, keyStoreService, didService) + credentialService := testCredentialService(tt, db, keyStoreService, didService, schemaService) + manifestRouter, manifestService := testManifest(tt, db, keyStoreService, didService, credentialService) + + w := httptest.NewRecorder() + + // create an issuer + 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 for the creds to be issued against + kid := issuerDID.DID.VerificationMethod[0].ID + createdSchema, err := schemaService.CreateSchema(context.Background(), schema.CreateSchemaRequest{Issuer: issuerDID.DID.ID, IssuerKID: kid, Name: "license schema", Schema: getLicenseApplicationSchema()}) + assert.NoError(tt, err) + assert.NotEmpty(tt, createdSchema) + + // good request + createManifestRequest := getValidCreateManifestRequest(issuerDID.DID.ID, issuerDID.DID.VerificationMethod[0].ID, createdSchema.ID) + + requestValue := newRequestValue(tt, createManifestRequest) + req := httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/manifests", requestValue) + c := newRequestContext(w, req) + manifestRouter.CreateManifest(c) + assert.True(tt, util.Is2xxResponse(w.Code)) + + var resp router.CreateManifestResponse + err = json.NewDecoder(w.Body).Decode(&resp) + assert.NoError(tt, err) + + // list all manifests + req = httptest.NewRequest(http.MethodGet, "https://ssi-service.com/v1/manifests", nil) + c = newRequestContext(w, req) + manifestRouter.ListManifests(c) + assert.True(tt, util.Is2xxResponse(w.Code)) + + var getManifestsResp router.ListManifestsResponse + err = json.NewDecoder(w.Body).Decode(&getManifestsResp) + assert.NoError(tt, err) + assert.NotEmpty(tt, getManifestsResp) + assert.Len(tt, getManifestsResp.Manifests, 1) + assert.Equal(tt, resp.Manifest.ID, getManifestsResp.Manifests[0].ID) + + // create a credential manifest request + manifestRequestRequest := getValidManifestRequestRequest(issuerDID, kid, resp.Manifest) + requestValue = newRequestValue(tt, manifestRequestRequest) + w = httptest.NewRecorder() + req = httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/manifests/requests", requestValue) + c = newRequestContext(w, req) + manifestRouter.CreateRequest(c) + assert.True(tt, util.Is2xxResponse(w.Code)) + + var reqResp router.CreateManifestRequestResponse + err = json.NewDecoder(w.Body).Decode(&reqResp) + assert.NoError(tt, err) + assert.NotEmpty(tt, reqResp.Request) + + // list the manifest requests + req = httptest.NewRequest(http.MethodGet, "https://ssi-service.com/v1/manifests/requests", nil) + c = newRequestContext(w, req) + manifestRouter.ListRequests(c) + assert.True(tt, util.Is2xxResponse(w.Code)) + + var getManifestReqsResp router.ListManifestRequestsResponse + err = json.NewDecoder(w.Body).Decode(&getManifestReqsResp) + assert.NoError(tt, err) + assert.NotEmpty(tt, getManifestReqsResp) + assert.Len(tt, getManifestReqsResp.Requests, 1) + assert.Equal(tt, reqResp.Request.ID, getManifestReqsResp.Requests[0].ID) + + // verify each manifest request + for _, m := range getManifestReqsResp.Requests { + verificationResponse, err := manifestService.VerifyManifest(context.Background(), manifestsvc.VerifyManifestRequest{ManifestJWT: m.CredentialManifestJWT}) + assert.NoError(tt, err) + assert.NotEmpty(tt, verificationResponse) + assert.True(tt, verificationResponse.Verified) + } + }) + + t.Run("Test Delete Manifest", func(tt *testing.T) { + db := test.ServiceStorage(t) + require.NotEmpty(tt, db) + + keyStoreService := testKeyStoreService(tt, db) + didService := testDIDService(tt, db, keyStoreService) + schemaService := testSchemaService(tt, db, keyStoreService, didService) + credentialService := testCredentialService(tt, db, keyStoreService, didService, schemaService) + manifestRouter, _ := testManifest(tt, db, keyStoreService, didService, credentialService) + + // create an issuer + 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 for the creds to be issued against + kid := issuerDID.DID.VerificationMethod[0].ID + createdSchema, err := schemaService.CreateSchema(context.Background(), schema.CreateSchemaRequest{Issuer: issuerDID.DID.ID, IssuerKID: kid, Name: "license schema", Schema: getLicenseApplicationSchema()}) + assert.NoError(tt, err) + assert.NotEmpty(tt, createdSchema) + + // good request + createManifestRequest := getValidCreateManifestRequest(issuerDID.DID.ID, issuerDID.DID.VerificationMethod[0].ID, createdSchema.ID) + + requestValue := newRequestValue(tt, createManifestRequest) + req := httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/manifests", requestValue) + w := httptest.NewRecorder() + c := newRequestContext(w, req) + manifestRouter.CreateManifest(c) + assert.True(tt, util.Is2xxResponse(w.Code)) + + var resp router.CreateManifestResponse + err = json.NewDecoder(w.Body).Decode(&resp) + assert.NoError(tt, err) + + w = httptest.NewRecorder() + + // get credential by id + req = httptest.NewRequest(http.MethodGet, fmt.Sprintf("https://ssi-service.com/v1/manifests/%s", resp.Manifest.ID), nil) + c = newRequestContextWithParams(w, req, map[string]string{"id": resp.Manifest.ID}) + manifestRouter.GetManifest(c) + assert.True(tt, util.Is2xxResponse(w.Code)) + + var getManifestResp router.GetCredentialResponse + err = json.NewDecoder(w.Body).Decode(&getManifestResp) + assert.NoError(tt, err) + assert.NotEmpty(tt, getManifestResp) + assert.Equal(tt, resp.Manifest.ID, getManifestResp.ID) + + w = httptest.NewRecorder() + + // delete it + req = httptest.NewRequest(http.MethodDelete, fmt.Sprintf("https://ssi-service.com/v1/manifests/%s", resp.Manifest.ID), nil) + c = newRequestContextWithParams(w, req, map[string]string{"id": resp.Manifest.ID}) + manifestRouter.DeleteManifest(c) + assert.True(tt, util.Is2xxResponse(w.Code)) + + w = httptest.NewRecorder() + + // get it back + req = httptest.NewRequest(http.MethodGet, fmt.Sprintf("https://ssi-service.com/v1/manifests/%s", resp.Manifest.ID), nil) + c = newRequestContextWithParams(w, req, map[string]string{"id": resp.Manifest.ID}) + manifestRouter.GetManifest(c) + assert.Contains(tt, w.Body.String(), fmt.Sprintf("could not get manifest with id: %s", resp.Manifest.ID)) + }) + + t.Run("Submit Application With Issuance Template", func(tt *testing.T) { + db := test.ServiceStorage(t) + require.NotEmpty(tt, db) + + keyStoreService := testKeyStoreService(tt, db) + issuanceService := testIssuanceService(tt, db) + didService := testDIDService(tt, db, keyStoreService) + schemaService := testSchemaService(tt, db, keyStoreService, didService) + credentialService := testCredentialService(tt, db, keyStoreService, didService, schemaService) + manifestRouter, manifestSvc := testManifest(tt, db, keyStoreService, didService, credentialService) + + // create an issuer + issuerDID, err := didService.CreateDIDByMethod(context.Background(), did.CreateDIDRequest{ + Method: didsdk.KeyMethod, + KeyType: crypto.Ed25519, + }) + assert.NoError(tt, err) + assert.NotEmpty(tt, issuerDID) + + // create an applicant + applicantPrivKey, applicantDIDKey, err := key.GenerateDIDKey(crypto.Ed25519) + assert.NoError(tt, err) + assert.NotEmpty(tt, applicantPrivKey) + assert.NotEmpty(tt, applicantDIDKey) + + applicantDID, err := applicantDIDKey.Expand() + assert.NoError(tt, err) + assert.NotEmpty(tt, applicantDID) + + // create a schema for the creds to be issued against, needed for the application + kid := issuerDID.DID.VerificationMethod[0].ID + licenseApplicationSchema, err := schemaService.CreateSchema( + context.Background(), + schema.CreateSchemaRequest{Issuer: issuerDID.DID.ID, IssuerKID: kid, Name: "license application schema", Schema: getLicenseApplicationSchema()}) + assert.NoError(tt, err) + assert.NotEmpty(tt, licenseApplicationSchema) + + // create a second schema for the creds to be issued after the application is approved + licenseSchema, err := schemaService.CreateSchema( + context.Background(), + schema.CreateSchemaRequest{Issuer: issuerDID.DID.ID, IssuerKID: kid, Name: "license schema", Schema: getLicenseSchema()}) + assert.NoError(tt, err) + assert.NotEmpty(tt, licenseSchema) + + // issue a credential against the schema to the subject, from the issuer + createdCred, err := credentialService.CreateCredential( + context.Background(), + credential.CreateCredentialRequest{ + Issuer: issuerDID.DID.ID, + IssuerKID: kid, + Subject: applicantDID.ID, + SchemaID: licenseApplicationSchema.ID, + Data: map[string]any{ + "licenseType": "Class D", + "firstName": "Tester", + "lastName": "McTest", + }, + }) + assert.NoError(tt, err) + assert.NotEmpty(tt, createdCred) + + // create a manifest with the schema we'll be issuing against after reviewing applications + createManifestRequest := getValidCreateManifestRequest(issuerDID.DID.ID, issuerDID.DID.VerificationMethod[0].ID, licenseSchema.ID) + requestValue := newRequestValue(tt, createManifestRequest) + req := httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/manifests", requestValue) + w := httptest.NewRecorder() + c := newRequestContext(w, req) + manifestRouter.CreateManifest(c) + assert.True(tt, util.Is2xxResponse(w.Code)) + + var resp router.CreateManifestResponse + err = json.NewDecoder(w.Body).Decode(&resp) + assert.NoError(tt, err) + + m := resp.Manifest + assert.NotEmpty(tt, m) + assert.Equal(tt, m.Issuer.ID, issuerDID.DID.ID) + + // good application request + container := []credmodel.Container{{CredentialJWT: createdCred.CredentialJWT}} + applicationRequest := getValidApplicationRequest(m.ID, m.PresentationDefinition.ID, m.PresentationDefinition.InputDescriptors[0].ID, container) + + // sign application + signer, err := keyaccess.NewJWKKeyAccess(applicantDID.ID, applicantDID.VerificationMethod[0].ID, applicantPrivKey) + assert.NoError(tt, err) + signed, err := signer.SignJSON(applicationRequest) + assert.NoError(tt, err) + + expiryDateTime := time.Date(2022, 10, 31, 0, 0, 0, 0, time.UTC) + mockClock := clock.NewMock() + manifestSvc.Clock = mockClock + mockClock.Set(expiryDateTime) + expiryDuration := 5 * time.Second + issuanceTemplate, err := issuanceService.CreateIssuanceTemplate(context.Background(), + getValidIssuanceTemplateRequest(m, issuerDID, licenseSchema.ID, expiryDateTime, expiryDuration)) + assert.NoError(tt, err) + assert.NotEmpty(tt, issuanceTemplate) + + applicationRequestValue := newRequestValue(tt, router.SubmitApplicationRequest{ApplicationJWT: *signed}) + req = httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/manifests/applications", applicationRequestValue) + c = newRequestContext(w, req) + manifestRouter.SubmitApplication(c) + assert.True(tt, util.Is2xxResponse(w.Code)) + + var op router.Operation + err = json.NewDecoder(w.Body).Decode(&op) + assert.NoError(tt, err) + assert.True(tt, op.Done) + + var appResp router.SubmitApplicationResponse + respData, err := json.Marshal(op.Result.Response) + assert.NoError(tt, err) + err = json.Unmarshal(respData, &appResp) + assert.NoError(tt, err) + assert.Len(tt, appResp.Credentials, 2, "each output_descriptor in the definition should result in a credential") + + _, _, vc, err := credsdk.ToCredential(appResp.Credentials[0]) + assert.NoError(tt, err) + expectedSubject := credsdk.CredentialSubject{ + "id": applicantDID.ID, + "state": "CA", + "firstName": "Tester", + "lastName": "McTest", + } + assert.Equal(tt, expectedSubject, vc.CredentialSubject) + assert.Equal(tt, time.Date(2022, 10, 31, 0, 0, 0, 0, time.UTC).Format(time.RFC3339), vc.ExpirationDate) + assert.Equal(tt, licenseSchema.ID, vc.CredentialSchema.ID) + assert.Empty(tt, vc.CredentialStatus) + + _, _, vc2, err := credsdk.ToCredential(appResp.Credentials[1]) + assert.NoError(tt, err) + expectedSubject = credsdk.CredentialSubject{ + "id": applicantDID.ID, + "firstName": "Tester", + "lastName": "McTest", + "state": "NY", + "someCrazyObject": map[string]any{ + "foo": 123., + "bar": false, + "baz": []any{ + "yay", 123., nil, + }, }, - Expiry: &expireAt, - Revocable: true, - }, - }, - }) - applicationID := storage.StatusObjectID(op.ID) - req = httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/manifests/applications/"+applicationID+"/review", reviewApplicationRequestValue) - c = newRequestContextWithParams(w, req, map[string]string{"id": applicationID}) - manifestRouter.ReviewApplication(c) - assert.True(tt, util.Is2xxResponse(w.Code)) - - var appResp router.SubmitApplicationResponse - err = json.NewDecoder(w.Body).Decode(&appResp) - assert.NoError(tt, err) - - assert.NotEmpty(tt, appResp.Response) - assert.Equal(tt, resp.Manifest.ID, appResp.Response.ManifestID) - assert.NotEmpty(tt, appResp.Response.Fulfillment) - assert.Len(tt, appResp.Response.Fulfillment.DescriptorMap, 2) - assert.Len(tt, appResp.Credentials, 2) - assert.Empty(tt, appResp.Response.Denial) - - _, _, vc, err := credsdk.ToCredential(appResp.Credentials[0]) - assert.NoError(tt, err) - assert.Equal(tt, credsdk.CredentialSubject{ - "id": applicantDID.ID, - "firstName": "John", - "lastName": "Doe", - "state": "CA", - "looks": "pretty darn handsome", - }, vc.CredentialSubject) - assert.Equal(tt, expireAt.Format(time.RFC3339), vc.ExpirationDate) - assert.NotEmpty(tt, vc.CredentialStatus) - assert.Equal(tt, licenseSchema.ID, vc.CredentialSchema.ID) - }) - - t.Run("Test Denied Application", func(tt *testing.T) { - bolt := setupTestDB(tt) - require.NotEmpty(tt, bolt) - - keyStoreService := testKeyStoreService(tt, bolt) - didService := testDIDService(tt, bolt, keyStoreService) - schemaService := testSchemaService(tt, bolt, keyStoreService, didService) - credentialService := testCredentialService(tt, bolt, keyStoreService, didService, schemaService) - manifestRouter, _ := testManifest(tt, bolt, keyStoreService, didService, credentialService) - - // missing required field: Application - badManifestRequest := router.SubmitApplicationRequest{ - ApplicationJWT: "bad", - } - - badRequestValue := newRequestValue(tt, badManifestRequest) - req := httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/manifests/applications", badRequestValue) - w := httptest.NewRecorder() - - c := newRequestContext(w, req) - manifestRouter.SubmitApplication(c) - assert.Contains(tt, w.Body.String(), "invalid submit application request") - - // create an issuer - issuerDID, err := didService.CreateDIDByMethod(context.Background(), did.CreateDIDRequest{ - Method: didsdk.KeyMethod, - KeyType: crypto.Ed25519, - }) - assert.NoError(tt, err) - assert.NotEmpty(tt, issuerDID) - - // create an applicant - applicantPrivKey, applicantDIDKey, err := key.GenerateDIDKey(crypto.Ed25519) - assert.NoError(tt, err) - assert.NotEmpty(tt, applicantPrivKey) - assert.NotEmpty(tt, applicantDIDKey) - - applicantDID, err := applicantDIDKey.Expand() - assert.NoError(tt, err) - assert.NotEmpty(tt, applicantDID) - - // create a schema for the creds to be issued against - kid := issuerDID.DID.VerificationMethod[0].ID - licenseApplicationSchema, err := schemaService.CreateSchema(context.Background(), - schema.CreateSchemaRequest{Issuer: issuerDID.DID.ID, IssuerKID: kid, Name: "license application schema", Schema: getLicenseApplicationSchema()}) - assert.NoError(tt, err) - assert.NotEmpty(tt, licenseApplicationSchema) - - // issue a credential against the schema to the subject, from the issuer - createdCred, err := credentialService.CreateCredential(context.Background(), credential.CreateCredentialRequest{ - Issuer: issuerDID.DID.ID, - IssuerKID: kid, - Subject: applicantDID.ID, - SchemaID: licenseApplicationSchema.ID, - Data: map[string]any{"licenseType": "Class D"}, - }) - assert.NoError(tt, err) - assert.NotEmpty(tt, createdCred) - - w = httptest.NewRecorder() - - // good request - createManifestRequest := getValidCreateManifestRequest(issuerDID.DID.ID, issuerDID.DID.VerificationMethod[0].ID, licenseApplicationSchema.ID) - - requestValue := newRequestValue(tt, createManifestRequest) - req = httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/manifests", requestValue) - c = newRequestContext(w, req) - manifestRouter.CreateManifest(c) - assert.True(tt, util.Is2xxResponse(w.Code)) - - var resp router.CreateManifestResponse - err = json.NewDecoder(w.Body).Decode(&resp) - assert.NoError(tt, err) - - m := resp.Manifest - assert.NotEmpty(tt, m) - assert.Equal(tt, m.Issuer.ID, issuerDID.DID.ID) - - // good application request - container := []credmodel.Container{{CredentialJWT: createdCred.CredentialJWT}} - applicationRequest := getValidApplicationRequest(m.ID, m.PresentationDefinition.ID, m.PresentationDefinition.InputDescriptors[0].ID, container) - - // remove the presentation to make this a bad request - savedSubmission := applicationRequest.CredentialApplication.PresentationSubmission - applicationRequest.CredentialApplication.PresentationSubmission = nil - - // sign application - signer, err := keyaccess.NewJWKKeyAccess(applicantDID.ID, applicantDID.VerificationMethod[0].ID, applicantPrivKey) - assert.NoError(tt, err) - signed, err := signer.SignJSON(applicationRequest) - assert.NoError(tt, err) - - w = httptest.NewRecorder() - - applicationRequestValue := newRequestValue(tt, router.SubmitApplicationRequest{ApplicationJWT: *signed}) - req = httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/manifests/applications", applicationRequestValue) - - c = newRequestContext(w, req) - manifestRouter.SubmitApplication(c) - assert.True(tt, util.Is2xxResponse(w.Code)) - - var op router.Operation - err = json.NewDecoder(w.Body).Decode(&op) - assert.NoError(tt, err) - - var appResp router.SubmitApplicationResponse - respData, err := json.Marshal(op.Result.Response) - assert.NoError(tt, err) - err = json.Unmarshal(respData, &appResp) - assert.NoError(tt, err) - - assert.NotEmpty(tt, appResp.Response) - assert.Equal(tt, resp.Manifest.ID, appResp.Response.ManifestID) - assert.NotEmpty(tt, appResp.Response.Denial) - assert.Contains(tt, appResp.Response.Denial.Reason, "no descriptors provided for application") - assert.Len(tt, appResp.Response.Denial.InputDescriptors, 0) - - // submit it again, with an unfulfilled descriptor - savedSubmission.DescriptorMap[0].ID = "bad" - applicationRequest.CredentialApplication.PresentationSubmission = savedSubmission - - w = httptest.NewRecorder() - - // sign application - signed, err = signer.SignJSON(applicationRequest) - assert.NoError(tt, err) - - applicationRequestValue = newRequestValue(tt, router.SubmitApplicationRequest{ApplicationJWT: *signed}) - req = httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/manifests/applications", applicationRequestValue) - c = newRequestContext(w, req) - manifestRouter.SubmitApplication(c) - assert.True(tt, util.Is2xxResponse(w.Code)) - - err = json.NewDecoder(w.Body).Decode(&op) - assert.NoError(tt, err) - - respData, err = json.Marshal(op.Result.Response) - assert.NoError(tt, err) - err = json.Unmarshal(respData, &appResp) - assert.NoError(tt, err) - - assert.NotEmpty(tt, appResp.Response) - assert.Equal(tt, resp.Manifest.ID, appResp.Response.ManifestID) - assert.NotEmpty(tt, appResp.Response.Denial) - assert.Contains(tt, appResp.Response.Denial.Reason, "unfilled input descriptor(s): license-type: no submission descriptor found for input descriptor") - assert.Len(tt, appResp.Response.Denial.InputDescriptors, 1) - assert.Equal(tt, appResp.Response.Denial.InputDescriptors[0], "license-type") - }) - - t.Run("Test Get Application By ID and Get Applications", func(tt *testing.T) { - bolt := setupTestDB(tt) - require.NotEmpty(tt, bolt) - - keyStoreService := testKeyStoreService(tt, bolt) - didService := testDIDService(tt, bolt, keyStoreService) - schemaService := testSchemaService(tt, bolt, keyStoreService, didService) - credentialService := testCredentialService(tt, bolt, keyStoreService, didService, schemaService) - manifestRouter, _ := testManifest(tt, bolt, keyStoreService, didService, credentialService) - w := httptest.NewRecorder() - - // get a application that doesn't exit - req := httptest.NewRequest(http.MethodGet, "https://ssi-service.com/v1/manifests/applications/bad", nil) - c := newRequestContext(w, req) - manifestRouter.GetApplication(c) - assert.Contains(tt, w.Body.String(), "cannot get application without ID parameter") - - // reset recorder between calls - w = httptest.NewRecorder() - - // create an issuer - issuerDID, err := didService.CreateDIDByMethod(context.Background(), did.CreateDIDRequest{ - Method: didsdk.KeyMethod, - KeyType: crypto.Ed25519, - }) - assert.NoError(tt, err) - assert.NotEmpty(tt, issuerDID) - - // create an applicant - applicantPrivKey, applicantDIDKey, err := key.GenerateDIDKey(crypto.Ed25519) - assert.NoError(tt, err) - assert.NotEmpty(tt, applicantPrivKey) - assert.NotEmpty(tt, applicantDIDKey) - - applicantDID, err := applicantDIDKey.Expand() - assert.NoError(tt, err) - assert.NotEmpty(tt, applicantDID) - - // create a schema for the creds to be issued against - kid := issuerDID.DID.VerificationMethod[0].ID - licenseApplicationSchema, err := schemaService.CreateSchema(context.Background(), - schema.CreateSchemaRequest{Issuer: issuerDID.DID.ID, IssuerKID: kid, Name: "license application schema", Schema: getLicenseApplicationSchema()}) - assert.NoError(tt, err) - assert.NotEmpty(tt, licenseApplicationSchema) - - licenseSchema, err := schemaService.CreateSchema( - context.Background(), - schema.CreateSchemaRequest{Issuer: issuerDID.DID.ID, IssuerKID: kid, Name: "license schema", Schema: getLicenseSchema()}) - assert.NoError(tt, err) - assert.NotEmpty(tt, licenseSchema) - // issue a credential against the schema to the subject, from the issuer - createdCred, err := credentialService.CreateCredential(context.Background(), credential.CreateCredentialRequest{ - Issuer: issuerDID.DID.ID, - IssuerKID: kid, - Subject: applicantDID.ID, - SchemaID: licenseApplicationSchema.ID, - Data: map[string]any{"licenseType": "Class D"}, - }) - assert.NoError(tt, err) - assert.NotEmpty(tt, createdCred) - - // good request - createManifestRequest := getValidCreateManifestRequest(issuerDID.DID.ID, issuerDID.DID.VerificationMethod[0].ID, licenseSchema.ID) - - requestValue := newRequestValue(tt, createManifestRequest) - req = httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/manifests", requestValue) - c = newRequestContext(w, req) - manifestRouter.CreateManifest(c) - assert.True(tt, util.Is2xxResponse(w.Code)) - - var resp router.CreateManifestResponse - err = json.NewDecoder(w.Body).Decode(&resp) - assert.NoError(tt, err) - - m := resp.Manifest - assert.NotEmpty(tt, m) - assert.Equal(tt, m.Issuer.ID, issuerDID.DID.ID) - - // good application request - container := []credmodel.Container{{CredentialJWT: createdCred.CredentialJWT}} - applicationRequest := getValidApplicationRequest(m.ID, m.PresentationDefinition.ID, m.PresentationDefinition.InputDescriptors[0].ID, container) - - // sign application - signer, err := keyaccess.NewJWKKeyAccess(applicantDID.ID, applicantDID.VerificationMethod[0].ID, applicantPrivKey) - assert.NoError(tt, err) - signed, err := signer.SignJSON(applicationRequest) - assert.NoError(tt, err) - - applicationRequestValue := newRequestValue(tt, router.SubmitApplicationRequest{ApplicationJWT: *signed}) - req = httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/manifests/applications", applicationRequestValue) - c = newRequestContext(w, req) - manifestRouter.SubmitApplication(c) - assert.True(tt, util.Is2xxResponse(w.Code)) - - var op router.Operation - err = json.NewDecoder(w.Body).Decode(&op) - assert.NoError(tt, err) - - // review application - reviewApplicationRequestValue := newRequestValue(tt, router.ReviewApplicationRequest{ - Approved: true, - Reason: "I'm the almighty approver", - CredentialOverrides: map[string]manifestsvc.CredentialOverride{ - "drivers-license-ca": { - Data: map[string]any{ - "firstName": "John", - "lastName": "Doe", - "state": "CA", - "looks": "pretty darn handsome", + } + assert.Equal(tt, expectedSubject, vc2.CredentialSubject) + assert.Equal(tt, + time.Date(2022, 10, 31, 0, 0, 5, 0, time.UTC).Format(time.RFC3339), + vc2.ExpirationDate, + ) + assert.Equal(tt, licenseSchema.ID, vc2.CredentialSchema.ID) + assert.NotEmpty(tt, vc2.CredentialStatus) + }) + + t.Run("Test Submit Application with multiple outputs and overrides", func(tt *testing.T) { + db := test.ServiceStorage(t) + require.NotEmpty(tt, db) + + keyStoreService := testKeyStoreService(tt, db) + didService := testDIDService(tt, db, keyStoreService) + schemaService := testSchemaService(tt, db, keyStoreService, didService) + credentialService := testCredentialService(tt, db, keyStoreService, didService, schemaService) + manifestRouter, _ := testManifest(tt, db, keyStoreService, didService, credentialService) + + // missing required field: Application + badManifestRequest := router.SubmitApplicationRequest{ApplicationJWT: "bad"} + badRequestValue := newRequestValue(tt, badManifestRequest) + req := httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/manifests/applications", badRequestValue) + w := httptest.NewRecorder() + + c := newRequestContext(w, req) + manifestRouter.SubmitApplication(c) + assert.Contains(tt, w.Body.String(), "invalid submit application request") + + // create an issuer + issuerDID, err := didService.CreateDIDByMethod(context.Background(), did.CreateDIDRequest{ + Method: didsdk.KeyMethod, + KeyType: crypto.Ed25519, + }) + assert.NoError(tt, err) + assert.NotEmpty(tt, issuerDID) + + // create an applicant + applicantPrivKey, applicantDIDKey, err := key.GenerateDIDKey(crypto.Ed25519) + assert.NoError(tt, err) + assert.NotEmpty(tt, applicantPrivKey) + assert.NotEmpty(tt, applicantDIDKey) + + applicantDID, err := applicantDIDKey.Expand() + assert.NoError(tt, err) + assert.NotEmpty(tt, applicantDID) + + // create a schema for the creds to be issued against, needed for the application + kid := issuerDID.DID.VerificationMethod[0].ID + licenseApplicationSchema, err := schemaService.CreateSchema( + context.Background(), + schema.CreateSchemaRequest{Issuer: issuerDID.DID.ID, IssuerKID: kid, Name: "license application schema", Schema: getLicenseApplicationSchema()}) + assert.NoError(tt, err) + assert.NotEmpty(tt, licenseApplicationSchema) + + // create a second schema for the creds to be issued after the application is approved + licenseSchema, err := schemaService.CreateSchema( + context.Background(), + schema.CreateSchemaRequest{Issuer: issuerDID.DID.ID, IssuerKID: kid, Name: "license schema", Schema: getLicenseSchema()}) + assert.NoError(tt, err) + assert.NotEmpty(tt, licenseSchema) + + // issue a credential against the schema to the subject, from the issuer + createdCred, err := credentialService.CreateCredential(context.Background(), credential.CreateCredentialRequest{ + Issuer: issuerDID.DID.ID, + IssuerKID: kid, + Subject: applicantDID.ID, + SchemaID: licenseApplicationSchema.ID, + Data: map[string]any{"licenseType": "Class D"}, + }) + assert.NoError(tt, err) + assert.NotEmpty(tt, createdCred) + + // good request to create a manifest + createManifestRequest := getValidCreateManifestRequest(issuerDID.DID.ID, issuerDID.DID.VerificationMethod[0].ID, licenseSchema.ID) + w = httptest.NewRecorder() + requestValue := newRequestValue(tt, createManifestRequest) + req = httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/manifests", requestValue) + c = newRequestContext(w, req) + manifestRouter.CreateManifest(c) + assert.True(tt, util.Is2xxResponse(w.Code)) + + var resp router.CreateManifestResponse + err = json.NewDecoder(w.Body).Decode(&resp) + assert.NoError(tt, err) + + m := resp.Manifest + assert.NotEmpty(tt, m) + assert.Equal(tt, m.Issuer.ID, issuerDID.DID.ID) + + // good application request + container := []credmodel.Container{{CredentialJWT: createdCred.CredentialJWT}} + applicationRequest := getValidApplicationRequest(m.ID, m.PresentationDefinition.ID, m.PresentationDefinition.InputDescriptors[0].ID, container) + + // sign application + signer, err := keyaccess.NewJWKKeyAccess(applicantDID.ID, applicantDID.VerificationMethod[0].ID, applicantPrivKey) + assert.NoError(tt, err) + signed, err := signer.SignJSON(applicationRequest) + assert.NoError(tt, err) + + w = httptest.NewRecorder() + + applicationRequestValue := newRequestValue(tt, router.SubmitApplicationRequest{ApplicationJWT: *signed}) + req = httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/manifests/applications", applicationRequestValue) + c = newRequestContext(w, req) + manifestRouter.SubmitApplication(c) + assert.NoError(tt, err) + + var op router.Operation + err = json.NewDecoder(w.Body).Decode(&op) + assert.NoError(tt, err) + + assert.False(tt, op.Done) + assert.Contains(tt, op.ID, "credentials/responses/") + + // review application + expireAt := time.Date(2025, 10, 32, 0, 0, 0, 0, time.UTC) + reviewApplicationRequestValue := newRequestValue(tt, router.ReviewApplicationRequest{ + Approved: true, + Reason: "I'm the almighty approver", + CredentialOverrides: map[string]manifestsvc.CredentialOverride{ + "drivers-license-ca": { + Data: map[string]any{ + "firstName": "John", + "lastName": "Doe", + "state": "CA", + "looks": "pretty darn handsome", + }, + Expiry: &expireAt, + Revocable: true, + }, + "drivers-license-ny": { + Data: map[string]any{ + "firstName": "John", + "lastName": "Doe", + "state": "NY", + "looks": "even handsomer", + }, + Expiry: &expireAt, + Revocable: true, + }, }, - }, - "drivers-license-ny": { - Data: map[string]any{ - "firstName": "John", - "lastName": "Doe", - "state": "NY", - "looks": "even handsomer", + }) + applicationID := storage.StatusObjectID(op.ID) + req = httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/manifests/applications/"+applicationID+"/review", reviewApplicationRequestValue) + c = newRequestContextWithParams(w, req, map[string]string{"id": applicationID}) + manifestRouter.ReviewApplication(c) + assert.True(tt, util.Is2xxResponse(w.Code)) + + var appResp router.SubmitApplicationResponse + err = json.NewDecoder(w.Body).Decode(&appResp) + assert.NoError(tt, err) + + assert.NotEmpty(tt, appResp.Response) + assert.Equal(tt, resp.Manifest.ID, appResp.Response.ManifestID) + assert.NotEmpty(tt, appResp.Response.Fulfillment) + assert.Len(tt, appResp.Response.Fulfillment.DescriptorMap, 2) + assert.Len(tt, appResp.Credentials, 2) + assert.Empty(tt, appResp.Response.Denial) + + _, _, vc, err := credsdk.ToCredential(appResp.Credentials[0]) + assert.NoError(tt, err) + assert.Equal(tt, credsdk.CredentialSubject{ + "id": applicantDID.ID, + "firstName": "John", + "lastName": "Doe", + "state": "CA", + "looks": "pretty darn handsome", + }, vc.CredentialSubject) + assert.Equal(tt, expireAt.Format(time.RFC3339), vc.ExpirationDate) + assert.NotEmpty(tt, vc.CredentialStatus) + assert.Equal(tt, licenseSchema.ID, vc.CredentialSchema.ID) + }) + + t.Run("Test Denied Application", func(tt *testing.T) { + db := test.ServiceStorage(t) + require.NotEmpty(tt, db) + + keyStoreService := testKeyStoreService(tt, db) + didService := testDIDService(tt, db, keyStoreService) + schemaService := testSchemaService(tt, db, keyStoreService, didService) + credentialService := testCredentialService(tt, db, keyStoreService, didService, schemaService) + manifestRouter, _ := testManifest(tt, db, keyStoreService, didService, credentialService) + + // missing required field: Application + badManifestRequest := router.SubmitApplicationRequest{ + ApplicationJWT: "bad", + } + + badRequestValue := newRequestValue(tt, badManifestRequest) + req := httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/manifests/applications", badRequestValue) + w := httptest.NewRecorder() + + c := newRequestContext(w, req) + manifestRouter.SubmitApplication(c) + assert.Contains(tt, w.Body.String(), "invalid submit application request") + + // create an issuer + issuerDID, err := didService.CreateDIDByMethod(context.Background(), did.CreateDIDRequest{ + Method: didsdk.KeyMethod, + KeyType: crypto.Ed25519, + }) + assert.NoError(tt, err) + assert.NotEmpty(tt, issuerDID) + + // create an applicant + applicantPrivKey, applicantDIDKey, err := key.GenerateDIDKey(crypto.Ed25519) + assert.NoError(tt, err) + assert.NotEmpty(tt, applicantPrivKey) + assert.NotEmpty(tt, applicantDIDKey) + + applicantDID, err := applicantDIDKey.Expand() + assert.NoError(tt, err) + assert.NotEmpty(tt, applicantDID) + + // create a schema for the creds to be issued against + kid := issuerDID.DID.VerificationMethod[0].ID + licenseApplicationSchema, err := schemaService.CreateSchema(context.Background(), + schema.CreateSchemaRequest{Issuer: issuerDID.DID.ID, IssuerKID: kid, Name: "license application schema", Schema: getLicenseApplicationSchema()}) + assert.NoError(tt, err) + assert.NotEmpty(tt, licenseApplicationSchema) + + // issue a credential against the schema to the subject, from the issuer + createdCred, err := credentialService.CreateCredential(context.Background(), credential.CreateCredentialRequest{ + Issuer: issuerDID.DID.ID, + IssuerKID: kid, + Subject: applicantDID.ID, + SchemaID: licenseApplicationSchema.ID, + Data: map[string]any{"licenseType": "Class D"}, + }) + assert.NoError(tt, err) + assert.NotEmpty(tt, createdCred) + + w = httptest.NewRecorder() + + // good request + createManifestRequest := getValidCreateManifestRequest(issuerDID.DID.ID, issuerDID.DID.VerificationMethod[0].ID, licenseApplicationSchema.ID) + + requestValue := newRequestValue(tt, createManifestRequest) + req = httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/manifests", requestValue) + c = newRequestContext(w, req) + manifestRouter.CreateManifest(c) + assert.True(tt, util.Is2xxResponse(w.Code)) + + var resp router.CreateManifestResponse + err = json.NewDecoder(w.Body).Decode(&resp) + assert.NoError(tt, err) + + m := resp.Manifest + assert.NotEmpty(tt, m) + assert.Equal(tt, m.Issuer.ID, issuerDID.DID.ID) + + // good application request + container := []credmodel.Container{{CredentialJWT: createdCred.CredentialJWT}} + applicationRequest := getValidApplicationRequest(m.ID, m.PresentationDefinition.ID, m.PresentationDefinition.InputDescriptors[0].ID, container) + + // remove the presentation to make this a bad request + savedSubmission := applicationRequest.CredentialApplication.PresentationSubmission + applicationRequest.CredentialApplication.PresentationSubmission = nil + + // sign application + signer, err := keyaccess.NewJWKKeyAccess(applicantDID.ID, applicantDID.VerificationMethod[0].ID, applicantPrivKey) + assert.NoError(tt, err) + signed, err := signer.SignJSON(applicationRequest) + assert.NoError(tt, err) + + w = httptest.NewRecorder() + + applicationRequestValue := newRequestValue(tt, router.SubmitApplicationRequest{ApplicationJWT: *signed}) + req = httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/manifests/applications", applicationRequestValue) + + c = newRequestContext(w, req) + manifestRouter.SubmitApplication(c) + assert.True(tt, util.Is2xxResponse(w.Code)) + + var op router.Operation + err = json.NewDecoder(w.Body).Decode(&op) + assert.NoError(tt, err) + + var appResp router.SubmitApplicationResponse + respData, err := json.Marshal(op.Result.Response) + assert.NoError(tt, err) + err = json.Unmarshal(respData, &appResp) + assert.NoError(tt, err) + + assert.NotEmpty(tt, appResp.Response) + assert.Equal(tt, resp.Manifest.ID, appResp.Response.ManifestID) + assert.NotEmpty(tt, appResp.Response.Denial) + assert.Contains(tt, appResp.Response.Denial.Reason, "no descriptors provided for application") + assert.Len(tt, appResp.Response.Denial.InputDescriptors, 0) + + // submit it again, with an unfulfilled descriptor + savedSubmission.DescriptorMap[0].ID = "bad" + applicationRequest.CredentialApplication.PresentationSubmission = savedSubmission + + w = httptest.NewRecorder() + + // sign application + signed, err = signer.SignJSON(applicationRequest) + assert.NoError(tt, err) + + applicationRequestValue = newRequestValue(tt, router.SubmitApplicationRequest{ApplicationJWT: *signed}) + req = httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/manifests/applications", applicationRequestValue) + c = newRequestContext(w, req) + manifestRouter.SubmitApplication(c) + assert.True(tt, util.Is2xxResponse(w.Code)) + + err = json.NewDecoder(w.Body).Decode(&op) + assert.NoError(tt, err) + + respData, err = json.Marshal(op.Result.Response) + assert.NoError(tt, err) + err = json.Unmarshal(respData, &appResp) + assert.NoError(tt, err) + + assert.NotEmpty(tt, appResp.Response) + assert.Equal(tt, resp.Manifest.ID, appResp.Response.ManifestID) + assert.NotEmpty(tt, appResp.Response.Denial) + assert.Contains(tt, appResp.Response.Denial.Reason, "unfilled input descriptor(s): license-type: no submission descriptor found for input descriptor") + assert.Len(tt, appResp.Response.Denial.InputDescriptors, 1) + assert.Equal(tt, appResp.Response.Denial.InputDescriptors[0], "license-type") + }) + + t.Run("Test Get Application By ID and Get Applications", func(tt *testing.T) { + db := test.ServiceStorage(t) + require.NotEmpty(tt, db) + + keyStoreService := testKeyStoreService(tt, db) + didService := testDIDService(tt, db, keyStoreService) + schemaService := testSchemaService(tt, db, keyStoreService, didService) + credentialService := testCredentialService(tt, db, keyStoreService, didService, schemaService) + manifestRouter, _ := testManifest(tt, db, keyStoreService, didService, credentialService) + w := httptest.NewRecorder() + + // get a application that doesn't exit + req := httptest.NewRequest(http.MethodGet, "https://ssi-service.com/v1/manifests/applications/bad", nil) + c := newRequestContext(w, req) + manifestRouter.GetApplication(c) + assert.Contains(tt, w.Body.String(), "cannot get application without ID parameter") + + // reset recorder between calls + w = httptest.NewRecorder() + + // create an issuer + issuerDID, err := didService.CreateDIDByMethod(context.Background(), did.CreateDIDRequest{ + Method: didsdk.KeyMethod, + KeyType: crypto.Ed25519, + }) + assert.NoError(tt, err) + assert.NotEmpty(tt, issuerDID) + + // create an applicant + applicantPrivKey, applicantDIDKey, err := key.GenerateDIDKey(crypto.Ed25519) + assert.NoError(tt, err) + assert.NotEmpty(tt, applicantPrivKey) + assert.NotEmpty(tt, applicantDIDKey) + + applicantDID, err := applicantDIDKey.Expand() + assert.NoError(tt, err) + assert.NotEmpty(tt, applicantDID) + + // create a schema for the creds to be issued against + kid := issuerDID.DID.VerificationMethod[0].ID + licenseApplicationSchema, err := schemaService.CreateSchema(context.Background(), + schema.CreateSchemaRequest{Issuer: issuerDID.DID.ID, IssuerKID: kid, Name: "license application schema", Schema: getLicenseApplicationSchema()}) + assert.NoError(tt, err) + assert.NotEmpty(tt, licenseApplicationSchema) + + licenseSchema, err := schemaService.CreateSchema( + context.Background(), + schema.CreateSchemaRequest{Issuer: issuerDID.DID.ID, IssuerKID: kid, Name: "license schema", Schema: getLicenseSchema()}) + assert.NoError(tt, err) + assert.NotEmpty(tt, licenseSchema) + // issue a credential against the schema to the subject, from the issuer + createdCred, err := credentialService.CreateCredential(context.Background(), credential.CreateCredentialRequest{ + Issuer: issuerDID.DID.ID, + IssuerKID: kid, + Subject: applicantDID.ID, + SchemaID: licenseApplicationSchema.ID, + Data: map[string]any{"licenseType": "Class D"}, + }) + assert.NoError(tt, err) + assert.NotEmpty(tt, createdCred) + + // good request + createManifestRequest := getValidCreateManifestRequest(issuerDID.DID.ID, issuerDID.DID.VerificationMethod[0].ID, licenseSchema.ID) + + requestValue := newRequestValue(tt, createManifestRequest) + req = httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/manifests", requestValue) + c = newRequestContext(w, req) + manifestRouter.CreateManifest(c) + assert.True(tt, util.Is2xxResponse(w.Code)) + + var resp router.CreateManifestResponse + err = json.NewDecoder(w.Body).Decode(&resp) + assert.NoError(tt, err) + + m := resp.Manifest + assert.NotEmpty(tt, m) + assert.Equal(tt, m.Issuer.ID, issuerDID.DID.ID) + + // good application request + container := []credmodel.Container{{CredentialJWT: createdCred.CredentialJWT}} + applicationRequest := getValidApplicationRequest(m.ID, m.PresentationDefinition.ID, m.PresentationDefinition.InputDescriptors[0].ID, container) + + // sign application + signer, err := keyaccess.NewJWKKeyAccess(applicantDID.ID, applicantDID.VerificationMethod[0].ID, applicantPrivKey) + assert.NoError(tt, err) + signed, err := signer.SignJSON(applicationRequest) + assert.NoError(tt, err) + + applicationRequestValue := newRequestValue(tt, router.SubmitApplicationRequest{ApplicationJWT: *signed}) + req = httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/manifests/applications", applicationRequestValue) + c = newRequestContext(w, req) + manifestRouter.SubmitApplication(c) + assert.True(tt, util.Is2xxResponse(w.Code)) + + var op router.Operation + err = json.NewDecoder(w.Body).Decode(&op) + assert.NoError(tt, err) + + // review application + reviewApplicationRequestValue := newRequestValue(tt, router.ReviewApplicationRequest{ + Approved: true, + Reason: "I'm the almighty approver", + CredentialOverrides: map[string]manifestsvc.CredentialOverride{ + "drivers-license-ca": { + Data: map[string]any{ + "firstName": "John", + "lastName": "Doe", + "state": "CA", + "looks": "pretty darn handsome", + }, + }, + "drivers-license-ny": { + Data: map[string]any{ + "firstName": "John", + "lastName": "Doe", + "state": "NY", + "looks": "even handsomer", + }, + }, }, - }, - }, - }) - applicationID := storage.StatusObjectID(op.ID) - req = httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/manifests/applications/"+applicationID+"/review", reviewApplicationRequestValue) - c = newRequestContextWithParams(w, req, map[string]string{"id": applicationID}) - manifestRouter.ReviewApplication(c) - assert.True(tt, util.Is2xxResponse(w.Code)) - - var appResp router.SubmitApplicationResponse - err = json.NewDecoder(w.Body).Decode(&appResp) - assert.NoError(tt, err) - - assert.NotEmpty(tt, appResp.Response.Fulfillment) - assert.Empty(tt, appResp.Response.Denial) - - // get response by id - req = httptest.NewRequest(http.MethodGet, fmt.Sprintf("https://ssi-service.com/v1/manifests/responses/%s", appResp.Response.ID), nil) - c = newRequestContextWithParams(w, req, map[string]string{"id": appResp.Response.ID}) - manifestRouter.GetResponse(c) - assert.True(tt, util.Is2xxResponse(w.Code)) - - var getResponseResponse router.GetResponseResponse - err = json.NewDecoder(w.Body).Decode(&getResponseResponse) - assert.NoError(tt, err) - assert.NotEmpty(tt, getResponseResponse) - assert.Equal(tt, appResp.Response.ID, getResponseResponse.Response.ID) - - // get all responses - req = httptest.NewRequest(http.MethodGet, "https://ssi-service.com/v1/manifests/responses", nil) - c = newRequestContext(w, req) - manifestRouter.ListResponses(c) - assert.True(tt, util.Is2xxResponse(w.Code)) - - var getResponsesResp router.ListResponsesResponse - err = json.NewDecoder(w.Body).Decode(&getResponsesResp) - assert.NoError(tt, err) - assert.NotEmpty(tt, getResponsesResp) - - assert.Len(tt, getResponsesResp.Responses, 1) - - // get all applications - req = httptest.NewRequest(http.MethodGet, "https://ssi-service.com/v1/manifests/applications", applicationRequestValue) - c = newRequestContext(w, req) - manifestRouter.ListApplications(c) - assert.True(tt, util.Is2xxResponse(w.Code)) - - var getApplicationsResp router.ListApplicationsResponse - err = json.NewDecoder(w.Body).Decode(&getApplicationsResp) - assert.NoError(tt, err) - assert.NotEmpty(tt, getApplicationsResp) - - assert.Len(tt, getApplicationsResp.Applications, 1) - - // get application by id - req = httptest.NewRequest(http.MethodGet, fmt.Sprintf("https://ssi-service.com/v1/manifests/applications/%s", getApplicationsResp.Applications[0].ID), nil) - c = newRequestContextWithParams(w, req, map[string]string{"id": getApplicationsResp.Applications[0].ID}) - manifestRouter.GetApplication(c) - assert.True(tt, util.Is2xxResponse(w.Code)) - - var getApplicationResponse router.GetApplicationResponse - err = json.NewDecoder(w.Body).Decode(&getApplicationResponse) - assert.NoError(tt, err) - assert.NotEmpty(tt, getApplicationResponse) - assert.Equal(tt, getApplicationsResp.Applications[0].ID, getApplicationResponse.ID) - }) - - t.Run("Test Delete Application", func(tt *testing.T) { - bolt := setupTestDB(tt) - require.NotEmpty(tt, bolt) - - keyStoreService := testKeyStoreService(tt, bolt) - didService := testDIDService(tt, bolt, keyStoreService) - schemaService := testSchemaService(tt, bolt, keyStoreService, didService) - credentialService := testCredentialService(tt, bolt, keyStoreService, didService, schemaService) - manifestRouter, _ := testManifest(tt, bolt, keyStoreService, didService, credentialService) - - w := httptest.NewRecorder() - - // create an issuer - issuerDID, err := didService.CreateDIDByMethod(context.Background(), did.CreateDIDRequest{ - Method: didsdk.KeyMethod, - KeyType: crypto.Ed25519, - }) - assert.NoError(tt, err) - assert.NotEmpty(tt, issuerDID) - - // create an applicant - applicantPrivKey, applicantDIDKey, err := key.GenerateDIDKey(crypto.Ed25519) - assert.NoError(tt, err) - assert.NotEmpty(tt, applicantPrivKey) - assert.NotEmpty(tt, applicantDIDKey) - - applicantDID, err := applicantDIDKey.Expand() - assert.NoError(tt, err) - assert.NotEmpty(tt, applicantDID) - - // create a schema for the creds to be issued against - kid := issuerDID.DID.VerificationMethod[0].ID - createdSchema, err := schemaService.CreateSchema(context.Background(), - schema.CreateSchemaRequest{Issuer: issuerDID.DID.ID, IssuerKID: kid, Name: "license schema", Schema: getLicenseApplicationSchema()}) - assert.NoError(tt, err) - assert.NotEmpty(tt, createdSchema) - - // issue a credential against the schema to the subject, from the issuer - createdCred, err := credentialService.CreateCredential(context.Background(), credential.CreateCredentialRequest{ - Issuer: issuerDID.DID.ID, - IssuerKID: kid, - Subject: applicantDID.ID, - SchemaID: createdSchema.ID, - Data: map[string]any{"licenseType": "WA-DL-CLASS-A"}, + }) + applicationID := storage.StatusObjectID(op.ID) + req = httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/manifests/applications/"+applicationID+"/review", reviewApplicationRequestValue) + c = newRequestContextWithParams(w, req, map[string]string{"id": applicationID}) + manifestRouter.ReviewApplication(c) + assert.True(tt, util.Is2xxResponse(w.Code)) + + var appResp router.SubmitApplicationResponse + err = json.NewDecoder(w.Body).Decode(&appResp) + assert.NoError(tt, err) + + assert.NotEmpty(tt, appResp.Response.Fulfillment) + assert.Empty(tt, appResp.Response.Denial) + + // get response by id + req = httptest.NewRequest(http.MethodGet, fmt.Sprintf("https://ssi-service.com/v1/manifests/responses/%s", appResp.Response.ID), nil) + c = newRequestContextWithParams(w, req, map[string]string{"id": appResp.Response.ID}) + manifestRouter.GetResponse(c) + assert.True(tt, util.Is2xxResponse(w.Code)) + + var getResponseResponse router.GetResponseResponse + err = json.NewDecoder(w.Body).Decode(&getResponseResponse) + assert.NoError(tt, err) + assert.NotEmpty(tt, getResponseResponse) + assert.Equal(tt, appResp.Response.ID, getResponseResponse.Response.ID) + + // get all responses + req = httptest.NewRequest(http.MethodGet, "https://ssi-service.com/v1/manifests/responses", nil) + c = newRequestContext(w, req) + manifestRouter.ListResponses(c) + assert.True(tt, util.Is2xxResponse(w.Code)) + + var getResponsesResp router.ListResponsesResponse + err = json.NewDecoder(w.Body).Decode(&getResponsesResp) + assert.NoError(tt, err) + assert.NotEmpty(tt, getResponsesResp) + + assert.Len(tt, getResponsesResp.Responses, 1) + + // get all applications + req = httptest.NewRequest(http.MethodGet, "https://ssi-service.com/v1/manifests/applications", applicationRequestValue) + c = newRequestContext(w, req) + manifestRouter.ListApplications(c) + assert.True(tt, util.Is2xxResponse(w.Code)) + + var getApplicationsResp router.ListApplicationsResponse + err = json.NewDecoder(w.Body).Decode(&getApplicationsResp) + assert.NoError(tt, err) + assert.NotEmpty(tt, getApplicationsResp) + + assert.Len(tt, getApplicationsResp.Applications, 1) + + // get application by id + req = httptest.NewRequest(http.MethodGet, fmt.Sprintf("https://ssi-service.com/v1/manifests/applications/%s", getApplicationsResp.Applications[0].ID), nil) + c = newRequestContextWithParams(w, req, map[string]string{"id": getApplicationsResp.Applications[0].ID}) + manifestRouter.GetApplication(c) + assert.True(tt, util.Is2xxResponse(w.Code)) + + var getApplicationResponse router.GetApplicationResponse + err = json.NewDecoder(w.Body).Decode(&getApplicationResponse) + assert.NoError(tt, err) + assert.NotEmpty(tt, getApplicationResponse) + assert.Equal(tt, getApplicationsResp.Applications[0].ID, getApplicationResponse.ID) + }) + + t.Run("Test Delete Application", func(tt *testing.T) { + db := test.ServiceStorage(t) + require.NotEmpty(tt, db) + + keyStoreService := testKeyStoreService(tt, db) + didService := testDIDService(tt, db, keyStoreService) + schemaService := testSchemaService(tt, db, keyStoreService, didService) + credentialService := testCredentialService(tt, db, keyStoreService, didService, schemaService) + manifestRouter, _ := testManifest(tt, db, keyStoreService, didService, credentialService) + + w := httptest.NewRecorder() + + // create an issuer + issuerDID, err := didService.CreateDIDByMethod(context.Background(), did.CreateDIDRequest{ + Method: didsdk.KeyMethod, + KeyType: crypto.Ed25519, + }) + assert.NoError(tt, err) + assert.NotEmpty(tt, issuerDID) + + // create an applicant + applicantPrivKey, applicantDIDKey, err := key.GenerateDIDKey(crypto.Ed25519) + assert.NoError(tt, err) + assert.NotEmpty(tt, applicantPrivKey) + assert.NotEmpty(tt, applicantDIDKey) + + applicantDID, err := applicantDIDKey.Expand() + assert.NoError(tt, err) + assert.NotEmpty(tt, applicantDID) + + // create a schema for the creds to be issued against + kid := issuerDID.DID.VerificationMethod[0].ID + createdSchema, err := schemaService.CreateSchema(context.Background(), + schema.CreateSchemaRequest{Issuer: issuerDID.DID.ID, IssuerKID: kid, Name: "license schema", Schema: getLicenseApplicationSchema()}) + assert.NoError(tt, err) + assert.NotEmpty(tt, createdSchema) + + // issue a credential against the schema to the subject, from the issuer + createdCred, err := credentialService.CreateCredential(context.Background(), credential.CreateCredentialRequest{ + Issuer: issuerDID.DID.ID, + IssuerKID: kid, + Subject: applicantDID.ID, + SchemaID: createdSchema.ID, + Data: map[string]any{"licenseType": "WA-DL-CLASS-A"}, + }) + assert.NoError(tt, err) + assert.NotEmpty(tt, createdCred) + + // good request + createManifestRequest := getValidCreateManifestRequest(issuerDID.DID.ID, issuerDID.DID.VerificationMethod[0].ID, createdSchema.ID) + + requestValue := newRequestValue(tt, createManifestRequest) + req := httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/manifests", requestValue) + c := newRequestContext(w, req) + manifestRouter.CreateManifest(c) + assert.True(tt, util.Is2xxResponse(w.Code)) + + var resp router.CreateManifestResponse + err = json.NewDecoder(w.Body).Decode(&resp) + assert.NoError(tt, err) + + m := resp.Manifest + assert.NotEmpty(tt, m) + assert.Equal(tt, m.Issuer.ID, issuerDID.DID.ID) + + // good application request + container := []credmodel.Container{{CredentialJWT: createdCred.CredentialJWT}} + applicationRequest := getValidApplicationRequest(m.ID, m.PresentationDefinition.ID, m.PresentationDefinition.InputDescriptors[0].ID, container) + + // sign application + signer, err := keyaccess.NewJWKKeyAccess(applicantDID.ID, applicantDID.VerificationMethod[0].ID, applicantPrivKey) + assert.NoError(tt, err) + signed, err := signer.SignJSON(applicationRequest) + assert.NoError(tt, err) + + applicationRequestValue := newRequestValue(tt, router.SubmitApplicationRequest{ApplicationJWT: *signed}) + req = httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/manifests/applications", applicationRequestValue) + c = newRequestContext(w, req) + manifestRouter.SubmitApplication(c) + assert.True(tt, util.Is2xxResponse(w.Code)) + + var appResp router.SubmitApplicationResponse + err = json.NewDecoder(w.Body).Decode(&appResp) + assert.NoError(tt, err) + + // get all applications + req = httptest.NewRequest(http.MethodGet, "https://ssi-service.com/v1/manifests/applications", applicationRequestValue) + c = newRequestContext(w, req) + manifestRouter.ListApplications(c) + assert.True(tt, util.Is2xxResponse(w.Code)) + + var getApplicationsResp router.ListApplicationsResponse + err = json.NewDecoder(w.Body).Decode(&getApplicationsResp) + assert.NoError(tt, err) + assert.NotEmpty(tt, getApplicationsResp) + assert.Len(tt, getApplicationsResp.Applications, 1) + + // get the application + req = httptest.NewRequest(http.MethodGet, fmt.Sprintf("https://ssi-service.com/v1/manifests/applications/%s", getApplicationsResp.Applications[0].ID), nil) + c = newRequestContextWithParams(w, req, map[string]string{"id": getApplicationsResp.Applications[0].ID}) + manifestRouter.GetApplication(c) + assert.True(tt, util.Is2xxResponse(w.Code)) + + var getApplicationResp router.GetApplicationResponse + err = json.NewDecoder(w.Body).Decode(&getApplicationResp) + assert.NoError(tt, err) + assert.NotEmpty(tt, getApplicationResp) + assert.Equal(tt, resp.Manifest.ID, getApplicationResp.Application.ManifestID) + + // delete the application + req = httptest.NewRequest(http.MethodDelete, fmt.Sprintf("https://ssi-service.com/v1/manifests/applications/%s", getApplicationResp.Application.ID), nil) + c = newRequestContextWithParams(w, req, map[string]string{"id": getApplicationsResp.Applications[0].ID}) + manifestRouter.DeleteApplication(c) + assert.True(tt, util.Is2xxResponse(w.Code)) + + w = httptest.NewRecorder() + + // get it back + req = httptest.NewRequest(http.MethodGet, fmt.Sprintf("https://ssi-service.com/v1/manifests/applications/%s", appResp.Response.ID), nil) + c = newRequestContextWithParams(w, req, map[string]string{"id": getApplicationsResp.Applications[0].ID}) + manifestRouter.GetApplication(c) + assert.Contains(tt, w.Body.String(), fmt.Sprintf("could not get application with id: %s", appResp.Response.ID)) + }) }) - assert.NoError(tt, err) - assert.NotEmpty(tt, createdCred) - - // good request - createManifestRequest := getValidCreateManifestRequest(issuerDID.DID.ID, issuerDID.DID.VerificationMethod[0].ID, createdSchema.ID) - - requestValue := newRequestValue(tt, createManifestRequest) - req := httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/manifests", requestValue) - c := newRequestContext(w, req) - manifestRouter.CreateManifest(c) - assert.True(tt, util.Is2xxResponse(w.Code)) - - var resp router.CreateManifestResponse - err = json.NewDecoder(w.Body).Decode(&resp) - assert.NoError(tt, err) - - m := resp.Manifest - assert.NotEmpty(tt, m) - assert.Equal(tt, m.Issuer.ID, issuerDID.DID.ID) - - // good application request - container := []credmodel.Container{{CredentialJWT: createdCred.CredentialJWT}} - applicationRequest := getValidApplicationRequest(m.ID, m.PresentationDefinition.ID, m.PresentationDefinition.InputDescriptors[0].ID, container) - - // sign application - signer, err := keyaccess.NewJWKKeyAccess(applicantDID.ID, applicantDID.VerificationMethod[0].ID, applicantPrivKey) - assert.NoError(tt, err) - signed, err := signer.SignJSON(applicationRequest) - assert.NoError(tt, err) - - applicationRequestValue := newRequestValue(tt, router.SubmitApplicationRequest{ApplicationJWT: *signed}) - req = httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/manifests/applications", applicationRequestValue) - c = newRequestContext(w, req) - manifestRouter.SubmitApplication(c) - assert.True(tt, util.Is2xxResponse(w.Code)) - - var appResp router.SubmitApplicationResponse - err = json.NewDecoder(w.Body).Decode(&appResp) - assert.NoError(tt, err) - - // get all applications - req = httptest.NewRequest(http.MethodGet, "https://ssi-service.com/v1/manifests/applications", applicationRequestValue) - c = newRequestContext(w, req) - manifestRouter.ListApplications(c) - assert.True(tt, util.Is2xxResponse(w.Code)) - - var getApplicationsResp router.ListApplicationsResponse - err = json.NewDecoder(w.Body).Decode(&getApplicationsResp) - assert.NoError(tt, err) - assert.NotEmpty(tt, getApplicationsResp) - assert.Len(tt, getApplicationsResp.Applications, 1) - - // get the application - req = httptest.NewRequest(http.MethodGet, fmt.Sprintf("https://ssi-service.com/v1/manifests/applications/%s", getApplicationsResp.Applications[0].ID), nil) - c = newRequestContextWithParams(w, req, map[string]string{"id": getApplicationsResp.Applications[0].ID}) - manifestRouter.GetApplication(c) - assert.True(tt, util.Is2xxResponse(w.Code)) - - var getApplicationResp router.GetApplicationResponse - err = json.NewDecoder(w.Body).Decode(&getApplicationResp) - assert.NoError(tt, err) - assert.NotEmpty(tt, getApplicationResp) - assert.Equal(tt, resp.Manifest.ID, getApplicationResp.Application.ManifestID) - - // delete the application - req = httptest.NewRequest(http.MethodDelete, fmt.Sprintf("https://ssi-service.com/v1/manifests/applications/%s", getApplicationResp.Application.ID), nil) - c = newRequestContextWithParams(w, req, map[string]string{"id": getApplicationsResp.Applications[0].ID}) - manifestRouter.DeleteApplication(c) - assert.True(tt, util.Is2xxResponse(w.Code)) - - w = httptest.NewRecorder() - - // get it back - req = httptest.NewRequest(http.MethodGet, fmt.Sprintf("https://ssi-service.com/v1/manifests/applications/%s", appResp.Response.ID), nil) - c = newRequestContextWithParams(w, req, map[string]string{"id": getApplicationsResp.Applications[0].ID}) - manifestRouter.GetApplication(c) - assert.Contains(tt, w.Body.String(), fmt.Sprintf("could not get application with id: %s", appResp.Response.ID)) - }) + } } func getValidManifestRequestRequest(issuerDID *did.CreateDIDResponse, kid string, credentialManifest manifest.CredentialManifest) router.CreateManifestRequestRequest { diff --git a/pkg/server/server_operation_test.go b/pkg/server/server_operation_test.go index 6fda77e39..5bfc32157 100644 --- a/pkg/server/server_operation_test.go +++ b/pkg/server/server_operation_test.go @@ -5,7 +5,7 @@ import ( "net/http" "net/http/httptest" "net/url" - "os" + "strings" "testing" "github.com/TBD54566975/ssi-sdk/credential/exchange" @@ -13,278 +13,272 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "github.com/tbd54566975/ssi-service/internal/util" "github.com/tbd54566975/ssi-service/pkg/server/router" "github.com/tbd54566975/ssi-service/pkg/service/operation" opstorage "github.com/tbd54566975/ssi-service/pkg/service/operation/storage" "github.com/tbd54566975/ssi-service/pkg/storage" + "github.com/tbd54566975/ssi-service/pkg/testutil" ) func TestOperationsAPI(t *testing.T) { - t.Run("Marks operation as done after reviewing submission", func(tt *testing.T) { - s := setupTestDB(tt) - pRouter, didService := setupPresentationRouter(tt, s) - authorDID := createDID(tt, didService) - opRouter := setupOperationsRouter(tt, s) - - holderSigner, holderDID := getSigner(tt) - definition := createPresentationDefinition(t, pRouter) - submissionOp := createSubmission(t, pRouter, definition.PresentationDefinition.ID, authorDID.DID.ID, VerifiableCredential(), holderDID, holderSigner) - sub := reviewSubmission(t, pRouter, opstorage.StatusObjectID(submissionOp.ID)) - - createdID := submissionOp.ID - req := httptest.NewRequest(http.MethodPut, fmt.Sprintf("https://ssi-service.com/v1/operations/%s", createdID), nil) - w := httptest.NewRecorder() - - c := newRequestContextWithParams(w, req, map[string]string{"id": createdID}) - opRouter.GetOperation(c) - assert.True(tt, util.Is2xxResponse(w.Code)) - - var resp router.Operation - assert.NoError(tt, json.NewDecoder(w.Body).Decode(&resp)) - assert.True(tt, resp.Done) - assert.Empty(tt, resp.Result.Error) - data, err := json.Marshal(sub) - assert.NoError(tt, err) - - var responseAsMap map[string]any - assert.NoError(tt, json.Unmarshal(data, &responseAsMap)) - assert.Equal(tt, responseAsMap, resp.Result.Response) - }) - - t.Run("GetOperation", func(tt *testing.T) { - tt.Run("Returns operation after submission", func(ttt *testing.T) { - s := setupTestDB(ttt) - pRouter, didService := setupPresentationRouter(ttt, s) - authorDID := createDID(ttt, didService) - opRouter := setupOperationsRouter(ttt, s) - - holderSigner, holderDID := getSigner(ttt) - definition := createPresentationDefinition(ttt, pRouter) - submissionOp := createSubmission(ttt, pRouter, definition.PresentationDefinition.ID, authorDID.DID.ID, VerifiableCredential(), holderDID, holderSigner) - - createdID := submissionOp.ID - req := httptest.NewRequest( - http.MethodPut, - fmt.Sprintf("https://ssi-service.com/v1/operations/%s", createdID), - nil) - w := httptest.NewRecorder() - - c := newRequestContextWithParams(w, req, map[string]string{"id": createdID}) - opRouter.GetOperation(c) - assert.True(tt, util.Is2xxResponse(w.Code)) - - var resp router.Operation - assert.NoError(ttt, json.NewDecoder(w.Body).Decode(&resp)) - assert.False(ttt, resp.Done) - assert.Contains(ttt, resp.ID, "presentations/submissions/") - }) - - tt.Run("Returns error when id doesn't exist", func(ttt *testing.T) { - s := setupTestDB(ttt) - opRouter := setupOperationsRouter(ttt, s) - - req := httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/operations/some_fake_id", nil) - w := httptest.NewRecorder() - - c := newRequestContextWithParams(w, req, map[string]string{"id": "some_fake_id"}) - opRouter.GetOperation(c) - assert.Contains(ttt, w.Body.String(), "operation not found with id") + for _, test := range testutil.TestDatabases { + t.Run(test.Name, func(t *testing.T) { + t.Run("Marks operation as done after reviewing submission", func(tt *testing.T) { + s := test.ServiceStorage(t) + pRouter, didService := setupPresentationRouter(tt, s) + authorDID := createDID(tt, didService) + opRouter := setupOperationsRouter(tt, s) + + holderSigner, holderDID := getSigner(tt) + definition := createPresentationDefinition(t, pRouter) + submissionOp := createSubmission(t, pRouter, definition.PresentationDefinition.ID, authorDID.DID.ID, VerifiableCredential(), holderDID, holderSigner) + sub := reviewSubmission(t, pRouter, opstorage.StatusObjectID(submissionOp.ID)) + + createdID := submissionOp.ID + req := httptest.NewRequest(http.MethodPut, fmt.Sprintf("https://ssi-service.com/v1/operations/%s", createdID), nil) + w := httptest.NewRecorder() + + c := newRequestContextWithParams(w, req, map[string]string{"id": createdID}) + opRouter.GetOperation(c) + assert.True(tt, util.Is2xxResponse(w.Code)) + + var resp router.Operation + assert.NoError(tt, json.NewDecoder(w.Body).Decode(&resp)) + assert.True(tt, resp.Done) + assert.Empty(tt, resp.Result.Error) + data, err := json.Marshal(sub) + assert.NoError(tt, err) + + var responseAsMap map[string]any + assert.NoError(tt, json.Unmarshal(data, &responseAsMap)) + assert.Equal(tt, responseAsMap, resp.Result.Response) + }) + + t.Run("GetOperation", func(tt *testing.T) { + tt.Run("Returns operation after submission", func(ttt *testing.T) { + s := test.ServiceStorage(t) + pRouter, didService := setupPresentationRouter(ttt, s) + authorDID := createDID(ttt, didService) + opRouter := setupOperationsRouter(ttt, s) + + holderSigner, holderDID := getSigner(ttt) + definition := createPresentationDefinition(ttt, pRouter) + submissionOp := createSubmission(ttt, pRouter, definition.PresentationDefinition.ID, authorDID.DID.ID, VerifiableCredential(), holderDID, holderSigner) + + createdID := submissionOp.ID + req := httptest.NewRequest( + http.MethodPut, + fmt.Sprintf("https://ssi-service.com/v1/operations/%s", createdID), + nil) + w := httptest.NewRecorder() + + c := newRequestContextWithParams(w, req, map[string]string{"id": createdID}) + opRouter.GetOperation(c) + assert.True(tt, util.Is2xxResponse(w.Code)) + + var resp router.Operation + assert.NoError(ttt, json.NewDecoder(w.Body).Decode(&resp)) + assert.False(ttt, resp.Done) + assert.Contains(ttt, resp.ID, "presentations/submissions/") + }) + + tt.Run("Returns error when id doesn't exist", func(ttt *testing.T) { + s := test.ServiceStorage(t) + opRouter := setupOperationsRouter(ttt, s) + + req := httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/operations/some_fake_id", nil) + w := httptest.NewRecorder() + + c := newRequestContextWithParams(w, req, map[string]string{"id": "some_fake_id"}) + opRouter.GetOperation(c) + assert.Contains(ttt, w.Body.String(), "operation not found with id") + }) + }) + + t.Run("ListOperations", func(tt *testing.T) { + tt.Run("Returns empty when no operations stored", func(ttt *testing.T) { + s := test.ServiceStorage(t) + opRouter := setupOperationsRouter(ttt, s) + + query := url.QueryEscape("presentations/submissions") + req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("https://ssi-service.com/v1/operations?parent=%s", query), nil) + w := httptest.NewRecorder() + + c := newRequestContextWithParams(w, req, map[string]string{"parent": query}) + opRouter.ListOperations(c) + assert.True(tt, util.Is2xxResponse(w.Code)) + + var resp router.ListOperationsResponse + assert.NoError(ttt, json.NewDecoder(w.Body).Decode(&resp)) + assert.Empty(ttt, resp.Operations) + }) + + tt.Run("Returns one operation for every submission", func(ttt *testing.T) { + s := test.ServiceStorage(t) + pRouter, didService := setupPresentationRouter(ttt, s) + authorDID := createDID(ttt, didService) + opRouter := setupOperationsRouter(ttt, s) + + def := createPresentationDefinition(ttt, pRouter) + holderSigner, holderDID := getSigner(ttt) + submissionOp := createSubmission(ttt, pRouter, def.PresentationDefinition.ID, authorDID.DID.ID, VerifiableCredential(), holderDID, holderSigner) + + holderSigner2, holderDID2 := getSigner(ttt) + submissionOp2 := createSubmission(ttt, pRouter, def.PresentationDefinition.ID, authorDID.DID.ID, VerifiableCredential(), holderDID2, holderSigner2) + + query := url.QueryEscape("presentations/submissions") + req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("https://ssi-service.com/v1/operations?parent=%s", query), nil) + w := httptest.NewRecorder() + + c := newRequestContextWithParams(w, req, map[string]string{"parent": query}) + opRouter.ListOperations(c) + assert.True(tt, util.Is2xxResponse(w.Code)) + + var resp router.ListOperationsResponse + assert.NoError(ttt, json.NewDecoder(w.Body).Decode(&resp)) + ops := []router.Operation{submissionOp, submissionOp2} + diff := cmp.Diff(ops, resp.Operations, + cmpopts.IgnoreFields(exchange.PresentationSubmission{}, "DescriptorMap"), + cmpopts.SortSlices(func(l, r router.Operation) bool { + return l.ID < r.ID + }), + ) + if diff != "" { + ttt.Errorf("Mismatch on submissions (-want +got):\n%s", diff) + } + }) + + tt.Run("Returns operation when filtering to include", func(ttt *testing.T) { + s := test.ServiceStorage(t) + pRouter, didService := setupPresentationRouter(ttt, s) + authorDID := createDID(ttt, didService) + opRouter := setupOperationsRouter(ttt, s) + + def := createPresentationDefinition(ttt, pRouter) + holderSigner, holderDID := getSigner(ttt) + _ = createSubmission(ttt, pRouter, def.PresentationDefinition.ID, authorDID.DID.ID, VerifiableCredential(), holderDID, holderSigner) + + queryParent := url.QueryEscape("presentations/submissions") + queryDone := url.QueryEscape("done=false") + req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("https://ssi-service.com/v1/operations?parent=%s&filter=%s", queryParent, queryDone), nil) + w := httptest.NewRecorder() + + c := newRequestContextWithParams(w, req, map[string]string{"parent": queryParent, "done": queryDone}) + opRouter.ListOperations(c) + assert.True(tt, util.Is2xxResponse(w.Code)) + + var resp router.ListOperationsResponse + assert.NoError(ttt, json.NewDecoder(w.Body).Decode(&resp)) + assert.Len(ttt, resp.Operations, 1) + assert.False(ttt, resp.Operations[0].Done) + }) + + // TODO: Fix pagesize issue on redis - https://github.com/TBD54566975/ssi-service/issues/538 + if !strings.Contains(test.Name, "Redis") { + tt.Run("Returns zero operations when filtering to exclude", func(ttt *testing.T) { + + s := test.ServiceStorage(t) + pRouter, didService := setupPresentationRouter(ttt, s) + authorDID := createDID(ttt, didService) + opRouter := setupOperationsRouter(ttt, s) + + def := createPresentationDefinition(ttt, pRouter) + holderSigner, holderDID := getSigner(ttt) + _ = createSubmission(ttt, pRouter, def.PresentationDefinition.ID, authorDID.DID.ID, VerifiableCredential(), holderDID, holderSigner) + + queryParent := url.QueryEscape("presentations/submissions") + queryDone := url.QueryEscape("done=true") + sprintf := fmt.Sprintf("https://ssi-service.com/v1/operations?parent=%s&filter=%s", queryParent, queryDone) + req := httptest.NewRequest(http.MethodGet, sprintf, nil) + w := httptest.NewRecorder() + + c := newRequestContextWithParams(w, req, map[string]string{"parent": queryParent, "filter": queryDone}) + opRouter.ListOperations(c) + assert.True(tt, util.Is2xxResponse(w.Code)) + + var resp router.ListOperationsResponse + assert.NoError(ttt, json.NewDecoder(w.Body).Decode(&resp)) + assert.Empty(ttt, resp.Operations) + }) + } + + // TODO: Fix pagesize issue on redis - https://github.com/TBD54566975/ssi-service/issues/538 + if !strings.Contains(test.Name, "Redis") { + tt.Run("Returns zero operations when wrong parent is specified", func(ttt *testing.T) { + + s := test.ServiceStorage(t) + pRouter, didService := setupPresentationRouter(ttt, s) + authorDID := createDID(ttt, didService) + opRouter := setupOperationsRouter(ttt, s) + + def := createPresentationDefinition(ttt, pRouter) + holderSigner, holderDID := getSigner(ttt) + _ = createSubmission(ttt, pRouter, def.PresentationDefinition.ID, authorDID.DID.ID, VerifiableCredential(), holderDID, holderSigner) + + queryParent := url.QueryEscape("/presentations/other") + req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("https://ssi-service.com/v1/operations?parent=%s", queryParent), nil) + w := httptest.NewRecorder() + + c := newRequestContextWithParams(w, req, map[string]string{"parent": queryParent}) + opRouter.ListOperations(c) + assert.True(tt, util.Is2xxResponse(w.Code)) + + var resp router.ListOperationsResponse + assert.NoError(ttt, json.NewDecoder(w.Body).Decode(&resp)) + assert.Empty(ttt, resp.Operations) + + }) + } + }) + + t.Run("CancelOperation", func(tt *testing.T) { + tt.Run("Marks an operation as done", func(ttt *testing.T) { + s := test.ServiceStorage(t) + pRouter, didService := setupPresentationRouter(ttt, s) + authorDID := createDID(ttt, didService) + opRouter := setupOperationsRouter(ttt, s) + + holderSigner, holderDID := getSigner(ttt) + definition := createPresentationDefinition(ttt, pRouter) + submissionOp := createSubmission(ttt, pRouter, definition.PresentationDefinition.ID, authorDID.DID.ID, VerifiableCredential(), holderDID, holderSigner) + + createdID := submissionOp.ID + req := httptest.NewRequest(http.MethodPut, fmt.Sprintf("https://ssi-service.com/v1/operations/%s", createdID), nil) + w := httptest.NewRecorder() + + c := newRequestContextWithParams(w, req, map[string]string{"id": createdID}) + opRouter.CancelOperation(c) + assert.True(tt, util.Is2xxResponse(w.Code)) + + var resp router.Operation + assert.NoError(ttt, json.NewDecoder(w.Body).Decode(&resp)) + assert.True(ttt, resp.Done) + assert.Contains(ttt, resp.Result.Response, "verifiablePresentation") + assert.Equal(ttt, "cancelled", resp.Result.Response.(map[string]any)["status"]) + }) + + tt.Run("Returns error when operation is done already", func(ttt *testing.T) { + s := test.ServiceStorage(t) + pRouter, didService := setupPresentationRouter(ttt, s) + authorDID := createDID(ttt, didService) + opRouter := setupOperationsRouter(ttt, s) + + holderSigner, holderDID := getSigner(ttt) + definition := createPresentationDefinition(ttt, pRouter) + submissionOp := createSubmission(ttt, pRouter, definition.PresentationDefinition.ID, authorDID.DID.ID, VerifiableCredential(), holderDID, holderSigner) + _ = reviewSubmission(ttt, pRouter, opstorage.StatusObjectID(submissionOp.ID)) + + createdID := submissionOp.ID + req := httptest.NewRequest(http.MethodPut, fmt.Sprintf("https://ssi-service.com/v1/operations/%s", createdID), nil) + w := httptest.NewRecorder() + c := newRequestContextWithParams(w, req, map[string]string{"id": createdID}) + opRouter.CancelOperation(c) + assert.Contains(ttt, w.Body.String(), "operation already marked as done") + }) + }) }) - }) - - t.Run("ListOperations", func(tt *testing.T) { - tt.Run("Returns empty when no operations stored", func(ttt *testing.T) { - s := setupTestDB(ttt) - opRouter := setupOperationsRouter(ttt, s) - - query := url.QueryEscape("presentations/submissions") - req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("https://ssi-service.com/v1/operations?parent=%s", query), nil) - w := httptest.NewRecorder() - - c := newRequestContextWithParams(w, req, map[string]string{"parent": query}) - opRouter.ListOperations(c) - assert.True(tt, util.Is2xxResponse(w.Code)) - - var resp router.ListOperationsResponse - assert.NoError(ttt, json.NewDecoder(w.Body).Decode(&resp)) - assert.Empty(ttt, resp.Operations) - }) - - tt.Run("Returns one operation for every submission", func(ttt *testing.T) { - s := setupTestDB(ttt) - pRouter, didService := setupPresentationRouter(ttt, s) - authorDID := createDID(ttt, didService) - opRouter := setupOperationsRouter(ttt, s) - - def := createPresentationDefinition(ttt, pRouter) - holderSigner, holderDID := getSigner(ttt) - submissionOp := createSubmission(ttt, pRouter, def.PresentationDefinition.ID, authorDID.DID.ID, VerifiableCredential(), holderDID, holderSigner) - - holderSigner2, holderDID2 := getSigner(ttt) - submissionOp2 := createSubmission(ttt, pRouter, def.PresentationDefinition.ID, authorDID.DID.ID, VerifiableCredential(), holderDID2, holderSigner2) - - query := url.QueryEscape("presentations/submissions") - req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("https://ssi-service.com/v1/operations?parent=%s", query), nil) - w := httptest.NewRecorder() - - c := newRequestContextWithParams(w, req, map[string]string{"parent": query}) - opRouter.ListOperations(c) - assert.True(tt, util.Is2xxResponse(w.Code)) - - var resp router.ListOperationsResponse - assert.NoError(ttt, json.NewDecoder(w.Body).Decode(&resp)) - ops := []router.Operation{submissionOp, submissionOp2} - diff := cmp.Diff(ops, resp.Operations, - cmpopts.IgnoreFields(exchange.PresentationSubmission{}, "DescriptorMap"), - cmpopts.SortSlices(func(l, r router.Operation) bool { - return l.ID < r.ID - }), - ) - if diff != "" { - ttt.Errorf("Mismatch on submissions (-want +got):\n%s", diff) - } - }) - - tt.Run("Returns operation when filtering to include", func(ttt *testing.T) { - s := setupTestDB(ttt) - pRouter, didService := setupPresentationRouter(ttt, s) - authorDID := createDID(ttt, didService) - opRouter := setupOperationsRouter(ttt, s) - - def := createPresentationDefinition(ttt, pRouter) - holderSigner, holderDID := getSigner(ttt) - _ = createSubmission(ttt, pRouter, def.PresentationDefinition.ID, authorDID.DID.ID, VerifiableCredential(), holderDID, holderSigner) - - queryParent := url.QueryEscape("presentations/submissions") - queryDone := url.QueryEscape("done=false") - req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("https://ssi-service.com/v1/operations?parent=%s&filter=%s", queryParent, queryDone), nil) - w := httptest.NewRecorder() - - c := newRequestContextWithParams(w, req, map[string]string{"parent": queryParent, "done": queryDone}) - opRouter.ListOperations(c) - assert.True(tt, util.Is2xxResponse(w.Code)) - - var resp router.ListOperationsResponse - assert.NoError(ttt, json.NewDecoder(w.Body).Decode(&resp)) - assert.Len(ttt, resp.Operations, 1) - assert.False(ttt, resp.Operations[0].Done) - }) - - tt.Run("Returns zero operations when filtering to exclude", func(ttt *testing.T) { - s := setupTestDB(ttt) - pRouter, didService := setupPresentationRouter(ttt, s) - authorDID := createDID(ttt, didService) - opRouter := setupOperationsRouter(ttt, s) - - def := createPresentationDefinition(ttt, pRouter) - holderSigner, holderDID := getSigner(ttt) - _ = createSubmission(ttt, pRouter, def.PresentationDefinition.ID, authorDID.DID.ID, VerifiableCredential(), holderDID, holderSigner) - - queryParent := url.QueryEscape("presentations/submissions") - queryDone := url.QueryEscape("done=true") - sprintf := fmt.Sprintf("https://ssi-service.com/v1/operations?parent=%s&filter=%s", queryParent, queryDone) - req := httptest.NewRequest(http.MethodGet, sprintf, nil) - w := httptest.NewRecorder() - - c := newRequestContextWithParams(w, req, map[string]string{"parent": queryParent, "filter": queryDone}) - opRouter.ListOperations(c) - assert.True(tt, util.Is2xxResponse(w.Code)) - - var resp router.ListOperationsResponse - assert.NoError(ttt, json.NewDecoder(w.Body).Decode(&resp)) - assert.Empty(ttt, resp.Operations) - }) - - tt.Run("Returns zero operations when wrong parent is specified", func(ttt *testing.T) { - s := setupTestDB(ttt) - pRouter, didService := setupPresentationRouter(ttt, s) - authorDID := createDID(ttt, didService) - opRouter := setupOperationsRouter(ttt, s) - - def := createPresentationDefinition(ttt, pRouter) - holderSigner, holderDID := getSigner(ttt) - _ = createSubmission(ttt, pRouter, def.PresentationDefinition.ID, authorDID.DID.ID, VerifiableCredential(), holderDID, holderSigner) - - queryParent := url.QueryEscape("/presentations/other") - req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("https://ssi-service.com/v1/operations?parent=%s", queryParent), nil) - w := httptest.NewRecorder() - - c := newRequestContextWithParams(w, req, map[string]string{"parent": queryParent}) - opRouter.ListOperations(c) - assert.True(tt, util.Is2xxResponse(w.Code)) - - var resp router.ListOperationsResponse - assert.NoError(ttt, json.NewDecoder(w.Body).Decode(&resp)) - assert.Empty(ttt, resp.Operations) - }) - }) - - t.Run("CancelOperation", func(tt *testing.T) { - tt.Run("Marks an operation as done", func(ttt *testing.T) { - s := setupTestDB(ttt) - pRouter, didService := setupPresentationRouter(ttt, s) - authorDID := createDID(ttt, didService) - opRouter := setupOperationsRouter(ttt, s) - - holderSigner, holderDID := getSigner(ttt) - definition := createPresentationDefinition(ttt, pRouter) - submissionOp := createSubmission(ttt, pRouter, definition.PresentationDefinition.ID, authorDID.DID.ID, VerifiableCredential(), holderDID, holderSigner) - - createdID := submissionOp.ID - req := httptest.NewRequest(http.MethodPut, fmt.Sprintf("https://ssi-service.com/v1/operations/%s", createdID), nil) - w := httptest.NewRecorder() - - c := newRequestContextWithParams(w, req, map[string]string{"id": createdID}) - opRouter.CancelOperation(c) - assert.True(tt, util.Is2xxResponse(w.Code)) - - var resp router.Operation - assert.NoError(ttt, json.NewDecoder(w.Body).Decode(&resp)) - assert.True(ttt, resp.Done) - assert.Contains(ttt, resp.Result.Response, "verifiablePresentation") - assert.Equal(ttt, "cancelled", resp.Result.Response.(map[string]any)["status"]) - }) - - tt.Run("Returns error when operation is done already", func(ttt *testing.T) { - s := setupTestDB(ttt) - pRouter, didService := setupPresentationRouter(ttt, s) - authorDID := createDID(ttt, didService) - opRouter := setupOperationsRouter(ttt, s) - - holderSigner, holderDID := getSigner(ttt) - definition := createPresentationDefinition(ttt, pRouter) - submissionOp := createSubmission(ttt, pRouter, definition.PresentationDefinition.ID, authorDID.DID.ID, VerifiableCredential(), holderDID, holderSigner) - _ = reviewSubmission(ttt, pRouter, opstorage.StatusObjectID(submissionOp.ID)) - - createdID := submissionOp.ID - req := httptest.NewRequest(http.MethodPut, fmt.Sprintf("https://ssi-service.com/v1/operations/%s", createdID), nil) - w := httptest.NewRecorder() - c := newRequestContextWithParams(w, req, map[string]string{"id": createdID}) - opRouter.CancelOperation(c) - assert.Contains(ttt, w.Body.String(), "operation already marked as done") - }) - }) - -} - -func setupTestDB(t *testing.T) storage.ServiceStorage { - file, err := os.CreateTemp("", "bolt") - require.NoError(t, err) - name := file.Name() - s, err := storage.NewStorage(storage.Bolt, storage.Option{ - ID: storage.BoltDBFilePathOption, - Option: name, - }) - require.NoError(t, err) - t.Cleanup(func() { - _ = s.Close() - _ = file.Close() - _ = os.Remove(name) - }) - return s + } } func reviewSubmission(t *testing.T, pRouter *router.PresentationRouter, submissionID string) router.ReviewSubmissionResponse { diff --git a/pkg/server/server_presentation_test.go b/pkg/server/server_presentation_test.go index ece4a61c7..3af130d20 100644 --- a/pkg/server/server_presentation_test.go +++ b/pkg/server/server_presentation_test.go @@ -20,6 +20,7 @@ import ( "github.com/google/uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/tbd54566975/ssi-service/pkg/testutil" "github.com/tbd54566975/ssi-service/config" "github.com/tbd54566975/ssi-service/internal/keyaccess" @@ -51,508 +52,511 @@ func TestPresentationAPI(t *testing.T) { pd, err := builder.Build() assert.NoError(t, err) - t.Run("Create, Get, and Delete PresentationDefinition", func(tt *testing.T) { - s := setupTestDB(tt) - pRouter, _ := setupPresentationRouter(tt, s) + for _, test := range testutil.TestDatabases { + t.Run(test.Name, func(t *testing.T) { + t.Run("Create, Get, and Delete PresentationDefinition", func(tt *testing.T) { + s := test.ServiceStorage(t) + pRouter, _ := setupPresentationRouter(tt, s) - var createdID string - { - resp := createPresentationDefinition(tt, pRouter, WithInputDescriptors(inputDescriptors)) - if diff := cmp.Diff(*pd, resp.PresentationDefinition, cmpopts.IgnoreFields(exchange.PresentationDefinition{}, "ID")); diff != "" { - t.Errorf("PresentationDefinition mismatch (-want +got):\n%s", diff) - } - - createdID = resp.PresentationDefinition.ID - } - { - // We can get the PD after it's created. - req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("https://ssi-service.com/v1/presentations/definitions/%s", createdID), nil) - w := httptest.NewRecorder() - c := newRequestContextWithParams(w, req, map[string]string{"id": createdID}) - pRouter.GetDefinition(c) - assert.True(tt, util.Is2xxResponse(w.Code)) - - var resp router.GetPresentationDefinitionResponse - assert.NoError(tt, json.NewDecoder(w.Body).Decode(&resp)) - assert.Equal(tt, createdID, resp.PresentationDefinition.ID) - if diff := cmp.Diff(*pd, resp.PresentationDefinition, cmpopts.IgnoreFields(exchange.PresentationDefinition{}, "ID")); diff != "" { - t.Errorf("PresentationDefinition mismatch (-want +got):\n%s", diff) - } - } - { - // And it can also be listed - req := httptest.NewRequest(http.MethodGet, "https://ssi-service.com/v1/presentations/definitions", nil) - w := httptest.NewRecorder() - - c := newRequestContext(w, req) - pRouter.ListDefinitions(c) - assert.True(tt, util.Is2xxResponse(w.Code)) - - var resp router.ListDefinitionsResponse - assert.NoError(tt, json.NewDecoder(w.Body).Decode(&resp)) - assert.Len(tt, resp.Definitions, 1) - assert.Equal(tt, createdID, resp.Definitions[0].ID) - if diff := cmp.Diff(pd, resp.Definitions[0], cmpopts.IgnoreFields(exchange.PresentationDefinition{}, "ID")); diff != "" { - t.Errorf("PresentationDefinition mismatch (-want +got):\n%s", diff) - } - } - { - // The PD can be deleted. - req := httptest.NewRequest(http.MethodDelete, fmt.Sprintf("https://ssi-service.com/v1/presentations/definitions/%s", createdID), nil) - w := httptest.NewRecorder() - c := newRequestContextWithParams(w, req, map[string]string{"id": createdID}) - pRouter.DeleteDefinition(c) - assert.True(tt, util.Is2xxResponse(w.Code)) - } - { - // And we cannot get the PD after it's been deleted. - req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("https://ssi-service.com/v1/presentations/definitions/%s", createdID), nil) - w := httptest.NewRecorder() - c := newRequestContextWithParams(w, req, map[string]string{"id": createdID}) - pRouter.GetDefinition(c) - assert.Contains(tt, w.Body.String(), "not found") - } - }) - - t.Run("List presentation requests returns empty", func(tt *testing.T) { - s := setupTestDB(tt) - pRouter, _ := setupPresentationRouter(tt, s) - - req := httptest.NewRequest(http.MethodGet, "https://ssi-service.com/v1/presentations/requests", nil) - w := httptest.NewRecorder() - c := newRequestContext(w, req) - pRouter.ListRequests(c) - assert.True(tt, util.Is2xxResponse(w.Code)) - - var resp router.ListPresentationRequestsResponse - assert.NoError(tt, json.NewDecoder(w.Body).Decode(&resp)) - assert.Empty(tt, resp.Requests) - }) - - t.Run("List presentation requests returns many requests", func(tt *testing.T) { - s := setupTestDB(tt) - pRouter, didService := setupPresentationRouter(tt, s) - issuerDID := createDID(tt, didService) - def := createPresentationDefinition(tt, pRouter) - req1 := createPresentationRequest(tt, pRouter, def.PresentationDefinition.ID, issuerDID.DID) - req2 := createPresentationRequest(tt, pRouter, def.PresentationDefinition.ID, issuerDID.DID) - - req := httptest.NewRequest(http.MethodGet, "https://ssi-service.com/v1/presentations/requests", nil) - w := httptest.NewRecorder() - c := newRequestContext(w, req) - pRouter.ListRequests(c) - assert.True(tt, util.Is2xxResponse(w.Code)) - - var resp router.ListPresentationRequestsResponse - assert.NoError(tt, json.NewDecoder(w.Body).Decode(&resp)) - assert.Len(tt, resp.Requests, 2) - assert.ElementsMatch(tt, resp.Requests, []model.Request{ - *req1.Request, - *req2.Request, - }) - }) - - t.Run("List definitions returns empty", func(tt *testing.T) { - s := setupTestDB(tt) - pRouter, _ := setupPresentationRouter(tt, s) - - req := httptest.NewRequest(http.MethodGet, "https://ssi-service.com/v1/presentations/definitions", nil) - w := httptest.NewRecorder() - c := newRequestContext(w, req) - pRouter.ListDefinitions(c) - assert.True(tt, util.Is2xxResponse(w.Code)) - - var resp router.ListDefinitionsResponse - assert.NoError(tt, json.NewDecoder(w.Body).Decode(&resp)) - assert.Empty(tt, resp.Definitions) - }) - - t.Run("List definitions returns many definitions", func(tt *testing.T) { - s := setupTestDB(tt) - pRouter, _ := setupPresentationRouter(tt, s) - def1 := createPresentationDefinition(tt, pRouter) - def2 := createPresentationDefinition(tt, pRouter) - - req := httptest.NewRequest(http.MethodGet, "https://ssi-service.com/v1/presentations/definitions", nil) - w := httptest.NewRecorder() - c := newRequestContext(w, req) - pRouter.ListDefinitions(c) - assert.True(tt, util.Is2xxResponse(w.Code)) - - var resp router.ListDefinitionsResponse - assert.NoError(tt, json.NewDecoder(w.Body).Decode(&resp)) - assert.Len(tt, resp.Definitions, 2) - assert.ElementsMatch(tt, resp.Definitions, []*exchange.PresentationDefinition{ - &def1.PresentationDefinition, - &def2.PresentationDefinition, - }) - }) - - t.Run("Create returns error without input descriptors", func(tt *testing.T) { - s := setupTestDB(tt) - pRouter, _ := setupPresentationRouter(tt, s) - request := router.CreatePresentationDefinitionRequest{} - value := newRequestValue(tt, request) - req := httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/presentations/definitions", value) - w := httptest.NewRecorder() - - c := newRequestContext(w, req) - pRouter.CreateDefinition(c) - assert.Contains(t, w.Body.String(), "inputDescriptors is a required field") - }) - - t.Run("Get without an ID returns error", func(tt *testing.T) { - s := setupTestDB(tt) - pRouter, _ := setupPresentationRouter(tt, s) - req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("https://ssi-service.com/v1/presentations/definitions/%s", pd.ID), nil) - w := httptest.NewRecorder() - - c := newRequestContextWithParams(w, req, map[string]string{"id": pd.ID}) - pRouter.GetDefinition(c) - assert.Contains(t, w.Body.String(), "not found") - }) - - t.Run("Delete without an ID returns error", func(tt *testing.T) { - s := setupTestDB(tt) - pRouter, _ := setupPresentationRouter(tt, s) - - req := httptest.NewRequest(http.MethodDelete, fmt.Sprintf("https://ssi-service.com/v1/presentations/definitions/%s", pd.ID), nil) - w := httptest.NewRecorder() - c := newRequestContextWithParams(w, req, map[string]string{"id": pd.ID}) - pRouter.DeleteDefinition(c) - assert.Contains(t, w.Body.String(), fmt.Sprintf("could not delete presentation definition with id: %s", pd.ID)) - }) - - t.Run("Submission endpoints", func(tt *testing.T) { - tt.Run("Get non-existing ID returns error", func(ttt *testing.T) { - s := setupTestDB(ttt) - pRouter, _ := setupPresentationRouter(ttt, s) - - req := httptest.NewRequest(http.MethodGet, "https://ssi-service.com/v1/presentations/submissions/myrandomid", nil) - w := httptest.NewRecorder() - c := newRequestContextWithParams(w, req, map[string]string{"id": "myrandomid"}) - pRouter.GetSubmission(c) - assert.Contains(ttt, w.Body.String(), "not found") - }) + var createdID string + { + resp := createPresentationDefinition(tt, pRouter, WithInputDescriptors(inputDescriptors)) + if diff := cmp.Diff(*pd, resp.PresentationDefinition, cmpopts.IgnoreFields(exchange.PresentationDefinition{}, "ID")); diff != "" { + t.Errorf("PresentationDefinition mismatch (-want +got):\n%s", diff) + } - tt.Run("Get returns submission after creation", func(ttt *testing.T) { - s := setupTestDB(ttt) - pRouter, didService := setupPresentationRouter(ttt, s) - authorDID := createDID(ttt, didService) - - holderSigner, holderDID := getSigner(ttt) - definition := createPresentationDefinition(ttt, pRouter) - op := createSubmission(ttt, pRouter, definition.PresentationDefinition.ID, authorDID.DID.ID, VerifiableCredential( - WithCredentialSubject(credential.CredentialSubject{ - "additionalName": "McLovin", - "dateOfBirth": "1987-01-02", - "familyName": "Andres", - "givenName": "Uribe", - "id": "did:web:andresuribe.com", - })), holderDID, holderSigner) - - req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("https://ssi-service.com/v1/presentations/submissions/%s", opstorage.StatusObjectID(op.ID)), nil) - w := httptest.NewRecorder() - - c := newRequestContextWithParams(w, req, map[string]string{"id": opstorage.StatusObjectID(op.ID)}) - pRouter.GetSubmission(c) - assert.True(tt, util.Is2xxResponse(w.Code)) - - var resp router.GetSubmissionResponse - assert.NoError(ttt, json.NewDecoder(w.Body).Decode(&resp)) - assert.Equal(ttt, opstorage.StatusObjectID(op.ID), resp.GetSubmission().ID) - assert.Equal(ttt, definition.PresentationDefinition.ID, resp.GetSubmission().DefinitionID) - assert.Equal(ttt, "pending", resp.Submission.Status) - }) + createdID = resp.PresentationDefinition.ID + } + { + // We can get the PD after it's created. + req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("https://ssi-service.com/v1/presentations/definitions/%s", createdID), nil) + w := httptest.NewRecorder() + c := newRequestContextWithParams(w, req, map[string]string{"id": createdID}) + pRouter.GetDefinition(c) + assert.True(tt, util.Is2xxResponse(w.Code)) + + var resp router.GetPresentationDefinitionResponse + assert.NoError(tt, json.NewDecoder(w.Body).Decode(&resp)) + assert.Equal(tt, createdID, resp.PresentationDefinition.ID) + if diff := cmp.Diff(*pd, resp.PresentationDefinition, cmpopts.IgnoreFields(exchange.PresentationDefinition{}, "ID")); diff != "" { + t.Errorf("PresentationDefinition mismatch (-want +got):\n%s", diff) + } + } + { + // And it can also be listed + req := httptest.NewRequest(http.MethodGet, "https://ssi-service.com/v1/presentations/definitions", nil) + w := httptest.NewRecorder() + + c := newRequestContext(w, req) + pRouter.ListDefinitions(c) + assert.True(tt, util.Is2xxResponse(w.Code)) + + var resp router.ListDefinitionsResponse + assert.NoError(tt, json.NewDecoder(w.Body).Decode(&resp)) + assert.Len(tt, resp.Definitions, 1) + assert.Equal(tt, createdID, resp.Definitions[0].ID) + if diff := cmp.Diff(pd, resp.Definitions[0], cmpopts.IgnoreFields(exchange.PresentationDefinition{}, "ID")); diff != "" { + t.Errorf("PresentationDefinition mismatch (-want +got):\n%s", diff) + } + } + { + // The PD can be deleted. + req := httptest.NewRequest(http.MethodDelete, fmt.Sprintf("https://ssi-service.com/v1/presentations/definitions/%s", createdID), nil) + w := httptest.NewRecorder() + c := newRequestContextWithParams(w, req, map[string]string{"id": createdID}) + pRouter.DeleteDefinition(c) + assert.True(tt, util.Is2xxResponse(w.Code)) + } + { + // And we cannot get the PD after it's been deleted. + req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("https://ssi-service.com/v1/presentations/definitions/%s", createdID), nil) + w := httptest.NewRecorder() + c := newRequestContextWithParams(w, req, map[string]string{"id": createdID}) + pRouter.GetDefinition(c) + assert.Contains(tt, w.Body.String(), "not found") + } + }) + + t.Run("List presentation requests returns empty", func(tt *testing.T) { + s := test.ServiceStorage(t) + pRouter, _ := setupPresentationRouter(tt, s) + + req := httptest.NewRequest(http.MethodGet, "https://ssi-service.com/v1/presentations/requests", nil) + w := httptest.NewRecorder() + c := newRequestContext(w, req) + pRouter.ListRequests(c) + assert.True(tt, util.Is2xxResponse(w.Code)) + + var resp router.ListPresentationRequestsResponse + assert.NoError(tt, json.NewDecoder(w.Body).Decode(&resp)) + assert.Empty(tt, resp.Requests) + }) + + t.Run("List presentation requests returns many requests", func(tt *testing.T) { + s := test.ServiceStorage(t) + pRouter, didService := setupPresentationRouter(tt, s) + issuerDID := createDID(tt, didService) + def := createPresentationDefinition(tt, pRouter) + req1 := createPresentationRequest(tt, pRouter, def.PresentationDefinition.ID, issuerDID.DID) + req2 := createPresentationRequest(tt, pRouter, def.PresentationDefinition.ID, issuerDID.DID) + + req := httptest.NewRequest(http.MethodGet, "https://ssi-service.com/v1/presentations/requests", nil) + w := httptest.NewRecorder() + c := newRequestContext(w, req) + pRouter.ListRequests(c) + assert.True(tt, util.Is2xxResponse(w.Code)) + + var resp router.ListPresentationRequestsResponse + assert.NoError(tt, json.NewDecoder(w.Body).Decode(&resp)) + assert.Len(tt, resp.Requests, 2) + assert.ElementsMatch(tt, resp.Requests, []model.Request{ + *req1.Request, + *req2.Request, + }) + }) + + t.Run("List definitions returns empty", func(tt *testing.T) { + s := test.ServiceStorage(t) + pRouter, _ := setupPresentationRouter(tt, s) + + req := httptest.NewRequest(http.MethodGet, "https://ssi-service.com/v1/presentations/definitions", nil) + w := httptest.NewRecorder() + c := newRequestContext(w, req) + pRouter.ListDefinitions(c) + assert.True(tt, util.Is2xxResponse(w.Code)) + + var resp router.ListDefinitionsResponse + assert.NoError(tt, json.NewDecoder(w.Body).Decode(&resp)) + assert.Empty(tt, resp.Definitions) + }) + + t.Run("List definitions returns many definitions", func(tt *testing.T) { + s := test.ServiceStorage(t) + pRouter, _ := setupPresentationRouter(tt, s) + def1 := createPresentationDefinition(tt, pRouter) + def2 := createPresentationDefinition(tt, pRouter) + + req := httptest.NewRequest(http.MethodGet, "https://ssi-service.com/v1/presentations/definitions", nil) + w := httptest.NewRecorder() + c := newRequestContext(w, req) + pRouter.ListDefinitions(c) + assert.True(tt, util.Is2xxResponse(w.Code)) + + var resp router.ListDefinitionsResponse + assert.NoError(tt, json.NewDecoder(w.Body).Decode(&resp)) + assert.Len(tt, resp.Definitions, 2) + assert.ElementsMatch(tt, resp.Definitions, []*exchange.PresentationDefinition{ + &def1.PresentationDefinition, + &def2.PresentationDefinition, + }) + }) + + t.Run("Create returns error without input descriptors", func(tt *testing.T) { + s := test.ServiceStorage(t) + pRouter, _ := setupPresentationRouter(tt, s) + request := router.CreatePresentationDefinitionRequest{} + value := newRequestValue(tt, request) + req := httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/presentations/definitions", value) + w := httptest.NewRecorder() + + c := newRequestContext(w, req) + pRouter.CreateDefinition(c) + assert.Contains(t, w.Body.String(), "inputDescriptors is a required field") + }) + + t.Run("Get without an ID returns error", func(tt *testing.T) { + s := test.ServiceStorage(t) + pRouter, _ := setupPresentationRouter(tt, s) + req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("https://ssi-service.com/v1/presentations/definitions/%s", pd.ID), nil) + w := httptest.NewRecorder() + + c := newRequestContextWithParams(w, req, map[string]string{"id": pd.ID}) + pRouter.GetDefinition(c) + assert.Contains(t, w.Body.String(), "not found") + }) + + t.Run("Delete without an ID returns error", func(tt *testing.T) { + s := test.ServiceStorage(t) + pRouter, _ := setupPresentationRouter(tt, s) + + req := httptest.NewRequest(http.MethodDelete, fmt.Sprintf("https://ssi-service.com/v1/presentations/definitions/%s", pd.ID), nil) + w := httptest.NewRecorder() + c := newRequestContextWithParams(w, req, map[string]string{"id": pd.ID}) + pRouter.DeleteDefinition(c) + assert.Contains(t, w.Body.String(), fmt.Sprintf("could not delete presentation definition with id: %s", pd.ID)) + }) + + t.Run("Submission endpoints", func(tt *testing.T) { + tt.Run("Get non-existing ID returns error", func(ttt *testing.T) { + s := test.ServiceStorage(t) + pRouter, _ := setupPresentationRouter(ttt, s) + + req := httptest.NewRequest(http.MethodGet, "https://ssi-service.com/v1/presentations/submissions/myrandomid", nil) + w := httptest.NewRecorder() + c := newRequestContextWithParams(w, req, map[string]string{"id": "myrandomid"}) + pRouter.GetSubmission(c) + assert.Contains(ttt, w.Body.String(), "not found") + }) - tt.Run("Create well formed submission returns operation", func(ttt *testing.T) { - s := setupTestDB(ttt) - pRouter, didService := setupPresentationRouter(ttt, s) - authorDID := createDID(ttt, didService) - - holderSigner, holderDID := getSigner(ttt) - definition := createPresentationDefinition(ttt, pRouter) - request := createSubmissionRequest(ttt, definition.PresentationDefinition.ID, authorDID.DID.ID, VerifiableCredential( - WithCredentialSubject(credential.CredentialSubject{ - "additionalName": "McLovin", - "dateOfBirth": "1987-01-02", - "familyName": "Andres", - "givenName": "Uribe", - "id": "did:web:andresuribe.com", - })), holderSigner, holderDID) - - value := newRequestValue(ttt, request) - req := httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/presentations/submissions", value) - w := httptest.NewRecorder() - - c := newRequestContext(w, req) - pRouter.CreateSubmission(c) - assert.True(tt, util.Is2xxResponse(w.Code)) - - var resp router.Operation - assert.NoError(ttt, json.NewDecoder(w.Body).Decode(&resp)) - assert.Contains(ttt, resp.ID, "presentations/submissions/") - assert.False(ttt, resp.Done) - assert.Zero(ttt, resp.Result) - }) + tt.Run("Get returns submission after creation", func(ttt *testing.T) { + s := test.ServiceStorage(t) + pRouter, didService := setupPresentationRouter(ttt, s) + authorDID := createDID(ttt, didService) + + holderSigner, holderDID := getSigner(ttt) + definition := createPresentationDefinition(ttt, pRouter) + op := createSubmission(ttt, pRouter, definition.PresentationDefinition.ID, authorDID.DID.ID, VerifiableCredential( + WithCredentialSubject(credential.CredentialSubject{ + "additionalName": "McLovin", + "dateOfBirth": "1987-01-02", + "familyName": "Andres", + "givenName": "Uribe", + "id": "did:web:andresuribe.com", + })), holderDID, holderSigner) + + req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("https://ssi-service.com/v1/presentations/submissions/%s", opstorage.StatusObjectID(op.ID)), nil) + w := httptest.NewRecorder() + + c := newRequestContextWithParams(w, req, map[string]string{"id": opstorage.StatusObjectID(op.ID)}) + pRouter.GetSubmission(c) + assert.True(tt, util.Is2xxResponse(w.Code)) + + var resp router.GetSubmissionResponse + assert.NoError(ttt, json.NewDecoder(w.Body).Decode(&resp)) + assert.Equal(ttt, opstorage.StatusObjectID(op.ID), resp.GetSubmission().ID) + assert.Equal(ttt, definition.PresentationDefinition.ID, resp.GetSubmission().DefinitionID) + assert.Equal(ttt, "pending", resp.Submission.Status) + }) - tt.Run("Review submission returns approved submission", func(ttt *testing.T) { - s := setupTestDB(ttt) - pRouter, didService := setupPresentationRouter(ttt, s) - authorDID := createDID(ttt, didService) - - holderSigner, holderDID := getSigner(ttt) - definition := createPresentationDefinition(ttt, pRouter) - submissionOp := createSubmission(t, pRouter, definition.PresentationDefinition.ID, authorDID.DID.ID, VerifiableCredential(), holderDID, holderSigner) - - request := router.ReviewSubmissionRequest{ - Approved: true, - Reason: "because I want to", - } - - value := newRequestValue(ttt, request) - createdID := opstorage.StatusObjectID(submissionOp.ID) - req := httptest.NewRequest( - http.MethodPut, - fmt.Sprintf("https://ssi-service.com/v1/presentations/submissions/%s/review", createdID), - value) - w := httptest.NewRecorder() - - c := newRequestContextWithParams(w, req, map[string]string{"id": createdID}) - pRouter.ReviewSubmission(c) - assert.True(tt, util.Is2xxResponse(w.Code)) - - var resp router.ReviewSubmissionResponse - assert.NoError(ttt, json.NewDecoder(w.Body).Decode(&resp)) - assert.Equal(ttt, "because I want to", resp.Reason) - assert.NotEmpty(ttt, resp.GetSubmission().ID) - assert.Equal(ttt, "approved", resp.Status) - assert.Equal(ttt, definition.PresentationDefinition.ID, resp.GetSubmission().DefinitionID) - }) + tt.Run("Create well formed submission returns operation", func(ttt *testing.T) { + s := test.ServiceStorage(t) + pRouter, didService := setupPresentationRouter(ttt, s) + authorDID := createDID(ttt, didService) + + holderSigner, holderDID := getSigner(ttt) + definition := createPresentationDefinition(ttt, pRouter) + request := createSubmissionRequest(ttt, definition.PresentationDefinition.ID, authorDID.DID.ID, VerifiableCredential( + WithCredentialSubject(credential.CredentialSubject{ + "additionalName": "McLovin", + "dateOfBirth": "1987-01-02", + "familyName": "Andres", + "givenName": "Uribe", + "id": "did:web:andresuribe.com", + })), holderSigner, holderDID) + + value := newRequestValue(ttt, request) + req := httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/presentations/submissions", value) + w := httptest.NewRecorder() + + c := newRequestContext(w, req) + pRouter.CreateSubmission(c) + assert.True(tt, util.Is2xxResponse(w.Code)) + + var resp router.Operation + assert.NoError(ttt, json.NewDecoder(w.Body).Decode(&resp)) + assert.Contains(ttt, resp.ID, "presentations/submissions/") + assert.False(ttt, resp.Done) + assert.Zero(ttt, resp.Result) + }) - tt.Run("Review submission twice fails", func(ttt *testing.T) { - s := setupTestDB(ttt) - pRouter, didService := setupPresentationRouter(ttt, s) - authorDID := createDID(ttt, didService) - - holderSigner, holderDID := getSigner(ttt) - definition := createPresentationDefinition(ttt, pRouter) - submissionOp := createSubmission(t, pRouter, definition.PresentationDefinition.ID, authorDID.DID.ID, VerifiableCredential(), holderDID, holderSigner) - createdID := opstorage.StatusObjectID(submissionOp.ID) - _ = reviewSubmission(ttt, pRouter, createdID) - - request := router.ReviewSubmissionRequest{ - Approved: true, - Reason: "because I want to review again", - } - - value := newRequestValue(ttt, request) - req := httptest.NewRequest( - http.MethodPut, - fmt.Sprintf("https://ssi-service.com/v1/presentations/submissions/%s/review", createdID), - value) - w := httptest.NewRecorder() - c := newRequestContextWithParams(w, req, map[string]string{"id": createdID}) - pRouter.ReviewSubmission(c) - assert.Contains(ttt, w.Body.String(), "operation already marked as done") - }) + tt.Run("Review submission returns approved submission", func(ttt *testing.T) { + s := test.ServiceStorage(t) + pRouter, didService := setupPresentationRouter(ttt, s) + authorDID := createDID(ttt, didService) + + holderSigner, holderDID := getSigner(ttt) + definition := createPresentationDefinition(ttt, pRouter) + submissionOp := createSubmission(t, pRouter, definition.PresentationDefinition.ID, authorDID.DID.ID, VerifiableCredential(), holderDID, holderSigner) + + request := router.ReviewSubmissionRequest{ + Approved: true, + Reason: "because I want to", + } + + value := newRequestValue(ttt, request) + createdID := opstorage.StatusObjectID(submissionOp.ID) + req := httptest.NewRequest( + http.MethodPut, + fmt.Sprintf("https://ssi-service.com/v1/presentations/submissions/%s/review", createdID), + value) + w := httptest.NewRecorder() + + c := newRequestContextWithParams(w, req, map[string]string{"id": createdID}) + pRouter.ReviewSubmission(c) + assert.True(tt, util.Is2xxResponse(w.Code)) + + var resp router.ReviewSubmissionResponse + assert.NoError(ttt, json.NewDecoder(w.Body).Decode(&resp)) + assert.Equal(ttt, "because I want to", resp.Reason) + assert.NotEmpty(ttt, resp.GetSubmission().ID) + assert.Equal(ttt, "approved", resp.Status) + assert.Equal(ttt, definition.PresentationDefinition.ID, resp.GetSubmission().DefinitionID) + }) - tt.Run("List submissions returns empty when there are none", func(ttt *testing.T) { - s := setupTestDB(ttt) - pRouter, _ := setupPresentationRouter(ttt, s) + tt.Run("Review submission twice fails", func(ttt *testing.T) { + s := test.ServiceStorage(t) + pRouter, didService := setupPresentationRouter(ttt, s) + authorDID := createDID(ttt, didService) + + holderSigner, holderDID := getSigner(ttt) + definition := createPresentationDefinition(ttt, pRouter) + submissionOp := createSubmission(t, pRouter, definition.PresentationDefinition.ID, authorDID.DID.ID, VerifiableCredential(), holderDID, holderSigner) + createdID := opstorage.StatusObjectID(submissionOp.ID) + _ = reviewSubmission(ttt, pRouter, createdID) + + request := router.ReviewSubmissionRequest{ + Approved: true, + Reason: "because I want to review again", + } + + value := newRequestValue(ttt, request) + req := httptest.NewRequest( + http.MethodPut, + fmt.Sprintf("https://ssi-service.com/v1/presentations/submissions/%s/review", createdID), + value) + w := httptest.NewRecorder() + c := newRequestContextWithParams(w, req, map[string]string{"id": createdID}) + pRouter.ReviewSubmission(c) + assert.Contains(ttt, w.Body.String(), "operation already marked as done") + }) - req := httptest.NewRequest(http.MethodGet, "https://ssi-service.com/v1/presentations/submissions", nil) - w := httptest.NewRecorder() + tt.Run("List submissions returns empty when there are none", func(ttt *testing.T) { + s := test.ServiceStorage(t) + pRouter, _ := setupPresentationRouter(ttt, s) - c := newRequestContext(w, req) - pRouter.ListSubmissions(c) - assert.True(tt, util.Is2xxResponse(w.Code)) + req := httptest.NewRequest(http.MethodGet, "https://ssi-service.com/v1/presentations/submissions", nil) + w := httptest.NewRecorder() - var resp router.ListSubmissionResponse - assert.NoError(ttt, json.NewDecoder(w.Body).Decode(&resp)) - assert.Empty(ttt, resp.Submissions) - }) + c := newRequestContext(w, req) + pRouter.ListSubmissions(c) + assert.True(tt, util.Is2xxResponse(w.Code)) - tt.Run("List submissions returns many submissions", func(ttt *testing.T) { - s := setupTestDB(ttt) - pRouter, didService := setupPresentationRouter(ttt, s) - authorDID := createDID(ttt, didService) - - holderSigner, holderDID := getSigner(ttt) - definition := createPresentationDefinition(ttt, pRouter) - op := createSubmission(t, pRouter, definition.PresentationDefinition.ID, authorDID.DID.ID, VerifiableCredential( - WithCredentialSubject(credential.CredentialSubject{ - "additionalName": "McLovin", - "dateOfBirth": "1987-01-02", - "familyName": "Andres", - "givenName": "Uribe", - "id": "did:web:andresuribe.com", - })), holderDID, holderSigner) - - mrTeeSigner, mrTeeDID := getSigner(ttt) - op2 := createSubmission(ttt, pRouter, definition.PresentationDefinition.ID, authorDID.DID.ID, VerifiableCredential( - WithCredentialSubject(credential.CredentialSubject{ - "additionalName": "Mr. T", - "dateOfBirth": "1999-01-02", - "familyName": "Mister", - "givenName": "Tee", - "id": "did:web:mrt.com"})), mrTeeDID, mrTeeSigner) - - req := httptest.NewRequest(http.MethodGet, "https://ssi-service.com/v1/presentations/submissions", nil) - w := httptest.NewRecorder() - - c := newRequestContext(w, req) - pRouter.ListSubmissions(c) - assert.True(tt, util.Is2xxResponse(w.Code)) - - var resp router.ListSubmissionResponse - assert.NoError(ttt, json.NewDecoder(w.Body).Decode(&resp)) - assert.Len(ttt, resp.Submissions, 2) - - expectedSubmissions := []model.Submission{ - { - Status: "pending", - VerifiablePresentation: &credential.VerifiablePresentation{ - Context: []any{"https://www.w3.org/2018/credentials/v1"}, - Holder: holderDID.String(), - Type: []any{"VerifiablePresentation"}, - }, - }, - { - Status: "pending", - VerifiablePresentation: &credential.VerifiablePresentation{ - Context: []any{"https://www.w3.org/2018/credentials/v1"}, - Holder: mrTeeDID.String(), - Type: []any{"VerifiablePresentation"}, - }, - }, - } - diff := cmp.Diff(expectedSubmissions, resp.Submissions, - cmpopts.IgnoreFields(credential.VerifiablePresentation{}, "ID", "VerifiableCredential", "PresentationSubmission"), - cmpopts.SortSlices(func(l, r model.Submission) bool { - return l.VerifiablePresentation.Holder < r.VerifiablePresentation.Holder - }), - ) - if diff != "" { - ttt.Errorf("Mismatch on submissions (-want +got):\n%s", diff) - } - assert.Len(ttt, resp.Submissions[0].VerifiablePresentation.VerifiableCredential, 1) - assert.Len(ttt, resp.Submissions[1].VerifiablePresentation.VerifiableCredential, 1) - - assert.ElementsMatch(ttt, - []string{ - opstorage.StatusObjectID(op.ID), - opstorage.StatusObjectID(op2.ID)}, - []string{ - resp.Submissions[0].GetSubmission().ID, - resp.Submissions[1].GetSubmission().ID, + var resp router.ListSubmissionResponse + assert.NoError(ttt, json.NewDecoder(w.Body).Decode(&resp)) + assert.Empty(ttt, resp.Submissions) }) - assert.Equal(ttt, - []string{ - definition.PresentationDefinition.ID, - definition.PresentationDefinition.ID, - }, - []string{ - resp.Submissions[0].GetSubmission().DefinitionID, - resp.Submissions[1].GetSubmission().DefinitionID, + + tt.Run("List submissions returns many submissions", func(ttt *testing.T) { + s := test.ServiceStorage(t) + pRouter, didService := setupPresentationRouter(ttt, s) + authorDID := createDID(ttt, didService) + + holderSigner, holderDID := getSigner(ttt) + definition := createPresentationDefinition(ttt, pRouter) + op := createSubmission(t, pRouter, definition.PresentationDefinition.ID, authorDID.DID.ID, VerifiableCredential( + WithCredentialSubject(credential.CredentialSubject{ + "additionalName": "McLovin", + "dateOfBirth": "1987-01-02", + "familyName": "Andres", + "givenName": "Uribe", + "id": "did:web:andresuribe.com", + })), holderDID, holderSigner) + + mrTeeSigner, mrTeeDID := getSigner(ttt) + op2 := createSubmission(ttt, pRouter, definition.PresentationDefinition.ID, authorDID.DID.ID, VerifiableCredential( + WithCredentialSubject(credential.CredentialSubject{ + "additionalName": "Mr. T", + "dateOfBirth": "1999-01-02", + "familyName": "Mister", + "givenName": "Tee", + "id": "did:web:mrt.com"})), mrTeeDID, mrTeeSigner) + + req := httptest.NewRequest(http.MethodGet, "https://ssi-service.com/v1/presentations/submissions", nil) + w := httptest.NewRecorder() + + c := newRequestContext(w, req) + pRouter.ListSubmissions(c) + assert.True(tt, util.Is2xxResponse(w.Code)) + + var resp router.ListSubmissionResponse + assert.NoError(ttt, json.NewDecoder(w.Body).Decode(&resp)) + assert.Len(ttt, resp.Submissions, 2) + + expectedSubmissions := []model.Submission{ + { + Status: "pending", + VerifiablePresentation: &credential.VerifiablePresentation{ + Context: []any{"https://www.w3.org/2018/credentials/v1"}, + Holder: holderDID.String(), + Type: []any{"VerifiablePresentation"}, + }, + }, + { + Status: "pending", + VerifiablePresentation: &credential.VerifiablePresentation{ + Context: []any{"https://www.w3.org/2018/credentials/v1"}, + Holder: mrTeeDID.String(), + Type: []any{"VerifiablePresentation"}, + }, + }, + } + diff := cmp.Diff(expectedSubmissions, resp.Submissions, + cmpopts.IgnoreFields(credential.VerifiablePresentation{}, "ID", "VerifiableCredential", "PresentationSubmission"), + cmpopts.SortSlices(func(l, r model.Submission) bool { + return l.VerifiablePresentation.Holder < r.VerifiablePresentation.Holder + }), + ) + if diff != "" { + ttt.Errorf("Mismatch on submissions (-want +got):\n%s", diff) + } + assert.Len(ttt, resp.Submissions[0].VerifiablePresentation.VerifiableCredential, 1) + assert.Len(ttt, resp.Submissions[1].VerifiablePresentation.VerifiableCredential, 1) + + assert.ElementsMatch(ttt, + []string{ + opstorage.StatusObjectID(op.ID), + opstorage.StatusObjectID(op2.ID)}, + []string{ + resp.Submissions[0].GetSubmission().ID, + resp.Submissions[1].GetSubmission().ID, + }) + assert.Equal(ttt, + []string{ + definition.PresentationDefinition.ID, + definition.PresentationDefinition.ID, + }, + []string{ + resp.Submissions[0].GetSubmission().DefinitionID, + resp.Submissions[1].GetSubmission().DefinitionID, + }) }) - }) - tt.Run("bad filter returns error", func(ttt *testing.T) { - s := setupTestDB(ttt) - pRouter, _ := setupPresentationRouter(ttt, s) + tt.Run("bad filter returns error", func(ttt *testing.T) { + s := test.ServiceStorage(t) + pRouter, _ := setupPresentationRouter(ttt, s) - query := url.QueryEscape("im a baaad filter that's trying to break a lot of stuff") - req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("https://ssi-service.com/v1/presentations/submissions?filter=%s", query), nil) - w := httptest.NewRecorder() + query := url.QueryEscape("im a baaad filter that's trying to break a lot of stuff") + req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("https://ssi-service.com/v1/presentations/submissions?filter=%s", query), nil) + w := httptest.NewRecorder() - c := newRequestContextWithParams(w, req, map[string]string{"filter": query}) - pRouter.ListSubmissions(c) - assert.Contains(ttt, w.Body.String(), "invalid filter") - }) + c := newRequestContextWithParams(w, req, map[string]string{"filter": query}) + pRouter.ListSubmissions(c) + assert.Contains(ttt, w.Body.String(), "invalid filter") + }) - tt.Run("List submissions filters based on status", func(ttt *testing.T) { - s := setupTestDB(ttt) - pRouter, didService := setupPresentationRouter(ttt, s) - authorDID := createDID(ttt, didService) - - holderSigner, holderDID := getSigner(ttt) - definition := createPresentationDefinition(ttt, pRouter) - op := createSubmission(ttt, pRouter, definition.PresentationDefinition.ID, authorDID.DID.ID, VerifiableCredential( - WithCredentialSubject(credential.CredentialSubject{ - "additionalName": "McLovin", - "dateOfBirth": "1987-01-02", - "familyName": "Andres", - "givenName": "Uribe", - "id": "did:web:andresuribe.com", - })), holderDID, holderSigner) - - query := url.QueryEscape("status=\"pending\"") - req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("https://ssi-service.com/v1/presentations/submissions?filter=%s", query), nil) - w := httptest.NewRecorder() - - c := newRequestContextWithParams(w, req, map[string]string{"filter": query}) - pRouter.ListSubmissions(c) - assert.True(tt, util.Is2xxResponse(w.Code)) - - var resp router.ListSubmissionResponse - assert.NoError(ttt, json.NewDecoder(w.Body).Decode(&resp)) - - expectedSubmissions := []model.Submission{ - { - Status: "pending", - VerifiablePresentation: &credential.VerifiablePresentation{ - Context: []any{"https://www.w3.org/2018/credentials/v1"}, - Holder: holderDID.String(), - Type: []any{"VerifiablePresentation"}, - }, - }, - } - diff := cmp.Diff(expectedSubmissions, resp.Submissions, - cmpopts.IgnoreFields(credential.VerifiablePresentation{}, "ID", "PresentationSubmission", "VerifiableCredential"), - cmpopts.SortSlices(func(l, r model.Submission) bool { - return l.VerifiablePresentation.Holder < r.VerifiablePresentation.Holder - }), - ) - if diff != "" { - ttt.Errorf("Mismatch on submissions (-want +got):\n%s", diff) - } - - assert.Len(ttt, resp.Submissions, 1) - assert.Len(ttt, resp.Submissions[0].VerifiablePresentation.VerifiableCredential, 1) - assert.Equal(ttt, opstorage.StatusObjectID(op.ID), resp.Submissions[0].GetSubmission().ID) - assert.Equal(ttt, definition.PresentationDefinition.ID, resp.Submissions[0].GetSubmission().DefinitionID) - }) + tt.Run("List submissions filters based on status", func(ttt *testing.T) { + s := test.ServiceStorage(t) + pRouter, didService := setupPresentationRouter(ttt, s) + authorDID := createDID(ttt, didService) + + holderSigner, holderDID := getSigner(ttt) + definition := createPresentationDefinition(ttt, pRouter) + op := createSubmission(ttt, pRouter, definition.PresentationDefinition.ID, authorDID.DID.ID, VerifiableCredential( + WithCredentialSubject(credential.CredentialSubject{ + "additionalName": "McLovin", + "dateOfBirth": "1987-01-02", + "familyName": "Andres", + "givenName": "Uribe", + "id": "did:web:andresuribe.com", + })), holderDID, holderSigner) + + query := url.QueryEscape("status=\"pending\"") + req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("https://ssi-service.com/v1/presentations/submissions?filter=%s", query), nil) + w := httptest.NewRecorder() + + c := newRequestContextWithParams(w, req, map[string]string{"filter": query}) + pRouter.ListSubmissions(c) + assert.True(tt, util.Is2xxResponse(w.Code)) + + var resp router.ListSubmissionResponse + assert.NoError(ttt, json.NewDecoder(w.Body).Decode(&resp)) + + expectedSubmissions := []model.Submission{ + { + Status: "pending", + VerifiablePresentation: &credential.VerifiablePresentation{ + Context: []any{"https://www.w3.org/2018/credentials/v1"}, + Holder: holderDID.String(), + Type: []any{"VerifiablePresentation"}, + }, + }, + } + diff := cmp.Diff(expectedSubmissions, resp.Submissions, + cmpopts.IgnoreFields(credential.VerifiablePresentation{}, "ID", "PresentationSubmission", "VerifiableCredential"), + cmpopts.SortSlices(func(l, r model.Submission) bool { + return l.VerifiablePresentation.Holder < r.VerifiablePresentation.Holder + }), + ) + if diff != "" { + ttt.Errorf("Mismatch on submissions (-want +got):\n%s", diff) + } + + assert.Len(ttt, resp.Submissions, 1) + assert.Len(ttt, resp.Submissions[0].VerifiablePresentation.VerifiableCredential, 1) + assert.Equal(ttt, opstorage.StatusObjectID(op.ID), resp.Submissions[0].GetSubmission().ID) + assert.Equal(ttt, definition.PresentationDefinition.ID, resp.Submissions[0].GetSubmission().DefinitionID) + }) - tt.Run("List submissions filter returns empty when status does not match", func(ttt *testing.T) { - s := setupTestDB(ttt) - pRouter, didService := setupPresentationRouter(ttt, s) - authorDID := createDID(ttt, didService) - - holderSigner, holderDID := getSigner(ttt) - definition := createPresentationDefinition(ttt, pRouter) - _ = createSubmission(t, pRouter, definition.PresentationDefinition.ID, authorDID.DID.ID, VerifiableCredential( - WithCredentialSubject(credential.CredentialSubject{ - "additionalName": "McLovin", - "dateOfBirth": "1987-01-02", - "familyName": "Andres", - "givenName": "Uribe", - "id": "did:web:andresuribe.com", - })), holderDID, holderSigner) - - query := url.QueryEscape(`status="done"`) - req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("https://ssi-service.com/v1/presentations/submissions?filter=%s", query), nil) - w := httptest.NewRecorder() - c := newRequestContextWithParams(w, req, map[string]string{"filter": query}) - pRouter.ListSubmissions(c) - assert.True(tt, util.Is2xxResponse(w.Code)) - - var resp router.ListSubmissionResponse - assert.NoError(ttt, json.NewDecoder(w.Body).Decode(&resp)) - assert.Empty(ttt, resp.Submissions) + tt.Run("List submissions filter returns empty when status does not match", func(ttt *testing.T) { + s := test.ServiceStorage(t) + pRouter, didService := setupPresentationRouter(ttt, s) + authorDID := createDID(ttt, didService) + + holderSigner, holderDID := getSigner(ttt) + definition := createPresentationDefinition(ttt, pRouter) + _ = createSubmission(t, pRouter, definition.PresentationDefinition.ID, authorDID.DID.ID, VerifiableCredential( + WithCredentialSubject(credential.CredentialSubject{ + "additionalName": "McLovin", + "dateOfBirth": "1987-01-02", + "familyName": "Andres", + "givenName": "Uribe", + "id": "did:web:andresuribe.com", + })), holderDID, holderSigner) + + query := url.QueryEscape(`status="done"`) + req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("https://ssi-service.com/v1/presentations/submissions?filter=%s", query), nil) + w := httptest.NewRecorder() + c := newRequestContextWithParams(w, req, map[string]string{"filter": query}) + pRouter.ListSubmissions(c) + assert.True(tt, util.Is2xxResponse(w.Code)) + + var resp router.ListSubmissionResponse + assert.NoError(ttt, json.NewDecoder(w.Body).Decode(&resp)) + assert.Empty(ttt, resp.Submissions) + }) + }) }) - }) - + } } func createPresentationRequest(t *testing.T, pRouter *router.PresentationRouter, definitionID string, issuerDID didsdk.Document) router.CreateRequestResponse { diff --git a/pkg/server/server_schema_test.go b/pkg/server/server_schema_test.go index 739ab2ead..572fcb6a5 100644 --- a/pkg/server/server_schema_test.go +++ b/pkg/server/server_schema_test.go @@ -17,277 +17,282 @@ import ( "github.com/tbd54566975/ssi-service/internal/util" "github.com/tbd54566975/ssi-service/pkg/server/router" "github.com/tbd54566975/ssi-service/pkg/service/did" + "github.com/tbd54566975/ssi-service/pkg/testutil" ) func TestSchemaAPI(t *testing.T) { - t.Run("Test Create JsonSchema2023 Schema", func(tt *testing.T) { - bolt := setupTestDB(tt) - require.NotEmpty(tt, bolt) - - keyStoreService := testKeyStoreService(tt, bolt) - didService := testDIDService(tt, bolt, keyStoreService) - schemaService := testSchemaRouter(tt, bolt, keyStoreService, didService) - - simpleSchema := getTestSchema() - badSchemaRequest := router.CreateSchemaRequest{Schema: simpleSchema} - schemaRequestValue := newRequestValue(tt, badSchemaRequest) - req := httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/schemas", schemaRequestValue) - w := httptest.NewRecorder() - - c := newRequestContext(w, req) - schemaService.CreateSchema(c) - assert.Contains(tt, w.Body.String(), "invalid create schema request") - - // reset the http recorder - w = httptest.NewRecorder() - - schemaRequest := router.CreateSchemaRequest{Name: "test schema", Schema: simpleSchema} - schemaRequestValue = newRequestValue(tt, schemaRequest) - req = httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/schemas", schemaRequestValue) - - c = newRequestContext(w, req) - schemaService.CreateSchema(c) - assert.True(tt, util.Is2xxResponse(w.Code)) - - var resp router.CreateSchemaResponse - err := json.NewDecoder(w.Body).Decode(&resp) - assert.NoError(tt, err) - assert.NotEmpty(tt, resp.ID) - - // since the id is generated, we need to manually override it - schemaRequest.Schema[schema.JSONSchemaIDProperty] = resp.Schema.ID() - assert.JSONEq(tt, schemaRequest.Schema.String(), resp.Schema.String()) - assert.Equal(tt, schema.JSONSchema2023Type, resp.Type) - assert.Empty(tt, resp.CredentialSchema) - assert.NotEmpty(tt, resp.Schema) - }) - - t.Run("Test Create CredentialSchema2023 Schema", func(tt *testing.T) { - bolt := setupTestDB(tt) - require.NotEmpty(tt, bolt) - - keyStoreService := testKeyStoreService(tt, bolt) - didService := testDIDService(tt, bolt, keyStoreService) - schemaService := testSchemaRouter(tt, bolt, keyStoreService, didService) - - simpleSchema := getTestSchema() - badSchemaRequest := router.CreateSchemaRequest{ - Name: "test schema", - Schema: simpleSchema, - CredentialSchemaRequest: &router.CredentialSchemaRequest{ - Issuer: "issuer", - }, - } - schemaRequestValue := newRequestValue(tt, badSchemaRequest) - req := httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/schemas", schemaRequestValue) - w := httptest.NewRecorder() - - c := newRequestContext(w, req) - schemaService.CreateSchema(c) - assert.Contains(tt, w.Body.String(), "issuerKid is a required field") - - // reset the http recorder - w = httptest.NewRecorder() - - // create an issuer for the credential schema - issuerResp, err := didService.CreateDIDByMethod(context.Background(), did.CreateDIDRequest{ - Method: "key", - KeyType: crypto.Ed25519, + for _, test := range testutil.TestDatabases { + t.Run(test.Name, func(t *testing.T) { + t.Run("Test Create JsonSchema2023 Schema", func(tt *testing.T) { + bolt := test.ServiceStorage(t) + require.NotEmpty(tt, bolt) + + keyStoreService := testKeyStoreService(tt, bolt) + didService := testDIDService(tt, bolt, keyStoreService) + schemaService := testSchemaRouter(tt, bolt, keyStoreService, didService) + + simpleSchema := getTestSchema() + badSchemaRequest := router.CreateSchemaRequest{Schema: simpleSchema} + schemaRequestValue := newRequestValue(tt, badSchemaRequest) + req := httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/schemas", schemaRequestValue) + w := httptest.NewRecorder() + + c := newRequestContext(w, req) + schemaService.CreateSchema(c) + assert.Contains(tt, w.Body.String(), "invalid create schema request") + + // reset the http recorder + w = httptest.NewRecorder() + + schemaRequest := router.CreateSchemaRequest{Name: "test schema", Schema: simpleSchema} + schemaRequestValue = newRequestValue(tt, schemaRequest) + req = httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/schemas", schemaRequestValue) + + c = newRequestContext(w, req) + schemaService.CreateSchema(c) + assert.True(tt, util.Is2xxResponse(w.Code)) + + var resp router.CreateSchemaResponse + err := json.NewDecoder(w.Body).Decode(&resp) + assert.NoError(tt, err) + assert.NotEmpty(tt, resp.ID) + + // since the id is generated, we need to manually override it + schemaRequest.Schema[schema.JSONSchemaIDProperty] = resp.Schema.ID() + assert.JSONEq(tt, schemaRequest.Schema.String(), resp.Schema.String()) + assert.Equal(tt, schema.JSONSchema2023Type, resp.Type) + assert.Empty(tt, resp.CredentialSchema) + assert.NotEmpty(tt, resp.Schema) + }) + + t.Run("Test Create CredentialSchema2023 Schema", func(tt *testing.T) { + bolt := test.ServiceStorage(t) + require.NotEmpty(tt, bolt) + + keyStoreService := testKeyStoreService(tt, bolt) + didService := testDIDService(tt, bolt, keyStoreService) + schemaService := testSchemaRouter(tt, bolt, keyStoreService, didService) + + simpleSchema := getTestSchema() + badSchemaRequest := router.CreateSchemaRequest{ + Name: "test schema", + Schema: simpleSchema, + CredentialSchemaRequest: &router.CredentialSchemaRequest{ + Issuer: "issuer", + }, + } + schemaRequestValue := newRequestValue(tt, badSchemaRequest) + req := httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/schemas", schemaRequestValue) + w := httptest.NewRecorder() + + c := newRequestContext(w, req) + schemaService.CreateSchema(c) + assert.Contains(tt, w.Body.String(), "issuerKid is a required field") + + // reset the http recorder + w = httptest.NewRecorder() + + // create an issuer for the credential schema + issuerResp, err := didService.CreateDIDByMethod(context.Background(), did.CreateDIDRequest{ + Method: "key", + KeyType: crypto.Ed25519, + }) + require.NoError(t, err) + issuerID := issuerResp.DID.ID + issuerKID := issuerResp.DID.VerificationMethod[0].ID + + // create the credential schema + schemaRequest := router.CreateSchemaRequest{ + Name: "test schema", + Schema: simpleSchema, + CredentialSchemaRequest: &router.CredentialSchemaRequest{ + Issuer: issuerID, + IssuerKID: issuerKID, + }, + } + schemaRequestValue = newRequestValue(tt, schemaRequest) + req = httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/schemas", schemaRequestValue) + + c = newRequestContext(w, req) + schemaService.CreateSchema(c) + assert.True(tt, util.Is2xxResponse(w.Code)) + + var resp router.CreateSchemaResponse + err = json.NewDecoder(w.Body).Decode(&resp) + assert.NoError(tt, err) + assert.NotEmpty(tt, resp.ID) + + assert.Empty(tt, resp.Schema) + assert.NotEmpty(tt, resp.CredentialSchema) + assert.Equal(tt, schema.CredentialSchema2023Type, resp.Type) + + // decode the schema from the response and verify it + _, _, cred, err := credential.ToCredential(resp.CredentialSchema.String()) + assert.NoError(tt, err) + credSubjectBytes, err := json.Marshal(cred.CredentialSubject) + assert.NoError(tt, err) + var s schema.JSONSchema + err = json.Unmarshal(credSubjectBytes, &s) + assert.NoError(tt, err) + assert.Equal(tt, schemaRequest.Issuer, cred.Issuer) + + // since the id is generated, we need to manually override it + schemaRequest.Schema[schema.JSONSchemaIDProperty] = s.ID() + delete(s, schema.JSONSchemaAdditionalIDProperty) + assert.JSONEq(tt, schemaRequest.Schema.String(), s.String()) + }) + + t.Run("Test Get Schema and Get Schemas", func(tt *testing.T) { + bolt := test.ServiceStorage(t) + require.NotEmpty(tt, bolt) + + keyStoreService := testKeyStoreService(tt, bolt) + didService := testDIDService(tt, bolt, keyStoreService) + schemaService := testSchemaRouter(tt, bolt, keyStoreService, didService) + + // get schema that doesn't exist + w := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodGet, "https://ssi-service.com/v1/schemas/bad", nil) + c := newRequestContext(w, req) + schemaService.GetSchema(c) + assert.Contains(tt, w.Body.String(), "cannot get schema without ID parameter") + + // reset recorder between calls + w = httptest.NewRecorder() + + // get schema with invalid id + req = httptest.NewRequest(http.MethodGet, "https://ssi-service.com/v1/schemas/bad", nil) + c = newRequestContextWithParams(w, req, map[string]string{"id": "bad"}) + schemaService.GetSchema(c) + assert.Contains(tt, w.Body.String(), "could not get schema with id: bad") + + // reset recorder between calls + w = httptest.NewRecorder() + + // get all schemas - get none + req = httptest.NewRequest(http.MethodGet, "https://ssi-service.com/v1/schemas", nil) + c = newRequestContext(w, req) + schemaService.ListSchemas(c) + assert.True(tt, util.Is2xxResponse(w.Code)) + + var getSchemasResp router.ListSchemasResponse + err := json.NewDecoder(w.Body).Decode(&getSchemasResp) + assert.NoError(tt, err) + assert.Len(tt, getSchemasResp.Schemas, 0) + + // reset recorder between calls + w = httptest.NewRecorder() + + // create a schema + simpleSchema := getTestSchema() + + schemaRequest := router.CreateSchemaRequest{Name: "test schema", Schema: simpleSchema} + schemaRequestValue := newRequestValue(tt, schemaRequest) + createReq := httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/schemas", schemaRequestValue) + + c = newRequestContext(w, createReq) + schemaService.CreateSchema(c) + assert.True(tt, util.Is2xxResponse(w.Code)) + + var createResp router.CreateSchemaResponse + err = json.NewDecoder(w.Body).Decode(&createResp) + assert.NoError(tt, err) + + assert.NotEmpty(tt, createResp.ID) + + // since the id is generated, we need to manually override it + schemaRequest.Schema[schema.JSONSchemaIDProperty] = createResp.Schema.ID() + assert.JSONEq(tt, schemaRequest.Schema.String(), createResp.Schema.String()) + + // reset recorder between calls + w = httptest.NewRecorder() + + // get it back + req = httptest.NewRequest(http.MethodGet, fmt.Sprintf("https://ssi-service.com/v1/schemas/%s", createResp.ID), nil) + c = newRequestContextWithParams(w, req, map[string]string{"id": createResp.ID}) + schemaService.GetSchema(c) + assert.True(tt, util.Is2xxResponse(w.Code)) + + var gotSchemaResp router.GetSchemaResponse + err = json.NewDecoder(w.Body).Decode(&gotSchemaResp) + assert.NoError(tt, err) + + assert.Contains(tt, gotSchemaResp.Schema.ID(), createResp.ID) + assert.Equal(tt, createResp.Schema.Schema(), gotSchemaResp.Schema.Schema()) + + // reset recorder between calls + w = httptest.NewRecorder() + + // get all schemas - get none + req = httptest.NewRequest(http.MethodGet, "https://ssi-service.com/v1/schemas", nil) + c = newRequestContext(w, req) + schemaService.ListSchemas(c) + assert.True(tt, util.Is2xxResponse(w.Code)) + + err = json.NewDecoder(w.Body).Decode(&getSchemasResp) + assert.NoError(tt, err) + assert.Len(tt, getSchemasResp.Schemas, 1) + }) + + t.Run("Test Delete Schema", func(tt *testing.T) { + bolt := test.ServiceStorage(t) + require.NotEmpty(tt, bolt) + + keyStoreService := testKeyStoreService(tt, bolt) + didService := testDIDService(tt, bolt, keyStoreService) + schemaService := testSchemaRouter(tt, bolt, keyStoreService, didService) + + w := httptest.NewRecorder() + + // delete a schema that doesn't exist + req := httptest.NewRequest(http.MethodDelete, "https://ssi-service.com/v1/schemas/bad", nil) + c := newRequestContextWithParams(w, req, map[string]string{"id": "bad"}) + schemaService.DeleteSchema(c) + assert.Contains(tt, w.Body.String(), "could not delete schema with id: bad") + + // create a schema + simpleSchema := getTestSchema() + + schemaRequest := router.CreateSchemaRequest{Name: "test schema", Schema: simpleSchema} + schemaRequestValue := newRequestValue(tt, schemaRequest) + req = httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/schemas", schemaRequestValue) + w = httptest.NewRecorder() + c = newRequestContext(w, req) + schemaService.CreateSchema(c) + assert.True(tt, util.Is2xxResponse(w.Code)) + + var resp router.CreateSchemaResponse + err := json.NewDecoder(w.Body).Decode(&resp) + assert.NoError(tt, err) + assert.NotEmpty(tt, resp.ID) + + // since the id is generated, we need to manually override it + schemaRequest.Schema[schema.JSONSchemaIDProperty] = resp.Schema.ID() + assert.JSONEq(tt, schemaRequest.Schema.String(), resp.Schema.String()) + + // get schema by id + req = httptest.NewRequest(http.MethodGet, fmt.Sprintf("https://ssi-service.com/v1/schemas/%s", resp.ID), nil) + w = httptest.NewRecorder() + c = newRequestContextWithParams(w, req, map[string]string{"id": resp.ID}) + schemaService.GetSchema(c) + assert.True(tt, util.Is2xxResponse(w.Code)) + + // delete it + req = httptest.NewRequest(http.MethodDelete, fmt.Sprintf("https://ssi-service.com/v1/schemas/%s", resp.ID), nil) + w = httptest.NewRecorder() + c = newRequestContextWithParams(w, req, map[string]string{"id": resp.ID}) + schemaService.DeleteSchema(c) + assert.True(tt, util.Is2xxResponse(w.Code)) + + // get it back + req = httptest.NewRequest(http.MethodGet, fmt.Sprintf("https://ssi-service.com/v1/schemas/%s", resp.ID), nil) + w = httptest.NewRecorder() + c = newRequestContextWithParams(w, req, map[string]string{"id": resp.ID}) + schemaService.GetSchema(c) + assert.Contains(tt, w.Body.String(), "schema not found") + }) }) - require.NoError(t, err) - issuerID := issuerResp.DID.ID - issuerKID := issuerResp.DID.VerificationMethod[0].ID - - // create the credential schema - schemaRequest := router.CreateSchemaRequest{ - Name: "test schema", - Schema: simpleSchema, - CredentialSchemaRequest: &router.CredentialSchemaRequest{ - Issuer: issuerID, - IssuerKID: issuerKID, - }, - } - schemaRequestValue = newRequestValue(tt, schemaRequest) - req = httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/schemas", schemaRequestValue) - - c = newRequestContext(w, req) - schemaService.CreateSchema(c) - assert.True(tt, util.Is2xxResponse(w.Code)) - - var resp router.CreateSchemaResponse - err = json.NewDecoder(w.Body).Decode(&resp) - assert.NoError(tt, err) - assert.NotEmpty(tt, resp.ID) - - assert.Empty(tt, resp.Schema) - assert.NotEmpty(tt, resp.CredentialSchema) - assert.Equal(tt, schema.CredentialSchema2023Type, resp.Type) - - // decode the schema from the response and verify it - _, _, cred, err := credential.ToCredential(resp.CredentialSchema.String()) - assert.NoError(tt, err) - credSubjectBytes, err := json.Marshal(cred.CredentialSubject) - assert.NoError(tt, err) - var s schema.JSONSchema - err = json.Unmarshal(credSubjectBytes, &s) - assert.NoError(tt, err) - assert.Equal(tt, schemaRequest.Issuer, cred.Issuer) - - // since the id is generated, we need to manually override it - schemaRequest.Schema[schema.JSONSchemaIDProperty] = s.ID() - delete(s, schema.JSONSchemaAdditionalIDProperty) - assert.JSONEq(tt, schemaRequest.Schema.String(), s.String()) - }) - - t.Run("Test Get Schema and Get Schemas", func(tt *testing.T) { - bolt := setupTestDB(tt) - require.NotEmpty(tt, bolt) - - keyStoreService := testKeyStoreService(tt, bolt) - didService := testDIDService(tt, bolt, keyStoreService) - schemaService := testSchemaRouter(tt, bolt, keyStoreService, didService) - - // get schema that doesn't exist - w := httptest.NewRecorder() - req := httptest.NewRequest(http.MethodGet, "https://ssi-service.com/v1/schemas/bad", nil) - c := newRequestContext(w, req) - schemaService.GetSchema(c) - assert.Contains(tt, w.Body.String(), "cannot get schema without ID parameter") - - // reset recorder between calls - w = httptest.NewRecorder() - - // get schema with invalid id - req = httptest.NewRequest(http.MethodGet, "https://ssi-service.com/v1/schemas/bad", nil) - c = newRequestContextWithParams(w, req, map[string]string{"id": "bad"}) - schemaService.GetSchema(c) - assert.Contains(tt, w.Body.String(), "could not get schema with id: bad") - - // reset recorder between calls - w = httptest.NewRecorder() - - // get all schemas - get none - req = httptest.NewRequest(http.MethodGet, "https://ssi-service.com/v1/schemas", nil) - c = newRequestContext(w, req) - schemaService.ListSchemas(c) - assert.True(tt, util.Is2xxResponse(w.Code)) - - var getSchemasResp router.ListSchemasResponse - err := json.NewDecoder(w.Body).Decode(&getSchemasResp) - assert.NoError(tt, err) - assert.Len(tt, getSchemasResp.Schemas, 0) - - // reset recorder between calls - w = httptest.NewRecorder() - - // create a schema - simpleSchema := getTestSchema() - - schemaRequest := router.CreateSchemaRequest{Name: "test schema", Schema: simpleSchema} - schemaRequestValue := newRequestValue(tt, schemaRequest) - createReq := httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/schemas", schemaRequestValue) - - c = newRequestContext(w, createReq) - schemaService.CreateSchema(c) - assert.True(tt, util.Is2xxResponse(w.Code)) - - var createResp router.CreateSchemaResponse - err = json.NewDecoder(w.Body).Decode(&createResp) - assert.NoError(tt, err) - - assert.NotEmpty(tt, createResp.ID) - - // since the id is generated, we need to manually override it - schemaRequest.Schema[schema.JSONSchemaIDProperty] = createResp.Schema.ID() - assert.JSONEq(tt, schemaRequest.Schema.String(), createResp.Schema.String()) - - // reset recorder between calls - w = httptest.NewRecorder() - - // get it back - req = httptest.NewRequest(http.MethodGet, fmt.Sprintf("https://ssi-service.com/v1/schemas/%s", createResp.ID), nil) - c = newRequestContextWithParams(w, req, map[string]string{"id": createResp.ID}) - schemaService.GetSchema(c) - assert.True(tt, util.Is2xxResponse(w.Code)) - - var gotSchemaResp router.GetSchemaResponse - err = json.NewDecoder(w.Body).Decode(&gotSchemaResp) - assert.NoError(tt, err) - - assert.Contains(tt, gotSchemaResp.Schema.ID(), createResp.ID) - assert.Equal(tt, createResp.Schema.Schema(), gotSchemaResp.Schema.Schema()) - - // reset recorder between calls - w = httptest.NewRecorder() - - // get all schemas - get none - req = httptest.NewRequest(http.MethodGet, "https://ssi-service.com/v1/schemas", nil) - c = newRequestContext(w, req) - schemaService.ListSchemas(c) - assert.True(tt, util.Is2xxResponse(w.Code)) - - err = json.NewDecoder(w.Body).Decode(&getSchemasResp) - assert.NoError(tt, err) - assert.Len(tt, getSchemasResp.Schemas, 1) - }) - - t.Run("Test Delete Schema", func(tt *testing.T) { - bolt := setupTestDB(tt) - require.NotEmpty(tt, bolt) - - keyStoreService := testKeyStoreService(tt, bolt) - didService := testDIDService(tt, bolt, keyStoreService) - schemaService := testSchemaRouter(tt, bolt, keyStoreService, didService) - - w := httptest.NewRecorder() - - // delete a schema that doesn't exist - req := httptest.NewRequest(http.MethodDelete, "https://ssi-service.com/v1/schemas/bad", nil) - c := newRequestContextWithParams(w, req, map[string]string{"id": "bad"}) - schemaService.DeleteSchema(c) - assert.Contains(tt, w.Body.String(), "could not delete schema with id: bad") - - // create a schema - simpleSchema := getTestSchema() - - schemaRequest := router.CreateSchemaRequest{Name: "test schema", Schema: simpleSchema} - schemaRequestValue := newRequestValue(tt, schemaRequest) - req = httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/schemas", schemaRequestValue) - w = httptest.NewRecorder() - c = newRequestContext(w, req) - schemaService.CreateSchema(c) - assert.True(tt, util.Is2xxResponse(w.Code)) - - var resp router.CreateSchemaResponse - err := json.NewDecoder(w.Body).Decode(&resp) - assert.NoError(tt, err) - assert.NotEmpty(tt, resp.ID) - - // since the id is generated, we need to manually override it - schemaRequest.Schema[schema.JSONSchemaIDProperty] = resp.Schema.ID() - assert.JSONEq(tt, schemaRequest.Schema.String(), resp.Schema.String()) - - // get schema by id - req = httptest.NewRequest(http.MethodGet, fmt.Sprintf("https://ssi-service.com/v1/schemas/%s", resp.ID), nil) - w = httptest.NewRecorder() - c = newRequestContextWithParams(w, req, map[string]string{"id": resp.ID}) - schemaService.GetSchema(c) - assert.True(tt, util.Is2xxResponse(w.Code)) - - // delete it - req = httptest.NewRequest(http.MethodDelete, fmt.Sprintf("https://ssi-service.com/v1/schemas/%s", resp.ID), nil) - w = httptest.NewRecorder() - c = newRequestContextWithParams(w, req, map[string]string{"id": resp.ID}) - schemaService.DeleteSchema(c) - assert.True(tt, util.Is2xxResponse(w.Code)) - - // get it back - req = httptest.NewRequest(http.MethodGet, fmt.Sprintf("https://ssi-service.com/v1/schemas/%s", resp.ID), nil) - w = httptest.NewRecorder() - c = newRequestContextWithParams(w, req, map[string]string{"id": resp.ID}) - schemaService.GetSchema(c) - assert.Contains(tt, w.Body.String(), "schema not found") - }) + } } func getTestSchema() schema.JSONSchema { diff --git a/pkg/server/server_webhook_test.go b/pkg/server/server_webhook_test.go index d56930a4c..8c12609b4 100644 --- a/pkg/server/server_webhook_test.go +++ b/pkg/server/server_webhook_test.go @@ -23,6 +23,7 @@ import ( "github.com/tbd54566975/ssi-service/pkg/server/router" "github.com/tbd54566975/ssi-service/pkg/service/webhook" "github.com/tbd54566975/ssi-service/pkg/storage" + "github.com/tbd54566975/ssi-service/pkg/testutil" ) func freePort() string { @@ -139,273 +140,277 @@ func put(t *testing.T, server *SSIServer, endpoint string, data []byte) { } func TestWebhookAPI(t *testing.T) { - t.Run("CreateWebhook returns error when missing request", func(tt *testing.T) { - db := setupTestDB(tt) - require.NotEmpty(tt, db) - - webhookRouter := testWebhookRouter(tt, db) - - badWebhookRequest := router.CreateWebhookRequest{ - Noun: "Credential", - } - - badRequestValue := newRequestValue(tt, badWebhookRequest) - req := httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/webhooks", badRequestValue) - w := httptest.NewRecorder() - - c := newRequestContext(w, req) - webhookRouter.CreateWebhook(c) - assert.Contains(tt, w.Body.String(), "invalid create webhook request") - }) - - t.Run("CreateWebhook returns error when verb is not supported", func(tt *testing.T) { - db := setupTestDB(tt) - require.NotEmpty(tt, db) - - webhookRouter := testWebhookRouter(tt, db) - - badWebhookRequest := router.CreateWebhookRequest{ - Noun: "Credential", - Verb: "bad", - URL: "www.abc.com", - } - - badRequestValue := newRequestValue(tt, badWebhookRequest) - req := httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/webhooks", badRequestValue) - w := httptest.NewRecorder() - - c := newRequestContext(w, req) - webhookRouter.CreateWebhook(c) - assert.Contains(tt, w.Body.String(), "invalid create webhook request") - }) - - t.Run("CreateWebhook returns error when url is not supported", func(tt *testing.T) { - db := setupTestDB(tt) - require.NotEmpty(tt, db) - - webhookRouter := testWebhookRouter(tt, db) - - badWebhookRequest := router.CreateWebhookRequest{ - Noun: "Credential", - Verb: "Create", - URL: "badurl", - } - - badRequestValue := newRequestValue(tt, badWebhookRequest) - req := httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/webhooks", badRequestValue) - w := httptest.NewRecorder() - - c := newRequestContext(w, req) - webhookRouter.CreateWebhook(c) - assert.Contains(tt, w.Body.String(), "invalid create webhook request") - }) - - t.Run("CreateWebhook returns error when url is is missing scheme", func(tt *testing.T) { - db := setupTestDB(tt) - require.NotEmpty(tt, db) - - webhookRouter := testWebhookRouter(tt, db) - - badWebhookRequest := router.CreateWebhookRequest{ - Noun: "Credential", - Verb: "Create", - URL: "www.tbd.website", - } - - badRequestValue := newRequestValue(tt, badWebhookRequest) - req := httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/webhooks", badRequestValue) - w := httptest.NewRecorder() - - c := newRequestContext(w, req) - webhookRouter.CreateWebhook(c) - assert.Contains(tt, w.Body.String(), "invalid create webhook request") - }) - - t.Run("CreateWebhook returns valid response", func(tt *testing.T) { - db := setupTestDB(tt) - require.NotEmpty(tt, db) - - webhookRouter := testWebhookRouter(tt, db) - - webhookRequest := router.CreateWebhookRequest{ - Noun: "Manifest", - Verb: "Create", - URL: "https://www.tbd.website/", - } - - requestValue := newRequestValue(tt, webhookRequest) - req := httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/webhooks", requestValue) - w := httptest.NewRecorder() - - c := newRequestContext(w, req) - webhookRouter.CreateWebhook(c) - assert.True(tt, util.Is2xxResponse(w.Code)) - }) - - t.Run("Test Happy Path Delete Webhook", func(tt *testing.T) { - db := setupTestDB(tt) - require.NotEmpty(tt, db) - - webhookRouter := testWebhookRouter(tt, db) - - webhookRequest := router.CreateWebhookRequest{ - Noun: "Manifest", - Verb: "Create", - URL: "https://www.tbd.website/", - } - - requestValue := newRequestValue(tt, webhookRequest) - req := httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/webhooks", requestValue) - w := httptest.NewRecorder() - - c := newRequestContext(w, req) - webhookRouter.CreateWebhook(c) - assert.True(tt, util.Is2xxResponse(w.Code)) - - req = httptest.NewRequest(http.MethodGet, "https://ssi-service.com/v1/webhooks", nil) - w = httptest.NewRecorder() - - c = newRequestContext(w, req) - webhookRouter.ListWebhooks(c) - assert.True(tt, util.Is2xxResponse(w.Code)) - - var resp router.ListWebhooksResponse - err := json.NewDecoder(w.Body).Decode(&resp) - assert.NoError(tt, err) - assert.Len(tt, resp.Webhooks, 1) - - c = newRequestContext(w, req) - webhookRouter.ListWebhooks(c) - assert.True(tt, util.Is2xxResponse(w.Code)) - - deleteWebhookRequest := router.DeleteWebhookRequest{ - Noun: "Manifest", - Verb: "Create", - URL: "https://www.tbd.website/", - } - - requestValue = newRequestValue(tt, deleteWebhookRequest) - req = httptest.NewRequest(http.MethodDelete, "https://ssi-service.com/v1/webhooks", requestValue) - w = httptest.NewRecorder() - - c = newRequestContext(w, req) - webhookRouter.DeleteWebhook(c) - assert.True(tt, util.Is2xxResponse(w.Code)) - - req = httptest.NewRequest(http.MethodGet, "https://ssi-service.com/v1/webhooks", nil) - w = httptest.NewRecorder() - - c = newRequestContext(w, req) - webhookRouter.ListWebhooks(c) - assert.True(tt, util.Is2xxResponse(w.Code)) - - var respAfter router.ListWebhooksResponse - err = json.NewDecoder(w.Body).Decode(&respAfter) - assert.NoError(tt, err) - assert.Len(tt, respAfter.Webhooks, 0) - }) - - t.Run("GetWebhook Throws Error When Webhook None Exist", func(tt *testing.T) { - db := setupTestDB(tt) - require.NotEmpty(tt, db) - - webhookService := testWebhookService(tt, db) - - wh, err := webhookService.GetWebhook(context.Background(), webhook.GetWebhookRequest{Noun: "Credential", Verb: "Create"}) - assert.ErrorContains(tt, err, "webhook does not exist") - assert.Nil(tt, wh) - }) - - t.Run("GetWebhook Returns Webhook That Does Exist", func(tt *testing.T) { - db := setupTestDB(tt) - require.NotEmpty(tt, db) - - webhookService := testWebhookService(tt, db) - - webhookRequest := webhook.CreateWebhookRequest{ - Noun: "Manifest", - Verb: "Create", - URL: "https://www.tbd.website/", - } - - createdWebhook, err := webhookService.CreateWebhook(context.Background(), webhookRequest) - assert.NoError(tt, err) - assert.Equal(tt, createdWebhook.Webhook.Noun, webhookRequest.Noun) - assert.Equal(tt, createdWebhook.Webhook.Verb, webhookRequest.Verb) - assert.Equal(tt, createdWebhook.Webhook.URLS[0], webhookRequest.URL) - - getWebhookRequest := webhook.GetWebhookRequest{Noun: "Manifest", Verb: "Create"} - - gotWebhook, err := webhookService.GetWebhook(context.Background(), getWebhookRequest) - assert.NoError(tt, err) - assert.Equal(tt, gotWebhook.Webhook.Noun, webhookRequest.Noun) - assert.Equal(tt, gotWebhook.Webhook.Verb, webhookRequest.Verb) - assert.Equal(tt, gotWebhook.Webhook.URLS[0], webhookRequest.URL) - }) - - t.Run("Test Get Webhooks", func(tt *testing.T) { - db := setupTestDB(tt) - require.NotEmpty(tt, db) - - webhookService := testWebhookService(tt, db) - - gotWebhooks, err := webhookService.ListWebhooks(context.Background()) - assert.NoError(tt, err) - assert.Len(tt, gotWebhooks.Webhooks, 0) - - _, err = webhookService.CreateWebhook(context.Background(), webhook.CreateWebhookRequest{ - Noun: "Manifest", - Verb: "Create", - URL: "https://www.tbd.website/", - }) - assert.NoError(tt, err) - - _, err = webhookService.CreateWebhook(context.Background(), webhook.CreateWebhookRequest{ - Noun: "Manifest", - Verb: "Create", - URL: "https://www.tbd.dev/", + for _, test := range testutil.TestDatabases { + t.Run(test.Name, func(t *testing.T) { + t.Run("CreateWebhook returns error when missing request", func(tt *testing.T) { + db := test.ServiceStorage(t) + require.NotEmpty(tt, db) + + webhookRouter := testWebhookRouter(tt, db) + + badWebhookRequest := router.CreateWebhookRequest{ + Noun: "Credential", + } + + badRequestValue := newRequestValue(tt, badWebhookRequest) + req := httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/webhooks", badRequestValue) + w := httptest.NewRecorder() + + c := newRequestContext(w, req) + webhookRouter.CreateWebhook(c) + assert.Contains(tt, w.Body.String(), "invalid create webhook request") + }) + + t.Run("CreateWebhook returns error when verb is not supported", func(tt *testing.T) { + db := test.ServiceStorage(t) + require.NotEmpty(tt, db) + + webhookRouter := testWebhookRouter(tt, db) + + badWebhookRequest := router.CreateWebhookRequest{ + Noun: "Credential", + Verb: "bad", + URL: "www.abc.com", + } + + badRequestValue := newRequestValue(tt, badWebhookRequest) + req := httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/webhooks", badRequestValue) + w := httptest.NewRecorder() + + c := newRequestContext(w, req) + webhookRouter.CreateWebhook(c) + assert.Contains(tt, w.Body.String(), "invalid create webhook request") + }) + + t.Run("CreateWebhook returns error when url is not supported", func(tt *testing.T) { + db := test.ServiceStorage(t) + require.NotEmpty(tt, db) + + webhookRouter := testWebhookRouter(tt, db) + + badWebhookRequest := router.CreateWebhookRequest{ + Noun: "Credential", + Verb: "Create", + URL: "badurl", + } + + badRequestValue := newRequestValue(tt, badWebhookRequest) + req := httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/webhooks", badRequestValue) + w := httptest.NewRecorder() + + c := newRequestContext(w, req) + webhookRouter.CreateWebhook(c) + assert.Contains(tt, w.Body.String(), "invalid create webhook request") + }) + + t.Run("CreateWebhook returns error when url is is missing scheme", func(tt *testing.T) { + db := test.ServiceStorage(t) + require.NotEmpty(tt, db) + + webhookRouter := testWebhookRouter(tt, db) + + badWebhookRequest := router.CreateWebhookRequest{ + Noun: "Credential", + Verb: "Create", + URL: "www.tbd.website", + } + + badRequestValue := newRequestValue(tt, badWebhookRequest) + req := httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/webhooks", badRequestValue) + w := httptest.NewRecorder() + + c := newRequestContext(w, req) + webhookRouter.CreateWebhook(c) + assert.Contains(tt, w.Body.String(), "invalid create webhook request") + }) + + t.Run("CreateWebhook returns valid response", func(tt *testing.T) { + db := test.ServiceStorage(t) + require.NotEmpty(tt, db) + + webhookRouter := testWebhookRouter(tt, db) + + webhookRequest := router.CreateWebhookRequest{ + Noun: "Manifest", + Verb: "Create", + URL: "https://www.tbd.website/", + } + + requestValue := newRequestValue(tt, webhookRequest) + req := httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/webhooks", requestValue) + w := httptest.NewRecorder() + + c := newRequestContext(w, req) + webhookRouter.CreateWebhook(c) + assert.True(tt, util.Is2xxResponse(w.Code)) + }) + + t.Run("Test Happy Path Delete Webhook", func(tt *testing.T) { + db := test.ServiceStorage(t) + require.NotEmpty(tt, db) + + webhookRouter := testWebhookRouter(tt, db) + + webhookRequest := router.CreateWebhookRequest{ + Noun: "Manifest", + Verb: "Create", + URL: "https://www.tbd.website/", + } + + requestValue := newRequestValue(tt, webhookRequest) + req := httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/webhooks", requestValue) + w := httptest.NewRecorder() + + c := newRequestContext(w, req) + webhookRouter.CreateWebhook(c) + assert.True(tt, util.Is2xxResponse(w.Code)) + + req = httptest.NewRequest(http.MethodGet, "https://ssi-service.com/v1/webhooks", nil) + w = httptest.NewRecorder() + + c = newRequestContext(w, req) + webhookRouter.ListWebhooks(c) + assert.True(tt, util.Is2xxResponse(w.Code)) + + var resp router.ListWebhooksResponse + err := json.NewDecoder(w.Body).Decode(&resp) + assert.NoError(tt, err) + assert.Len(tt, resp.Webhooks, 1) + + c = newRequestContext(w, req) + webhookRouter.ListWebhooks(c) + assert.True(tt, util.Is2xxResponse(w.Code)) + + deleteWebhookRequest := router.DeleteWebhookRequest{ + Noun: "Manifest", + Verb: "Create", + URL: "https://www.tbd.website/", + } + + requestValue = newRequestValue(tt, deleteWebhookRequest) + req = httptest.NewRequest(http.MethodDelete, "https://ssi-service.com/v1/webhooks", requestValue) + w = httptest.NewRecorder() + + c = newRequestContext(w, req) + webhookRouter.DeleteWebhook(c) + assert.True(tt, util.Is2xxResponse(w.Code)) + + req = httptest.NewRequest(http.MethodGet, "https://ssi-service.com/v1/webhooks", nil) + w = httptest.NewRecorder() + + c = newRequestContext(w, req) + webhookRouter.ListWebhooks(c) + assert.True(tt, util.Is2xxResponse(w.Code)) + + var respAfter router.ListWebhooksResponse + err = json.NewDecoder(w.Body).Decode(&respAfter) + assert.NoError(tt, err) + assert.Len(tt, respAfter.Webhooks, 0) + }) + + t.Run("GetWebhook Throws Error When Webhook None Exist", func(tt *testing.T) { + db := test.ServiceStorage(t) + require.NotEmpty(tt, db) + + webhookService := testWebhookService(tt, db) + + wh, err := webhookService.GetWebhook(context.Background(), webhook.GetWebhookRequest{Noun: "Credential", Verb: "Create"}) + assert.ErrorContains(tt, err, "webhook does not exist") + assert.Nil(tt, wh) + }) + + t.Run("GetWebhook Returns Webhook That Does Exist", func(tt *testing.T) { + db := test.ServiceStorage(t) + require.NotEmpty(tt, db) + + webhookService := testWebhookService(tt, db) + + webhookRequest := webhook.CreateWebhookRequest{ + Noun: "Manifest", + Verb: "Create", + URL: "https://www.tbd.website/", + } + + createdWebhook, err := webhookService.CreateWebhook(context.Background(), webhookRequest) + assert.NoError(tt, err) + assert.Equal(tt, createdWebhook.Webhook.Noun, webhookRequest.Noun) + assert.Equal(tt, createdWebhook.Webhook.Verb, webhookRequest.Verb) + assert.Equal(tt, createdWebhook.Webhook.URLS[0], webhookRequest.URL) + + getWebhookRequest := webhook.GetWebhookRequest{Noun: "Manifest", Verb: "Create"} + + gotWebhook, err := webhookService.GetWebhook(context.Background(), getWebhookRequest) + assert.NoError(tt, err) + assert.Equal(tt, gotWebhook.Webhook.Noun, webhookRequest.Noun) + assert.Equal(tt, gotWebhook.Webhook.Verb, webhookRequest.Verb) + assert.Equal(tt, gotWebhook.Webhook.URLS[0], webhookRequest.URL) + }) + + t.Run("Test Get Webhooks", func(tt *testing.T) { + db := test.ServiceStorage(t) + require.NotEmpty(tt, db) + + webhookService := testWebhookService(tt, db) + + gotWebhooks, err := webhookService.ListWebhooks(context.Background()) + assert.NoError(tt, err) + assert.Len(tt, gotWebhooks.Webhooks, 0) + + _, err = webhookService.CreateWebhook(context.Background(), webhook.CreateWebhookRequest{ + Noun: "Manifest", + Verb: "Create", + URL: "https://www.tbd.website/", + }) + assert.NoError(tt, err) + + _, err = webhookService.CreateWebhook(context.Background(), webhook.CreateWebhookRequest{ + Noun: "Manifest", + Verb: "Create", + URL: "https://www.tbd.dev/", + }) + assert.NoError(tt, err) + + _, err = webhookService.CreateWebhook(context.Background(), webhook.CreateWebhookRequest{ + Noun: "Manifest", + Verb: "Delete", + URL: "https://www.tbd.website/", + }) + assert.NoError(tt, err) + + gotWebhooks, err = webhookService.ListWebhooks(context.Background()) + assert.NoError(tt, err) + assert.Len(tt, gotWebhooks.Webhooks, 2) + }) + + t.Run("Test Delete Webhook", func(tt *testing.T) { + db := test.ServiceStorage(t) + require.NotEmpty(tt, db) + + webhookService := testWebhookService(tt, db) + + err := webhookService.DeleteWebhook(context.Background(), webhook.DeleteWebhookRequest{Noun: "Credential", Verb: "Create", URL: "https://www.tbd.website/"}) + assert.ErrorContains(tt, err, "webhook does not exist") + + _, err = webhookService.CreateWebhook(context.Background(), webhook.CreateWebhookRequest{ + Noun: "Manifest", + Verb: "Create", + URL: "https://www.tbd.website/", + }) + assert.NoError(tt, err) + + gotWebhook, err := webhookService.GetWebhook(context.Background(), webhook.GetWebhookRequest{Noun: "Manifest", Verb: "Create"}) + assert.NoError(tt, err) + assert.Equal(tt, gotWebhook.Webhook.Noun, webhook.Noun("Manifest")) + assert.Equal(tt, gotWebhook.Webhook.Verb, webhook.Verb("Create")) + assert.Equal(tt, gotWebhook.Webhook.URLS[0], "https://www.tbd.website/") + + err = webhookService.DeleteWebhook(context.Background(), webhook.DeleteWebhookRequest{Noun: "Manifest", Verb: "Create", URL: "https://www.tbd.website/"}) + assert.NoError(tt, err) + + gotWebhook, err = webhookService.GetWebhook(context.Background(), webhook.GetWebhookRequest{Noun: "Manifest", Verb: "Create"}) + assert.ErrorContains(tt, err, "webhook does not exist") + assert.Empty(tt, gotWebhook) + }) }) - assert.NoError(tt, err) - - _, err = webhookService.CreateWebhook(context.Background(), webhook.CreateWebhookRequest{ - Noun: "Manifest", - Verb: "Delete", - URL: "https://www.tbd.website/", - }) - assert.NoError(tt, err) - - gotWebhooks, err = webhookService.ListWebhooks(context.Background()) - assert.NoError(tt, err) - assert.Len(tt, gotWebhooks.Webhooks, 2) - }) - - t.Run("Test Delete Webhook", func(tt *testing.T) { - db := setupTestDB(tt) - require.NotEmpty(tt, db) - - webhookService := testWebhookService(tt, db) - - err := webhookService.DeleteWebhook(context.Background(), webhook.DeleteWebhookRequest{Noun: "Credential", Verb: "Create", URL: "https://www.tbd.website/"}) - assert.ErrorContains(tt, err, "webhook does not exist") - - _, err = webhookService.CreateWebhook(context.Background(), webhook.CreateWebhookRequest{ - Noun: "Manifest", - Verb: "Create", - URL: "https://www.tbd.website/", - }) - assert.NoError(tt, err) - - gotWebhook, err := webhookService.GetWebhook(context.Background(), webhook.GetWebhookRequest{Noun: "Manifest", Verb: "Create"}) - assert.NoError(tt, err) - assert.Equal(tt, gotWebhook.Webhook.Noun, webhook.Noun("Manifest")) - assert.Equal(tt, gotWebhook.Webhook.Verb, webhook.Verb("Create")) - assert.Equal(tt, gotWebhook.Webhook.URLS[0], "https://www.tbd.website/") - - err = webhookService.DeleteWebhook(context.Background(), webhook.DeleteWebhookRequest{Noun: "Manifest", Verb: "Create", URL: "https://www.tbd.website/"}) - assert.NoError(tt, err) - - gotWebhook, err = webhookService.GetWebhook(context.Background(), webhook.GetWebhookRequest{Noun: "Manifest", Verb: "Create"}) - assert.ErrorContains(tt, err, "webhook does not exist") - assert.Empty(tt, gotWebhook) - }) + } } diff --git a/pkg/service/did/ion_test.go b/pkg/service/did/ion_test.go index a753b2e99..aa9494244 100644 --- a/pkg/service/did/ion_test.go +++ b/pkg/service/did/ion_test.go @@ -9,218 +9,222 @@ import ( "github.com/TBD54566975/ssi-sdk/did" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "gopkg.in/h2non/gock.v1" - "github.com/tbd54566975/ssi-service/config" "github.com/tbd54566975/ssi-service/pkg/service/keystore" "github.com/tbd54566975/ssi-service/pkg/storage" + "github.com/tbd54566975/ssi-service/pkg/testutil" + "gopkg.in/h2non/gock.v1" ) func TestIONHandler(t *testing.T) { - t.Run("Test Create ION Handler", func(tt *testing.T) { - handler, err := NewIONHandler("", nil, nil) - assert.Error(tt, err) - assert.Empty(tt, handler) - assert.Contains(tt, err.Error(), "baseURL cannot be empty") - - s := setupTestDB(tt) - keystoreService := testKeyStoreService(tt, s) - didStorage, err := NewDIDStorage(s) - assert.NoError(tt, err) - handler, err = NewIONHandler("bad", nil, keystoreService) - assert.Error(tt, err) - assert.Empty(tt, handler) - assert.Contains(tt, err.Error(), "storage cannot be empty") - - handler, err = NewIONHandler("bad", didStorage, nil) - assert.Error(tt, err) - assert.Empty(tt, handler) - assert.Contains(tt, err.Error(), "keystore cannot be empty") - - handler, err = NewIONHandler("bad", didStorage, keystoreService) - assert.Error(tt, err) - assert.Empty(tt, handler) - assert.Contains(tt, err.Error(), "invalid resolution URL") - - handler, err = NewIONHandler("https://example.com", didStorage, keystoreService) - assert.NoError(tt, err) - assert.NotEmpty(tt, handler) - - assert.Equal(tt, handler.GetMethod(), did.IONMethod) - }) - - t.Run("Test Create DID", func(tt *testing.T) { - // create a handler - s := setupTestDB(tt) - keystoreService := testKeyStoreService(tt, s) - didStorage, err := NewDIDStorage(s) - assert.NoError(tt, err) - handler, err := NewIONHandler("https://test-ion-resolver.com", didStorage, keystoreService) - assert.NoError(tt, err) - assert.NotEmpty(tt, handler) - - gock.New("https://test-ion-resolver.com"). - Post("/operations"). - Reply(200) - defer gock.Off() - - // create a did - created, err := handler.CreateDID(context.Background(), CreateDIDRequest{ - Method: did.IONMethod, - KeyType: crypto.Ed25519, - }) - assert.NoError(tt, err) - assert.NotEmpty(tt, created) - }) - - t.Run("Test Create DID", func(tt *testing.T) { - gock.New("https://ion.tbddev.org"). - Post("/operations"). - Reply(200) - defer gock.Off() - - // create a handler - s := setupTestDB(tt) - keystoreService := testKeyStoreService(tt, s) - didStorage, err := NewDIDStorage(s) - assert.NoError(tt, err) - handler, err := NewIONHandler("https://ion.tbddev.org", didStorage, keystoreService) - assert.NoError(tt, err) - assert.NotEmpty(tt, handler) - - // create a did - created, err := handler.CreateDID(context.Background(), CreateDIDRequest{ - Method: did.IONMethod, - KeyType: crypto.Ed25519, - }) - assert.NoError(tt, err) - assert.NotEmpty(tt, created) - - // get the did - gotDID, err := handler.GetDID(context.Background(), GetDIDRequest{ - Method: did.IONMethod, - ID: created.DID.ID, + for _, test := range testutil.TestDatabases { + t.Run(test.Name, func(t *testing.T) { + t.Run("Test Create ION Handler", func(tt *testing.T) { + handler, err := NewIONHandler("", nil, nil) + assert.Error(tt, err) + assert.Empty(tt, handler) + assert.Contains(tt, err.Error(), "baseURL cannot be empty") + + s := test.ServiceStorage(t) + keystoreService := testKeyStoreService(tt, s) + didStorage, err := NewDIDStorage(s) + assert.NoError(tt, err) + handler, err = NewIONHandler("bad", nil, keystoreService) + assert.Error(tt, err) + assert.Empty(tt, handler) + assert.Contains(tt, err.Error(), "storage cannot be empty") + + handler, err = NewIONHandler("bad", didStorage, nil) + assert.Error(tt, err) + assert.Empty(tt, handler) + assert.Contains(tt, err.Error(), "keystore cannot be empty") + + handler, err = NewIONHandler("bad", didStorage, keystoreService) + assert.Error(tt, err) + assert.Empty(tt, handler) + assert.Contains(tt, err.Error(), "invalid resolution URL") + + handler, err = NewIONHandler("https://example.com", didStorage, keystoreService) + assert.NoError(tt, err) + assert.NotEmpty(tt, handler) + + assert.Equal(tt, handler.GetMethod(), did.IONMethod) + }) + + t.Run("Test Create DID", func(tt *testing.T) { + // create a handler + s := test.ServiceStorage(t) + keystoreService := testKeyStoreService(tt, s) + didStorage, err := NewDIDStorage(s) + assert.NoError(tt, err) + handler, err := NewIONHandler("https://test-ion-resolver.com", didStorage, keystoreService) + assert.NoError(tt, err) + assert.NotEmpty(tt, handler) + + gock.New("https://test-ion-resolver.com"). + Post("/operations"). + Reply(200) + defer gock.Off() + + // create a did + created, err := handler.CreateDID(context.Background(), CreateDIDRequest{ + Method: did.IONMethod, + KeyType: crypto.Ed25519, + }) + assert.NoError(tt, err) + assert.NotEmpty(tt, created) + }) + + t.Run("Test Create DID", func(tt *testing.T) { + gock.New("https://ion.tbddev.org"). + Post("/operations"). + Reply(200) + defer gock.Off() + + // create a handler + s := test.ServiceStorage(t) + keystoreService := testKeyStoreService(tt, s) + didStorage, err := NewDIDStorage(s) + assert.NoError(tt, err) + handler, err := NewIONHandler("https://ion.tbddev.org", didStorage, keystoreService) + assert.NoError(tt, err) + assert.NotEmpty(tt, handler) + + // create a did + created, err := handler.CreateDID(context.Background(), CreateDIDRequest{ + Method: did.IONMethod, + KeyType: crypto.Ed25519, + }) + assert.NoError(tt, err) + assert.NotEmpty(tt, created) + + // get the did + gotDID, err := handler.GetDID(context.Background(), GetDIDRequest{ + Method: did.IONMethod, + ID: created.DID.ID, + }) + assert.NoError(tt, err) + assert.NotEmpty(tt, gotDID) + }) + + t.Run("Test Get DID from storage", func(tt *testing.T) { + // create a handler + s := test.ServiceStorage(t) + keystoreService := testKeyStoreService(tt, s) + didStorage, err := NewDIDStorage(s) + assert.NoError(tt, err) + handler, err := NewIONHandler("https://test-ion-resolver.com", didStorage, keystoreService) + assert.NoError(tt, err) + assert.NotEmpty(tt, handler) + + gock.New("https://test-ion-resolver.com"). + Post("/operations"). + Reply(200) + defer gock.Off() + + // create a did + created, err := handler.CreateDID(context.Background(), CreateDIDRequest{ + Method: did.IONMethod, + KeyType: crypto.Ed25519, + }) + assert.NoError(tt, err) + assert.NotEmpty(tt, created) + + gock.New("https://test-ion-resolver.com"). + Get("/identifiers/" + created.DID.ID). + Reply(200).BodyString(fmt.Sprintf(`{"didDocument": {"id": "%s"}}`, created.DID.ID)) + defer gock.Off() + + // get the did + gotDID, err := handler.GetDID(context.Background(), GetDIDRequest{ + Method: did.IONMethod, + ID: created.DID.ID, + }) + assert.NoError(tt, err) + assert.NotEmpty(tt, gotDID) + assert.Equal(tt, created.DID.ID, gotDID.DID.ID) + }) + + t.Run("Test Get DIDs from storage", func(tt *testing.T) { + // create a handler + s := test.ServiceStorage(t) + keystoreService := testKeyStoreService(tt, s) + didStorage, err := NewDIDStorage(s) + assert.NoError(tt, err) + handler, err := NewIONHandler("https://test-ion-resolver.com", didStorage, keystoreService) + assert.NoError(tt, err) + assert.NotEmpty(tt, handler) + + gock.New("https://test-ion-resolver.com"). + Post("/operations"). + Reply(200) + defer gock.Off() + + // create a did + created, err := handler.CreateDID(context.Background(), CreateDIDRequest{ + Method: did.IONMethod, + KeyType: crypto.Ed25519, + }) + assert.NoError(tt, err) + assert.NotEmpty(tt, created) + + gock.New("https://test-ion-resolver.com"). + Get("/identifiers/" + created.DID.ID). + Reply(200).BodyString(fmt.Sprintf(`{"didDocument": {"id": "%s"}}`, created.DID.ID)) + defer gock.Off() + + // get all DIDs + gotDIDs, err := handler.ListDIDs(context.Background(), nil) + assert.NoError(tt, err) + assert.NotEmpty(tt, gotDIDs) + assert.Len(tt, gotDIDs.DIDs, 1) + + // delete a did + err = handler.SoftDeleteDID(context.Background(), DeleteDIDRequest{ + Method: did.IONMethod, + ID: created.DID.ID, + }) + assert.NoError(tt, err) + + // get all DIDs after deleting + gotDIDsAfterDelete, err := handler.ListDIDs(context.Background(), nil) + assert.NoError(tt, err) + assert.NotEmpty(tt, gotDIDs) + assert.Len(tt, gotDIDsAfterDelete.DIDs, 0) + + // get all deleted DIDs after delete + gotDeletedDIDs, err := handler.ListDeletedDIDs(context.Background()) + assert.NoError(tt, err) + assert.NotEmpty(tt, gotDIDs) + assert.Len(tt, gotDeletedDIDs.DIDs, 1) + }) + + t.Run("Test Get DID from resolver", func(tt *testing.T) { + // create a handler + s := test.ServiceStorage(t) + keystoreService := testKeyStoreService(tt, s) + didStorage, err := NewDIDStorage(s) + assert.NoError(tt, err) + handler, err := NewIONHandler("https://test-ion-resolver.com", didStorage, keystoreService) + assert.NoError(tt, err) + assert.NotEmpty(tt, handler) + + gock.New("https://test-ion-resolver.com"). + Get("/identifiers/did:ion:test"). + Reply(200).BodyString(`{"didDocument": {"id": "did:ion:test"}}`) + defer gock.Off() + + // get the did + gotDID, err := handler.GetDID(context.Background(), GetDIDRequest{ + Method: did.IONMethod, + ID: "did:ion:test", + }) + assert.NoError(tt, err) + assert.NotEmpty(tt, gotDID) + assert.Equal(tt, "did:ion:test", gotDID.DID.ID) + }) }) - assert.NoError(tt, err) - assert.NotEmpty(tt, gotDID) - }) - - t.Run("Test Get DID from storage", func(tt *testing.T) { - // create a handler - s := setupTestDB(tt) - keystoreService := testKeyStoreService(tt, s) - didStorage, err := NewDIDStorage(s) - assert.NoError(tt, err) - handler, err := NewIONHandler("https://test-ion-resolver.com", didStorage, keystoreService) - assert.NoError(tt, err) - assert.NotEmpty(tt, handler) - - gock.New("https://test-ion-resolver.com"). - Post("/operations"). - Reply(200) - defer gock.Off() - - // create a did - created, err := handler.CreateDID(context.Background(), CreateDIDRequest{ - Method: did.IONMethod, - KeyType: crypto.Ed25519, - }) - assert.NoError(tt, err) - assert.NotEmpty(tt, created) - - gock.New("https://test-ion-resolver.com"). - Get("/identifiers/" + created.DID.ID). - Reply(200).BodyString(fmt.Sprintf(`{"didDocument": {"id": "%s"}}`, created.DID.ID)) - defer gock.Off() - - // get the did - gotDID, err := handler.GetDID(context.Background(), GetDIDRequest{ - Method: did.IONMethod, - ID: created.DID.ID, - }) - assert.NoError(tt, err) - assert.NotEmpty(tt, gotDID) - assert.Equal(tt, created.DID.ID, gotDID.DID.ID) - }) - - t.Run("Test Get DIDs from storage", func(tt *testing.T) { - // create a handler - s := setupTestDB(tt) - keystoreService := testKeyStoreService(tt, s) - didStorage, err := NewDIDStorage(s) - assert.NoError(tt, err) - handler, err := NewIONHandler("https://test-ion-resolver.com", didStorage, keystoreService) - assert.NoError(tt, err) - assert.NotEmpty(tt, handler) - - gock.New("https://test-ion-resolver.com"). - Post("/operations"). - Reply(200) - defer gock.Off() - - // create a did - created, err := handler.CreateDID(context.Background(), CreateDIDRequest{ - Method: did.IONMethod, - KeyType: crypto.Ed25519, - }) - assert.NoError(tt, err) - assert.NotEmpty(tt, created) - - gock.New("https://test-ion-resolver.com"). - Get("/identifiers/" + created.DID.ID). - Reply(200).BodyString(fmt.Sprintf(`{"didDocument": {"id": "%s"}}`, created.DID.ID)) - defer gock.Off() - - // get all DIDs - gotDIDs, err := handler.ListDIDs(context.Background(), nil) - assert.NoError(tt, err) - assert.NotEmpty(tt, gotDIDs) - assert.Len(tt, gotDIDs.DIDs, 1) - - // delete a did - err = handler.SoftDeleteDID(context.Background(), DeleteDIDRequest{ - Method: did.IONMethod, - ID: created.DID.ID, - }) - assert.NoError(tt, err) - - // get all DIDs after deleting - gotDIDsAfterDelete, err := handler.ListDIDs(context.Background(), nil) - assert.NoError(tt, err) - assert.NotEmpty(tt, gotDIDs) - assert.Len(tt, gotDIDsAfterDelete.DIDs, 0) - - // get all deleted DIDs after delete - gotDeletedDIDs, err := handler.ListDeletedDIDs(context.Background()) - assert.NoError(tt, err) - assert.NotEmpty(tt, gotDIDs) - assert.Len(tt, gotDeletedDIDs.DIDs, 1) - }) - - t.Run("Test Get DID from resolver", func(tt *testing.T) { - // create a handler - s := setupTestDB(tt) - keystoreService := testKeyStoreService(tt, s) - didStorage, err := NewDIDStorage(s) - assert.NoError(tt, err) - handler, err := NewIONHandler("https://test-ion-resolver.com", didStorage, keystoreService) - assert.NoError(tt, err) - assert.NotEmpty(tt, handler) - - gock.New("https://test-ion-resolver.com"). - Get("/identifiers/did:ion:test"). - Reply(200).BodyString(`{"didDocument": {"id": "did:ion:test"}}`) - defer gock.Off() - - // get the did - gotDID, err := handler.GetDID(context.Background(), GetDIDRequest{ - Method: did.IONMethod, - ID: "did:ion:test", - }) - assert.NoError(tt, err) - assert.NotEmpty(tt, gotDID) - assert.Equal(tt, "did:ion:test", gotDID.DID.ID) - }) + } } func testKeyStoreService(t *testing.T, db storage.ServiceStorage) *keystore.Service { diff --git a/pkg/service/did/storage_test.go b/pkg/service/did/storage_test.go index b64cf0fa1..b5abd57b0 100644 --- a/pkg/service/did/storage_test.go +++ b/pkg/service/did/storage_test.go @@ -2,229 +2,206 @@ package did import ( "context" - "os" "testing" didsdk "github.com/TBD54566975/ssi-sdk/did" "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/tbd54566975/ssi-service/pkg/storage" + "github.com/tbd54566975/ssi-service/pkg/testutil" ) func TestStorage(t *testing.T) { - t.Run("Create bad DID - no namespace", func(tt *testing.T) { - ds, err := NewDIDStorage(setupTestDB(tt)) - assert.NoError(tt, err) - assert.NotEmpty(tt, ds) - - // create a did - toStore := DefaultStoredDID{ - ID: "did:bad:test", - DID: didsdk.Document{ - ID: "did:bad:test", - }, - SoftDeleted: false, - } - - // store - err = ds.StoreDID(context.Background(), toStore) - assert.Error(tt, err) - assert.Contains(tt, err.Error(), "could not store DID") - }) - - t.Run("Get bad DID - namespace does not exist", func(tt *testing.T) { - ds, err := NewDIDStorage(setupTestDB(tt)) - assert.NoError(tt, err) - assert.NotEmpty(tt, ds) - - // store - gotDID, err := ds.GetDIDDefault(context.Background(), "did:test:bad") - assert.Error(tt, err) - assert.Empty(tt, gotDID) - assert.Contains(tt, err.Error(), "could not get DID: did:test:bad: no namespace found for DID method: test") - }) - - t.Run("Get bad DID - does not exist", func(tt *testing.T) { - ds, err := NewDIDStorage(setupTestDB(tt)) - assert.NoError(tt, err) - assert.NotEmpty(tt, ds) - - // store - gotDID, err := ds.GetDIDDefault(context.Background(), "did:key:bad") - assert.Error(tt, err) - assert.Empty(tt, gotDID) - assert.Contains(tt, err.Error(), "could not get DID: did:key:bad") - }) - - t.Run("Create and Get DID", func(tt *testing.T) { - ds, err := NewDIDStorage(setupTestDB(tt)) - assert.NoError(tt, err) - assert.NotEmpty(tt, ds) - - // create a did - toStore := DefaultStoredDID{ - ID: "did:key:test", - DID: didsdk.Document{ - ID: "did:key:test", - }, - SoftDeleted: false, - } - - // store - err = ds.StoreDID(context.Background(), toStore) - assert.NoError(tt, err) - - // get it back as a default - got, err := ds.GetDIDDefault(context.Background(), "did:key:test") - assert.NoError(tt, err) - assert.Equal(tt, toStore, *got) - - // get it back as a did - outDID := new(DefaultStoredDID) - err = ds.GetDID(context.Background(), "did:key:test", outDID) - assert.NoError(tt, err) - assert.Equal(tt, toStore, *outDID) - }) - - t.Run("Create and Get DID of a custom type", func(tt *testing.T) { - ds, err := NewDIDStorage(setupTestDB(tt)) - assert.NoError(tt, err) - assert.NotEmpty(tt, ds) - - // create a did - toStore := customStoredDID{ - ID: "did:key:test", - Party: false, - } - - // store - err = ds.StoreDID(context.Background(), toStore) - assert.NoError(tt, err) - - // get it back as a default - which won't be equal - got, err := ds.GetDIDDefault(context.Background(), "did:key:test") - assert.NoError(tt, err) - assert.NotEqual(tt, toStore, *got) - - // get it back as a custom did - outDID := new(customStoredDID) - err = ds.GetDID(context.Background(), "did:key:test", outDID) - assert.NoError(tt, err) - assert.Equal(tt, toStore, *outDID) - }) - - t.Run("Create and Get Multiple DIDs", func(tt *testing.T) { - ds, err := NewDIDStorage(setupTestDB(tt)) - assert.NoError(tt, err) - assert.NotEmpty(tt, ds) - - // create two dids - toStore1 := DefaultStoredDID{ - ID: "did:key:test-1", - DID: didsdk.Document{ - ID: "did:key:test-1", - }, - SoftDeleted: false, - } - - toStore2 := DefaultStoredDID{ - ID: "did:key:test-2", - DID: didsdk.Document{ - ID: "did:key:test-2", - }, - SoftDeleted: false, - } - - // store - err = ds.StoreDID(context.Background(), toStore1) - assert.NoError(tt, err) - - err = ds.StoreDID(context.Background(), toStore2) - assert.NoError(tt, err) - - // get both back as default - got, err := ds.ListDIDsDefault(context.Background(), didsdk.KeyMethod.String()) - assert.NoError(tt, err) - assert.Len(tt, got, 2) - assert.Contains(tt, got, toStore1) - assert.Contains(tt, got, toStore2) - - // get back as did - gotDIDs, err := ds.ListDIDs(context.Background(), didsdk.KeyMethod.String(), new(DefaultStoredDID)) - assert.NoError(tt, err) - assert.Len(tt, gotDIDs, 2) - assert.Contains(tt, gotDIDs, &toStore1) - assert.Contains(tt, gotDIDs, &toStore2) - }) - - t.Run("Soft delete DID", func(tt *testing.T) { - ds, err := NewDIDStorage(setupTestDB(tt)) - assert.NoError(tt, err) - assert.NotEmpty(tt, ds) - - // create two dids - toStore1 := DefaultStoredDID{ - ID: "did:key:test-1", - DID: didsdk.Document{ - ID: "did:key:test-1", - }, - SoftDeleted: false, - } - - toStore2 := DefaultStoredDID{ - ID: "did:key:test-2", - DID: didsdk.Document{ - ID: "did:key:test-2", - }, - SoftDeleted: false, - } - - // store - err = ds.StoreDID(context.Background(), toStore1) - assert.NoError(tt, err) - - err = ds.StoreDID(context.Background(), toStore2) - assert.NoError(tt, err) - - // get both and verify there are two - gotDIDs, err := ds.ListDIDsDefault(context.Background(), didsdk.KeyMethod.String()) - assert.NoError(tt, err) - assert.Len(tt, gotDIDs, 2) - - // soft delete one - err = ds.DeleteDID(context.Background(), "did:key:test-1") - assert.NoError(tt, err) - - // get it back - _, err = ds.GetDIDDefault(context.Background(), "did:key:test-1") - assert.Error(tt, err) - assert.Contains(tt, err.Error(), "could not get DID: did:key:test-1") - - // get both and verify there is one - gotDIDs, err = ds.ListDIDsDefault(context.Background(), didsdk.KeyMethod.String()) - assert.NoError(tt, err) - assert.Len(tt, gotDIDs, 1) - assert.Contains(tt, gotDIDs, toStore2) - }) -} - -func setupTestDB(t *testing.T) storage.ServiceStorage { - file, err := os.CreateTemp("", "bolt") - require.NoError(t, err) - name := file.Name() - err = file.Close() - require.NoError(t, err) - s, err := storage.NewStorage(storage.Bolt, storage.Option{ - ID: storage.BoltDBFilePathOption, - Option: name, - }) - require.NoError(t, err) - t.Cleanup(func() { - _ = s.Close() - _ = os.Remove(s.URI()) - }) - return s + for _, test := range testutil.TestDatabases { + t.Run(test.Name, func(t *testing.T) { + + t.Run("Create bad DID - no namespace", func(tt *testing.T) { + ds, err := NewDIDStorage(test.ServiceStorage(t)) + assert.NoError(tt, err) + + // create a did + toStore := DefaultStoredDID{ + ID: "did:bad:test", + DID: didsdk.Document{ + ID: "did:bad:test", + }, + SoftDeleted: false, + } + + // store + err = ds.StoreDID(context.Background(), toStore) + assert.Error(tt, err) + assert.Contains(tt, err.Error(), "could not store DID") + }) + + t.Run("Get bad DID - namespace does not exist", func(tt *testing.T) { + ds, err := NewDIDStorage(test.ServiceStorage(t)) + assert.NoError(tt, err) + + // store + gotDID, err := ds.GetDIDDefault(context.Background(), "did:test:bad") + assert.Error(tt, err) + assert.Empty(tt, gotDID) + assert.Contains(tt, err.Error(), "could not get DID: did:test:bad: no namespace found for DID method: test") + }) + + t.Run("Get bad DID - does not exist", func(tt *testing.T) { + ds, err := NewDIDStorage(test.ServiceStorage(t)) + assert.NoError(tt, err) + + // store + gotDID, err := ds.GetDIDDefault(context.Background(), "did:key:bad") + assert.Error(tt, err) + assert.Empty(tt, gotDID) + assert.Contains(tt, err.Error(), "could not get DID: did:key:bad") + }) + + t.Run("Create and Get DID", func(tt *testing.T) { + ds, err := NewDIDStorage(test.ServiceStorage(t)) + assert.NoError(tt, err) + + // create a did + toStore := DefaultStoredDID{ + ID: "did:key:test", + DID: didsdk.Document{ + ID: "did:key:test", + }, + SoftDeleted: false, + } + + // store + err = ds.StoreDID(context.Background(), toStore) + assert.NoError(tt, err) + + // get it back as a default + got, err := ds.GetDIDDefault(context.Background(), "did:key:test") + assert.NoError(tt, err) + assert.Equal(tt, toStore, *got) + + // get it back as a did + outDID := new(DefaultStoredDID) + err = ds.GetDID(context.Background(), "did:key:test", outDID) + assert.NoError(tt, err) + assert.Equal(tt, toStore, *outDID) + }) + + t.Run("Create and Get DID of a custom type", func(tt *testing.T) { + ds, err := NewDIDStorage(test.ServiceStorage(t)) + assert.NoError(tt, err) + + // create a did + toStore := customStoredDID{ + ID: "did:key:test", + Party: false, + } + + // store + err = ds.StoreDID(context.Background(), toStore) + assert.NoError(tt, err) + + // get it back as a default - which won't be equal + got, err := ds.GetDIDDefault(context.Background(), "did:key:test") + assert.NoError(tt, err) + assert.NotEqual(tt, toStore, *got) + + // get it back as a custom did + outDID := new(customStoredDID) + err = ds.GetDID(context.Background(), "did:key:test", outDID) + assert.NoError(tt, err) + assert.Equal(tt, toStore, *outDID) + }) + + t.Run("Create and Get Multiple DIDs", func(tt *testing.T) { + ds, err := NewDIDStorage(test.ServiceStorage(t)) + assert.NoError(tt, err) + + // create two dids + toStore1 := DefaultStoredDID{ + ID: "did:key:test-1", + DID: didsdk.Document{ + ID: "did:key:test-1", + }, + SoftDeleted: false, + } + + toStore2 := DefaultStoredDID{ + ID: "did:key:test-2", + DID: didsdk.Document{ + ID: "did:key:test-2", + }, + SoftDeleted: false, + } + + // store + err = ds.StoreDID(context.Background(), toStore1) + assert.NoError(tt, err) + + err = ds.StoreDID(context.Background(), toStore2) + assert.NoError(tt, err) + + // get both back as default + got, err := ds.ListDIDsDefault(context.Background(), didsdk.KeyMethod.String()) + assert.NoError(tt, err) + assert.Len(tt, got, 2) + assert.Contains(tt, got, toStore1) + assert.Contains(tt, got, toStore2) + + // get back as did + gotDIDs, err := ds.ListDIDs(context.Background(), didsdk.KeyMethod.String(), new(DefaultStoredDID)) + assert.NoError(tt, err) + assert.Len(tt, gotDIDs, 2) + assert.Contains(tt, gotDIDs, &toStore1) + assert.Contains(tt, gotDIDs, &toStore2) + }) + + t.Run("Soft delete DID", func(tt *testing.T) { + ds, err := NewDIDStorage(test.ServiceStorage(t)) + assert.NoError(tt, err) + + // create two dids + toStore1 := DefaultStoredDID{ + ID: "did:key:test-1", + DID: didsdk.Document{ + ID: "did:key:test-1", + }, + SoftDeleted: false, + } + + toStore2 := DefaultStoredDID{ + ID: "did:key:test-2", + DID: didsdk.Document{ + ID: "did:key:test-2", + }, + SoftDeleted: false, + } + + // store + err = ds.StoreDID(context.Background(), toStore1) + assert.NoError(tt, err) + + err = ds.StoreDID(context.Background(), toStore2) + assert.NoError(tt, err) + + // get both and verify there are two + gotDIDs, err := ds.ListDIDsDefault(context.Background(), didsdk.KeyMethod.String()) + assert.NoError(tt, err) + assert.Len(tt, gotDIDs, 2) + + // soft delete one + err = ds.DeleteDID(context.Background(), "did:key:test-1") + assert.NoError(tt, err) + + // get it back + _, err = ds.GetDIDDefault(context.Background(), "did:key:test-1") + assert.Error(tt, err) + assert.Contains(tt, err.Error(), "could not get DID: did:key:test-1") + + // get both and verify there is one + gotDIDs, err = ds.ListDIDsDefault(context.Background(), didsdk.KeyMethod.String()) + assert.NoError(tt, err) + assert.Len(tt, gotDIDs, 1) + assert.Contains(tt, gotDIDs, toStore2) + }) + }) + } } // new stored DID type diff --git a/pkg/service/operation/storage_test.go b/pkg/service/operation/storage_test.go index e3db652d4..4bb152147 100644 --- a/pkg/service/operation/storage_test.go +++ b/pkg/service/operation/storage_test.go @@ -2,13 +2,13 @@ package operation import ( "context" - "os" "testing" "github.com/goccy/go-json" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/stretchr/testify/require" + "github.com/tbd54566975/ssi-service/pkg/testutil" manifeststg "github.com/tbd54566975/ssi-service/pkg/service/manifest/storage" "github.com/tbd54566975/ssi-service/pkg/service/operation/credential" @@ -17,98 +17,85 @@ import ( "github.com/tbd54566975/ssi-service/pkg/storage" ) -func setupTestDB(t *testing.T) storage.ServiceStorage { - file, err := os.CreateTemp("", "bolt") - require.NoError(t, err) - name := file.Name() - s, err := storage.NewStorage(storage.Bolt, storage.Option{ - ID: storage.BoltDBFilePathOption, - Option: name, - }) - require.NoError(t, err) - t.Cleanup(func() { - _ = s.Close() - _ = file.Close() - _ = os.Remove(name) - }) - return s -} - func TestStorage_CancelOperation(t *testing.T) { - s := setupTestDB(t) - data, err := json.Marshal(manifeststg.StoredApplication{}) - require.NoError(t, err) - require.NoError(t, s.Write(context.Background(), credential.ApplicationNamespace, "hello", data)) - - type fields struct { - db storage.ServiceStorage - } - type args struct { - id string - } - tests := []struct { - name string - fields fields - args args - want *opstorage.StoredOperation - wantErr bool - done bool - }{ - { - name: "bad id returns error", - fields: fields{ - db: s, - }, - args: args{ - id: "hello", - }, - wantErr: true, - }, - { - name: "operation for application can be cancelled", - fields: fields{ - db: s, - }, - args: args{ - id: "credentials/responses/hello", - }, - want: &opstorage.StoredOperation{ - ID: "credentials/responses/hello", - Done: true, - }, - }, - { - name: "done operation returns error on cancellation", - done: true, - fields: fields{ - db: s, - }, - args: args{ - id: "credentials/responses/hello", - }, - wantErr: true, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - - opData, err := json.Marshal(opstorage.StoredOperation{ - ID: "credentials/responses/hello", - Done: tt.done, - }) + for _, test := range testutil.TestDatabases { + t.Run(test.Name, func(t *testing.T) { + s := test.ServiceStorage(t) + data, err := json.Marshal(manifeststg.StoredApplication{}) require.NoError(t, err) - require.NoError(t, s.Write(context.Background(), namespace.FromParent("credentials/responses"), "credentials/responses/hello", opData)) + require.NoError(t, s.Write(context.Background(), credential.ApplicationNamespace, "hello", data)) - b := Storage{ - db: tt.fields.db, + type fields struct { + db storage.ServiceStorage + } + type args struct { + id string } - got, err := b.CancelOperation(context.Background(), tt.args.id) - if (err != nil) != tt.wantErr { - t.Errorf("CancelOperation() error = %v, wantErr %v", err, tt.wantErr) - return + tests := []struct { + name string + fields fields + args args + want *opstorage.StoredOperation + wantErr bool + done bool + }{ + { + name: "bad id returns error", + fields: fields{ + db: s, + }, + args: args{ + id: "hello", + }, + wantErr: true, + }, + { + name: "operation for application can be cancelled", + fields: fields{ + db: s, + }, + args: args{ + id: "credentials/responses/hello", + }, + want: &opstorage.StoredOperation{ + ID: "credentials/responses/hello", + Done: true, + }, + }, + { + name: "done operation returns error on cancellation", + done: true, + fields: fields{ + db: s, + }, + args: args{ + id: "credentials/responses/hello", + }, + wantErr: true, + }, } - if diff := cmp.Diff(got, tt.want, cmpopts.IgnoreFields(opstorage.StoredOperation{}, "Response")); diff != "" { - t.Errorf("CancelOperation() -got, +want:\n%s", diff) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + + opData, err := json.Marshal(opstorage.StoredOperation{ + ID: "credentials/responses/hello", + Done: tt.done, + }) + require.NoError(t, err) + require.NoError(t, s.Write(context.Background(), namespace.FromParent("credentials/responses"), "credentials/responses/hello", opData)) + + b := Storage{ + db: tt.fields.db, + } + got, err := b.CancelOperation(context.Background(), tt.args.id) + if (err != nil) != tt.wantErr { + t.Errorf("CancelOperation() error = %v, wantErr %v", err, tt.wantErr) + return + } + if diff := cmp.Diff(got, tt.want, cmpopts.IgnoreFields(opstorage.StoredOperation{}, "Response")); diff != "" { + t.Errorf("CancelOperation() -got, +want:\n%s", diff) + } + }) } }) } diff --git a/pkg/storage/redis.go b/pkg/storage/redis.go index e3adc5087..bfabd188c 100644 --- a/pkg/storage/redis.go +++ b/pkg/storage/redis.go @@ -351,8 +351,10 @@ func (b *RedisDB) Delete(ctx context.Context, namespace, key string) error { } res, err := b.db.GetDel(ctx, nameSpaceKey).Result() - if res == "" { - return errors.Wrapf(err, "key<%s> and namespace<%s> does not exist", key, namespace) + + // if we delete something that doesn't exist, don't return any error + if res == "" && errors.Is(err, goredislib.Nil) { + return nil } return err diff --git a/pkg/testutil/testutil.go b/pkg/testutil/testutil.go new file mode 100644 index 000000000..e13a3af40 --- /dev/null +++ b/pkg/testutil/testutil.go @@ -0,0 +1,67 @@ +package testutil + +import ( + "os" + "testing" + + "github.com/alicebob/miniredis/v2" + "github.com/stretchr/testify/require" + "github.com/tbd54566975/ssi-service/pkg/storage" +) + +var TestDatabases = []struct { + Name string + ServiceStorage func(t *testing.T) storage.ServiceStorage +}{ + { + Name: "Test with Bolt DB", + ServiceStorage: setupBoltTestDB, + }, + { + Name: "Test with Redis DB", + ServiceStorage: setupRedisTestDB, + }, +} + +func setupBoltTestDB(t *testing.T) storage.ServiceStorage { + file, err := os.CreateTemp("", "bolt") + require.NoError(t, err) + name := file.Name() + err = file.Close() + require.NoError(t, err) + s, err := storage.NewStorage(storage.Bolt, storage.Option{ + ID: storage.BoltDBFilePathOption, + Option: name, + }) + + require.NoError(t, err) + + t.Cleanup(func() { + _ = s.Close() + _ = os.Remove(s.URI()) + }) + + return s +} + +func setupRedisTestDB(t *testing.T) storage.ServiceStorage { + server := miniredis.RunT(t) + options := []storage.Option{ + { + ID: storage.RedisAddressOption, + Option: server.Addr(), + }, + { + ID: storage.PasswordOption, + Option: "test-password", + }, + } + s, err := storage.NewStorage(storage.Redis, options...) + require.NoError(t, err) + + t.Cleanup(func() { + _ = s.Close() + }) + + return s +}