From a82d75d816967b52c546b38f82842030cde3aba4 Mon Sep 17 00:00:00 2001 From: Andres Uribe Gonzalez Date: Thu, 12 Jan 2023 23:21:24 -0500 Subject: [PATCH] Added e2e test for automatic issuance and bug fixes. --- integration/common.go | 20 ++++ integration/steelthread_integration_test.go | 95 +++++++++++++++++++ .../testdata/issuance-template-input.json | 22 +++++ pkg/server/middleware/panics.go | 5 +- pkg/server/router/manifest.go | 11 ++- pkg/server/server_issuance_test.go | 90 ++++++------------ pkg/server/server_manifest_test.go | 40 ++++---- pkg/service/issuing/model.go | 37 +++----- pkg/service/manifest/model/model.go | 4 +- pkg/service/manifest/response.go | 8 +- pkg/service/manifest/service.go | 10 +- pkg/service/manifest/storage/storage.go | 2 +- 12 files changed, 220 insertions(+), 124 deletions(-) create mode 100644 integration/testdata/issuance-template-input.json diff --git a/integration/common.go b/integration/common.go index 2922e83d9..a409fbeac 100644 --- a/integration/common.go +++ b/integration/common.go @@ -426,3 +426,23 @@ func ReviewApplication(params reviewApplicationParams) (string, error) { return output, nil } + +type issuanceTemplateParams struct { + SchemaID string + ManifestID string + IssuerID string +} + +func CreateIssuanceTemplate(params issuanceTemplateParams) (string, error) { + logrus.Println("\n\nCreating Issuance Template:") + issuanceTemplateJSON, err := resolveTemplate(params, "issuance-template-input.json") + if err != nil { + return "", err + } + output, err := put(endpoint+version+"issuancetemplates", issuanceTemplateJSON) + if err != nil { + return "", errors.Wrapf(err, "creating issuance template yielded output: %s", output) + } + + return output, nil +} diff --git a/integration/steelthread_integration_test.go b/integration/steelthread_integration_test.go index b765b3d00..6b5105fc0 100644 --- a/integration/steelthread_integration_test.go +++ b/integration/steelthread_integration_test.go @@ -119,6 +119,101 @@ func TestCreateCredentialManifestIntegration(t *testing.T) { assert.NotEmpty(t, manifestID) } +func TestCreateIssuanceTemplateIntegration(t *testing.T) { + if testing.Short() { + t.Skip("skipping integration test") + } + + issuerDID, err := GetValue(steelThreadContext, "issuerDID") + assert.NoError(t, err) + assert.NotEmpty(t, issuerDID) + + schemaID, err := GetValue(steelThreadContext, "schemaID") + assert.NoError(t, err) + assert.NotEmpty(t, schemaID) + + cmOutput, err := CreateCredentialManifest(credManifestParams{ + IssuerID: issuerDID.(string), + SchemaID: schemaID.(string), + }) + assert.NoError(t, err) + + manifestID, err := getJSONElement(cmOutput, "$.credential_manifest.id") + SetValue(steelThreadContext, "manifestWithIssuanceTemplateID", manifestID) + assert.NoError(t, err) + assert.NotEmpty(t, manifestID) + + presentationDefinitionID, err := getJSONElement(cmOutput, "$.credential_manifest.presentation_definition.id") + SetValue(steelThreadContext, "presentationDefinitionWithIssuanceTemplateID", presentationDefinitionID) + assert.NoError(t, err) + assert.NotEmpty(t, presentationDefinitionID) + + itOutput, err := CreateIssuanceTemplate(issuanceTemplateParams{ + SchemaID: schemaID.(string), + ManifestID: manifestID, + IssuerID: issuerDID.(string), + }) + assert.NoError(t, err) + + issuanceTemplateID, err := getJSONElement(itOutput, "$.id") + assert.NoError(t, err) + assert.NotEmpty(t, issuanceTemplateID) + SetValue(steelThreadContext, "issuanceTemplateID", issuanceTemplateID) +} + +func TestSubmitApplicationWithIssuanceTemplateIntegration(t *testing.T) { + if testing.Short() { + t.Skip("skipping integration test") + } + + credentialJWT, err := GetValue(steelThreadContext, "credentialJWT") + assert.NoError(t, err) + assert.NotEmpty(t, credentialJWT) + + presentationDefinitionID, err := GetValue(steelThreadContext, "presentationDefinitionWithIssuanceTemplateID") + assert.NoError(t, err) + assert.NotEmpty(t, presentationDefinitionID) + + manifestID, err := GetValue(steelThreadContext, "manifestWithIssuanceTemplateID") + assert.NoError(t, err) + assert.NotEmpty(t, manifestID) + + aliceDID, err := GetValue(steelThreadContext, "aliceDID") + assert.NoError(t, err) + assert.NotEmpty(t, aliceDID) + + aliceDIDPrivateKey, err := GetValue(steelThreadContext, "aliceDIDPrivateKey") + assert.NoError(t, err) + assert.NotEmpty(t, aliceDIDPrivateKey) + + credAppJWT, err := CreateCredentialApplicationJWT(credApplicationParams{ + DefinitionID: presentationDefinitionID.(string), + ManifestID: manifestID.(string), + }, credentialJWT.(string), aliceDID.(string), aliceDIDPrivateKey.(string)) + assert.NoError(t, err) + assert.NotEmpty(t, credAppJWT) + + submitApplicationOutput, err := SubmitApplication(applicationParams{ + ApplicationJWT: credAppJWT, + }) + assert.NoError(t, err) + assert.NotEmpty(t, submitApplicationOutput) + + isDone, err := getJSONElement(submitApplicationOutput, "$.done") + assert.NoError(t, err) + assert.Equal(t, "true", isDone) + + credentialResponseID, err := getJSONElement(submitApplicationOutput, "$.result.response.credential_response.id") + assert.NoError(t, err) + + opCredentialResponse, err := getJSONElement(submitApplicationOutput, "$.result.response") + assert.NoError(t, err) + + responsesOutput, err := get(endpoint + version + "manifests/responses/" + credentialResponseID) + assert.NoError(t, err) + + assert.JSONEq(t, responsesOutput, opCredentialResponse) +} func TestSubmitAndReviewApplicationIntegration(t *testing.T) { if testing.Short() { t.Skip("skipping integration test") diff --git a/integration/testdata/issuance-template-input.json b/integration/testdata/issuance-template-input.json new file mode 100644 index 000000000..209b0b655 --- /dev/null +++ b/integration/testdata/issuance-template-input.json @@ -0,0 +1,22 @@ +{ + "credentialManifest": "{{.ManifestID}}", + "issuer": "{{.IssuerID}}", + "credentials": [ + { + "id": "kyc_credential", + "schema": "{{.SchemaID}}", + "credentialInputDescriptor": "kyc1", + "data": { + "givenName": "$.credentialSubject.givenName", + "familyName": "$.credentialSubject.familyName", + "postalAddress": { + "addressRegion": "CA" + } + }, + "expiry": { + "time": "2022-10-31T00:00:00Z" + }, + "revocable": true + } + ] +} diff --git a/pkg/server/middleware/panics.go b/pkg/server/middleware/panics.go index ad625e016..198f902a8 100644 --- a/pkg/server/middleware/panics.go +++ b/pkg/server/middleware/panics.go @@ -25,9 +25,10 @@ func Panics() framework.Middleware { defer func() { if r := recover(); r != nil { // log the stack trace for this panic'd goroutine - err = errors.Errorf("%s: \n%s", v.TraceID, debug.Stack()) + stack := debug.Stack() + err = errors.Errorf("%s: \n%s", v.TraceID, stack) - logrus.Infof("%s: PANIC :\n%s", v.TraceID, debug.Stack()) + logrus.Infof("%s: PANIC : %s : \n%s", r, v.TraceID, stack) } }() diff --git a/pkg/server/router/manifest.go b/pkg/server/router/manifest.go index 7674191c3..be9b7765b 100644 --- a/pkg/server/router/manifest.go +++ b/pkg/server/router/manifest.go @@ -420,8 +420,10 @@ func (mr ManifestRouter) DeleteApplication(ctx context.Context, w http.ResponseW } type GetResponseResponse struct { - ID string `json:"id"` - Response manifestsdk.CredentialResponse `json:"response"` + Response manifestsdk.CredentialResponse `json:"credential_response"` + // this is an interface type to union Data Integrity and JWT style VCs + Credentials any `json:"verifiableCredentials,omitempty"` + ResponseJWT keyaccess.JWT `json:"responseJwt,omitempty"` } // GetResponse godoc @@ -450,8 +452,9 @@ func (mr ManifestRouter) GetResponse(ctx context.Context, w http.ResponseWriter, } resp := GetResponseResponse{ - ID: gotResponse.Response.ID, - Response: gotResponse.Response, + Response: gotResponse.Response, + Credentials: gotResponse.Credentials, + ResponseJWT: gotResponse.ResponseJWT, } return framework.Respond(ctx, w, resp, http.StatusOK) } diff --git a/pkg/server/server_issuance_test.go b/pkg/server/server_issuance_test.go index edc058683..19f5a5e0e 100644 --- a/pkg/server/server_issuance_test.go +++ b/pkg/server/server_issuance_test.go @@ -42,13 +42,9 @@ func TestIssuanceRouter(t *testing.T) { { ID: "output_descriptor_1", Schema: createdSchema.Schema.ID, - Data: issuing.CredentialTemplateData{ - Claims: issuing.ClaimTemplates{ - Data: map[string]any{ - "foo": "bar", - "hello": "$.vcsomething.something", - }, - }, + Data: issuing.ClaimTemplates{ + "foo": "bar", + "hello": "$.vcsomething.something", }, Expiry: issuing.TimeLike{ Time: &now, @@ -68,13 +64,9 @@ func TestIssuanceRouter(t *testing.T) { { ID: "output_descriptor_1", Schema: "", - Data: issuing.CredentialTemplateData{ - Claims: issuing.ClaimTemplates{ - Data: map[string]any{ - "foo": "bar", - "hello": "$.vcsomething.something", - }, - }, + Data: issuing.ClaimTemplates{ + "foo": "bar", + "hello": "$.vcsomething.something", }, Expiry: issuing.TimeLike{ Time: &now, @@ -118,13 +110,9 @@ func TestIssuanceRouter(t *testing.T) { { ID: "", Schema: createdSchema.Schema.ID, - Data: issuing.CredentialTemplateData{ - Claims: issuing.ClaimTemplates{ - Data: map[string]any{ - "foo": "bar", - "hello": "$.vcsomething.something", - }, - }, + Data: issuing.ClaimTemplates{ + "foo": "bar", + "hello": "$.vcsomething.something", }, Expiry: issuing.TimeLike{ Time: &now, @@ -145,13 +133,9 @@ func TestIssuanceRouter(t *testing.T) { { ID: "output_descriptor_1", Schema: createdSchema.Schema.ID, - Data: issuing.CredentialTemplateData{ - Claims: issuing.ClaimTemplates{ - Data: map[string]any{ - "foo": "bar", - "hello": "$.vcsomething.something", - }, - }, + Data: issuing.ClaimTemplates{ + "foo": "bar", + "hello": "$.vcsomething.something", }, Expiry: issuing.TimeLike{ Time: &now, @@ -173,13 +157,9 @@ func TestIssuanceRouter(t *testing.T) { { ID: "output_descriptor_1", Schema: "fake schema", - Data: issuing.CredentialTemplateData{ - Claims: issuing.ClaimTemplates{ - Data: map[string]any{ - "foo": "bar", - "hello": "$.vcsomething.something", - }, - }, + Data: issuing.ClaimTemplates{ + "foo": "bar", + "hello": "$.vcsomething.something", }, Expiry: issuing.TimeLike{ Time: &now, @@ -200,13 +180,9 @@ func TestIssuanceRouter(t *testing.T) { { ID: "output_descriptor_1", Schema: createdSchema.ID, - Data: issuing.CredentialTemplateData{ - Claims: issuing.ClaimTemplates{ - Data: map[string]any{ - "foo": "bar", - "hello": "$.vcsomething.something", - }, - }, + Data: issuing.ClaimTemplates{ + "foo": "bar", + "hello": "$.vcsomething.something", }, Expiry: issuing.TimeLike{ Time: &now, @@ -226,13 +202,9 @@ func TestIssuanceRouter(t *testing.T) { { ID: "output_descriptor_1", Schema: createdSchema.ID, - Data: issuing.CredentialTemplateData{ - Claims: issuing.ClaimTemplates{ - Data: map[string]any{ - "foo": "bar", - "hello": "$.vcsomething.something", - }, - }, + Data: issuing.ClaimTemplates{ + "foo": "bar", + "hello": "$.vcsomething.something", }, Expiry: issuing.TimeLike{ Time: &now, @@ -269,13 +241,9 @@ func TestIssuanceRouter(t *testing.T) { { ID: "output_descriptor_1", Schema: createdSchema.Schema.ID, - Data: issuing.CredentialTemplateData{ - Claims: issuing.ClaimTemplates{ - Data: map[string]any{ - "foo": "bar", - "hello": "$.vcsomething.something", - }, - }, + Data: issuing.ClaimTemplates{ + "foo": "bar", + "hello": "$.vcsomething.something", }, Expiry: issuing.TimeLike{ Time: &now, @@ -391,13 +359,9 @@ func createSimpleTemplate(t *testing.T, manifest *model.CreateManifestResponse, { ID: "output_descriptor_1", Schema: createdSchema.Schema.ID, - Data: issuing.CredentialTemplateData{ - Claims: issuing.ClaimTemplates{ - Data: map[string]any{ - "foo": "bar", - "hello": "$.vcsomething.something", - }, - }, + Data: issuing.ClaimTemplates{ + "foo": "bar", + "hello": "$.vcsomething.something", }, Expiry: issuing.TimeLike{ Time: &now, diff --git a/pkg/server/server_manifest_test.go b/pkg/server/server_manifest_test.go index 99abc6be3..5b59c3ee0 100644 --- a/pkg/server/server_manifest_test.go +++ b/pkg/server/server_manifest_test.go @@ -1052,36 +1052,28 @@ func getValidIssuanceTemplateRequest(m manifest.CredentialManifest, issuerDID *d Issuer: issuerDID.DID.ID, Credentials: []issuing.CredentialTemplate{ { - ID: "id1", - Schema: createdSchema.ID, - Data: issuing.CredentialTemplateData{ - CredentialInputDescriptor: "test-id", - Claims: issuing.ClaimTemplates{ - Data: map[string]any{ - "firstName": "$.credentialSubject.firstName", - "lastName": "$.credentialSubject.lastName", - "state": "CA", - }, - }, + ID: "id1", + Schema: createdSchema.ID, + CredentialInputDescriptor: "test-id", + Data: issuing.ClaimTemplates{ + "firstName": "$.credentialSubject.firstName", + "lastName": "$.credentialSubject.lastName", + "state": "CA", }, Expiry: issuing.TimeLike{ Time: &now, }, }, { - ID: "id2", - Schema: createdSchema.ID, - Data: issuing.CredentialTemplateData{ - CredentialInputDescriptor: "test-id", - Claims: issuing.ClaimTemplates{ - Data: map[string]any{ - "someCrazyObject": map[string]any{ - "foo": 123, - "bar": false, - "baz": []any{ - "yay", 123, nil, - }, - }, + ID: "id2", + Schema: createdSchema.ID, + CredentialInputDescriptor: "test-id", + Data: issuing.ClaimTemplates{ + "someCrazyObject": map[string]any{ + "foo": 123, + "bar": false, + "baz": []any{ + "yay", 123, nil, }, }, }, diff --git a/pkg/service/issuing/model.go b/pkg/service/issuing/model.go index 80ae6cff5..22b69d50c 100644 --- a/pkg/service/issuing/model.go +++ b/pkg/service/issuing/model.go @@ -11,32 +11,15 @@ type GetIssuanceTemplateRequest struct { ID string `json:"id" validate:"required"` } -type CredentialTemplateData struct { - // Optional. - // When present, it's the ID of the input descriptor in the application. Corresponds to one of the - // PresentationDefinition.InputDescriptors[].ID in the credential manifest. When creating a credential, the base - // data will be used from the provided submission that matches this ID. - // When absent, there will be no base data for the credentials created. Additionally, no JSON path strings in - // Claims.Data will be resolved. - CredentialInputDescriptor string `json:"credentialInputDescriptor"` - - // The set of information that will be used to create claims. - Claims ClaimTemplates -} - type TimeLike struct { // For fixed time in the future. - Time *time.Time + Time *time.Time `json:"time,omitempty"` // For a fixed offset from when it was issued. - Duration *time.Duration + Duration *time.Duration `json:"duration,omitempty"` } -type ClaimTemplates struct { - // Values may be json path like strings, or any other JSON primitive. Each entry will be used to come up with a - // claim about the credentialSubject in the credential that will be issued. - Data map[string]any -} +type ClaimTemplates map[string]any type CredentialTemplate struct { // ID corresponding to an OutputDescriptor.ID from the manifest. @@ -45,11 +28,21 @@ type CredentialTemplate struct { // ID of the CredentialSchema to be used for the issued credential. Schema string `json:"schema"` + // Optional. + // When present, it's the ID of the input descriptor in the application. Corresponds to one of the + // PresentationDefinition.InputDescriptors[].ID in the credential manifest. When creating a credential, the base + // data will be populated from the provided submission that matches this ID. + // When absent, there will be no base data for the credentials created. Additionally, no JSON path strings in + // ClaimTemplates.Data will be resolved. + CredentialInputDescriptor string `json:"credentialInputDescriptor"` + // Data that will be used to determine credential claims. - Data CredentialTemplateData `json:"data"` + // Values may be json path like strings, or any other JSON primitive. Each entry will be used to come up with a + // claim about the credentialSubject in the credential that will be issued. + Data ClaimTemplates `json:"data,omitempty"` // Parameter to determine the expiry of the credential. - Expiry TimeLike `json:"expiry"` + Expiry TimeLike `json:"expiry,omitempty"` // Whether the credentials created should be revocable. Revocable bool `json:"revocable"` diff --git a/pkg/service/manifest/model/model.go b/pkg/service/manifest/model/model.go index bf50625c2..ebdb5c543 100644 --- a/pkg/service/manifest/model/model.go +++ b/pkg/service/manifest/model/model.go @@ -102,7 +102,9 @@ type GetResponseRequest struct { } type GetResponseResponse struct { - Response manifestsdk.CredentialResponse `json:"response"` + Response manifestsdk.CredentialResponse `json:"response"` + Credentials any + ResponseJWT keyaccess.JWT } type GetResponsesResponse struct { diff --git a/pkg/service/manifest/response.go b/pkg/service/manifest/response.go index 7aa522db7..b5561bacc 100644 --- a/pkg/service/manifest/response.go +++ b/pkg/service/manifest/response.go @@ -166,7 +166,7 @@ func (s Service) applyIssuanceTemplate( if err != nil { return err } - for k, v := range ct.Data.Claims.Data { + for k, v := range ct.Data { claimValue := v if vs, ok := v.(string); ok { if strings.HasPrefix(vs, "$") { @@ -197,7 +197,7 @@ func getCredential( credManifest manifest.CredentialManifest, submission *exchange.PresentationSubmission, ) (any, error) { - if ct.Data.CredentialInputDescriptor == "" { + if ct.CredentialInputDescriptor == "" { return nil, nil } @@ -207,7 +207,7 @@ func getCredential( // Lookup the claim that's sent in the submission. for _, descriptor := range submission.DescriptorMap { - if descriptor.ID == ct.Data.CredentialInputDescriptor { + if descriptor.ID == ct.CredentialInputDescriptor { c, err := jsonpath.JsonPathLookup(applicationJSON, descriptor.Path) if err != nil { return nil, errors.Wrapf( @@ -222,7 +222,7 @@ func getCredential( } return nil, errors.Errorf( "could not find credential for input_descriptor=\"%s\"", - ct.Data.CredentialInputDescriptor, + ct.CredentialInputDescriptor, ) } diff --git a/pkg/service/manifest/service.go b/pkg/service/manifest/service.go index addd6a9fd..786247cc0 100644 --- a/pkg/service/manifest/service.go +++ b/pkg/service/manifest/service.go @@ -401,7 +401,7 @@ func (s Service) maybeIssueAutomatically( return nil, errors.Wrap(err, "signing credential response") } - storeResponseRequest := manifeststg.StoredResponse{ + storedResponse := manifeststg.StoredResponse{ ID: credResp.ID, ManifestID: manifestID, ApplicantDID: applicantDID, @@ -415,7 +415,7 @@ func (s Service) maybeIssueAutomatically( true, "automatic from issuing template", opcredential.IDFromResponseID(applicationID), - storeResponseRequest, + storedResponse, ) if err != nil { return nil, errors.Wrap(err, "reviewing application") @@ -552,7 +552,11 @@ func (s Service) GetResponse(ctx context.Context, request model.GetResponseReque return nil, util.LoggingErrorMsgf(err, "could not get response: %s", request.ID) } - response := model.GetResponseResponse{Response: gotResponse.Response} + response := model.GetResponseResponse{ + Response: gotResponse.Response, + Credentials: credint.ContainersToInterface(gotResponse.Credentials), + ResponseJWT: gotResponse.ResponseJWT, + } return &response, nil } diff --git a/pkg/service/manifest/storage/storage.go b/pkg/service/manifest/storage/storage.go index 34bbde43f..9ddd4b2d1 100644 --- a/pkg/service/manifest/storage/storage.go +++ b/pkg/service/manifest/storage/storage.go @@ -202,7 +202,7 @@ func (ms *Storage) GetResponse(ctx context.Context, id string) (*StoredResponse, return nil, util.LoggingErrorMsgf(err, "could not get response: %s", id) } if len(responseBytes) == 0 { - return nil, util.LoggingErrorMsgf(err, "response not found with id: %s", id) + return nil, util.LoggingNewErrorf("response not found with id: %s", id) } var stored StoredResponse if err = json.Unmarshal(responseBytes, &stored); err != nil {