Repository: TBD54566975/ssi-service
Pull request #255 (2023-01-13 04:33:28)

Added e2e test for automatic issuance and bug fixes.
# Overview
Added the e2e for automatic issuance, which involves creating an IssuanceTemplate.

# Description
Some additional changes in this PR
* Logging the reason why a panic happened.
* Fixed a panic where the `err` was being returned though it was actually `nil`.
* Made the `GetResponse` return the same shape as what is in the `operation.result` of calling `SubmitApplication`.
* Slightly tweaked the issuance template shape so that `data` contains only the claims about credentialSubject.

# How Has This Been Tested?

The e2e test was added.
All other tests still pass.

## References
Final pieces of SIP 5. 



Patch:
diff --git a/integration/common.go b/integration/common.go
index 2922e83..a409fbe 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/pkg/server/middleware/panics.go b/pkg/server/middleware/panics.go
index ad625e0..198f902 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 9dfcda6..8f7dc6d 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/service/issuing/model.go b/pkg/service/issuing/model.go
index 80ae6cf..22b69d5 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 e20da95..ce43f3b 100644
--- a/pkg/service/manifest/model/model.go
+++ b/pkg/service/manifest/model/model.go
@@ -106,7 +106,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 b842180..35b3600 100644
--- a/pkg/service/manifest/response.go
+++ b/pkg/service/manifest/response.go
@@ -182,7 +182,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, "$") {
@@ -213,7 +213,7 @@ func getCredential(
 	credManifest manifest.CredentialManifest,
 	submission *exchange.PresentationSubmission,
 ) (any, error) {
-	if ct.Data.CredentialInputDescriptor == "" {
+	if ct.CredentialInputDescriptor == "" {
 		return nil, nil
 	}
 
@@ -223,7 +223,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(
@@ -238,7 +238,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 5184661..321597b 100644
--- a/pkg/service/manifest/service.go
+++ b/pkg/service/manifest/service.go
@@ -391,7 +391,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,
@@ -405,7 +405,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")
@@ -543,7 +543,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 34bbde4..9ddd4b2 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 {


Test patch:
diff --git a/integration/steelthread_integration_test.go b/integration/steelthread_integration_test.go
index 0f9d48d..fe4326d 100644
--- a/integration/steelthread_integration_test.go
+++ b/integration/steelthread_integration_test.go
@@ -120,6 +120,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 0000000..209b0b6
--- /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/server_issuance_test.go b/pkg/server/server_issuance_test.go
index edc0586..19f5a5e 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 7d738da..9c72f38 100644
--- a/pkg/server/server_manifest_test.go
+++ b/pkg/server/server_manifest_test.go
@@ -1083,36 +1083,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,
 							},
 						},
 					},