Repository: TBD54566975/ssi-service
Pull request #218 (2022-12-14 19:40:06)

[OSE-179] Integration tests for presentation exchange flow
# Overview
This follows a similar pattern as all other integration tests. 

# Description
Noteworthy:
* the id of operations was changed from `/presentations/submissions/<uuid>` to `presentations/submissions/<uuid>` (no leading `/`). This was in order to make it easier to pluck out the `id` of the presentation from the URL path.
* Changed the cancel endpoint from `v1/operations/presentations/submissions/{id}/cancel` to `v1/operations/cancel/presentations/submissions/{id}`. The reason was because I found no reasonable way to encode the first one using `httptreemux`. Any suggestions are welcome. 
* Note that using `*id` acts as a wildcard when registering handlers. 



Patch:
diff --git a/doc/swagger.yaml b/doc/swagger.yaml
index 3833160..add0567 100644
--- a/doc/swagger.yaml
+++ b/doc/swagger.yaml
@@ -799,7 +799,7 @@ definitions:
         description: Whether this operation has finished.
         type: boolean
       id:
-        description: The name of the resource related to this operation. E.g. "/presentations/submissions/<uuid>"
+        description: The name of the resource related to this operation. E.g. "presentations/submissions/<uuid>"
         type: string
       result:
         $ref: '#/definitions/github.com_tbd54566975_ssi-service_pkg_server_router.OperationResult'
@@ -1393,7 +1393,7 @@ definitions:
         description: Whether this operation has finished.
         type: boolean
       id:
-        description: The name of the resource related to this operation. E.g. "/presentations/submissions/<uuid>"
+        description: The name of the resource related to this operation. E.g. "presentations/submissions/<uuid>"
         type: string
       result:
         $ref: '#/definitions/pkg_server_router.OperationResult'
@@ -2049,7 +2049,7 @@ paths:
         name: request
         required: true
         schema:
-          $ref: '#/definitions/github.com_tbd54566975_ssi-service_pkg_server_router.StoreKeyRequest'
+          $ref: '#/definitions/pkg_server_router.StoreKeyRequest'
       produces:
       - application/json
       responses:
@@ -2083,7 +2083,7 @@ paths:
         "200":
           description: OK
           schema:
-            $ref: '#/definitions/github.com_tbd54566975_ssi-service_pkg_server_router.GetKeyDetailsResponse'
+            $ref: '#/definitions/pkg_server_router.GetKeyDetailsResponse'
         "400":
           description: Bad request
           schema:
@@ -2398,7 +2398,37 @@ paths:
     get:
       consumes:
       - application/json
-      description: Cancels an ongoing operation, if possible.
+      description: List operations according to the request
+      parameters:
+      - description: request body
+        in: body
+        name: request
+        required: true
+        schema:
+          $ref: '#/definitions/github.com_tbd54566975_ssi-service_pkg_server_router.GetOperationsRequest'
+      produces:
+      - application/json
+      responses:
+        "200":
+          description: OK
+          schema:
+            $ref: '#/definitions/github.com_tbd54566975_ssi-service_pkg_server_router.GetOperationsResponse'
+        "400":
+          description: Bad request
+          schema:
+            type: string
+        "500":
+          description: Internal server error
+          schema:
+            type: string
+      summary: List operations
+      tags:
+      - OperationAPI
+  /v1/operations/{id}:
+    get:
+      consumes:
+      - application/json
+      description: Get operation by its ID
       parameters:
       - description: ID
         in: path
@@ -2411,7 +2441,7 @@ paths:
         "200":
           description: OK
           schema:
-            $ref: '#/definitions/pkg_server_router.Operation'
+            $ref: '#/definitions/github.com_tbd54566975_ssi-service_pkg_server_router.Operation'
         "400":
           description: Bad request
           schema:
@@ -2420,14 +2450,14 @@ paths:
           description: Internal server error
           schema:
             type: string
-      summary: Cancel an ongoing operation
+      summary: Get an operation
       tags:
       - OperationAPI
-  /v1/operations/{id}:
+  /v1/operations/cancel/{id}:
     get:
       consumes:
       - application/json
-      description: Get operation by its ID
+      description: Cancels an ongoing operation, if possible.
       parameters:
       - description: ID
         in: path
@@ -2440,7 +2470,7 @@ paths:
         "200":
           description: OK
           schema:
-            $ref: '#/definitions/pkg_server_router.Operation'
+            $ref: '#/definitions/github.com_tbd54566975_ssi-service_pkg_server_router.Operation'
         "400":
           description: Bad request
           schema:
@@ -2449,7 +2479,7 @@ paths:
           description: Internal server error
           schema:
             type: string
-      summary: Get an operation
+      summary: Cancel an ongoing operation
       tags:
       - OperationAPI
   /v1/presentation/definition:
@@ -2463,14 +2493,14 @@ paths:
         name: request
         required: true
         schema:
-          $ref: '#/definitions/pkg_server_router.CreatePresentationDefinitionRequest'
+          $ref: '#/definitions/github.com_tbd54566975_ssi-service_pkg_server_router.CreatePresentationDefinitionRequest'
       produces:
       - application/json
       responses:
         "201":
           description: Created
           schema:
-            $ref: '#/definitions/pkg_server_router.CreatePresentationDefinitionResponse'
+            $ref: '#/definitions/github.com_tbd54566975_ssi-service_pkg_server_router.CreatePresentationDefinitionResponse'
         "400":
           description: Bad request
           schema:
@@ -2527,7 +2557,7 @@ paths:
         "200":
           description: OK
           schema:
-            $ref: '#/definitions/pkg_server_router.GetPresentationDefinitionResponse'
+            $ref: '#/definitions/github.com_tbd54566975_ssi-service_pkg_server_router.GetPresentationDefinitionResponse'
         "400":
           description: Bad request
           schema:
@@ -2547,14 +2577,14 @@ paths:
         name: request
         required: true
         schema:
-          $ref: '#/definitions/pkg_server_router.ListSubmissionRequest'
+          $ref: '#/definitions/github.com_tbd54566975_ssi-service_pkg_server_router.ListSubmissionRequest'
       produces:
       - application/json
       responses:
         "200":
           description: OK
           schema:
-            $ref: '#/definitions/pkg_server_router.ListSubmissionResponse'
+            $ref: '#/definitions/github.com_tbd54566975_ssi-service_pkg_server_router.ListSubmissionResponse'
         "400":
           description: Bad request
           schema:
@@ -2576,14 +2606,14 @@ paths:
         name: request
         required: true
         schema:
-          $ref: '#/definitions/pkg_server_router.CreateSubmissionRequest'
+          $ref: '#/definitions/github.com_tbd54566975_ssi-service_pkg_server_router.CreateSubmissionRequest'
       produces:
       - application/json
       responses:
         "201":
           description: The type of response is Submission once the operation has finished.
           schema:
-            $ref: '#/definitions/pkg_server_router.Operation'
+            $ref: '#/definitions/github.com_tbd54566975_ssi-service_pkg_server_router.Operation'
         "400":
           description: Bad request
           schema:
@@ -2612,7 +2642,7 @@ paths:
         "200":
           description: OK
           schema:
-            $ref: '#/definitions/pkg_server_router.GetSubmissionResponse'
+            $ref: '#/definitions/github.com_tbd54566975_ssi-service_pkg_server_router.GetSubmissionResponse'
         "400":
           description: Bad request
           schema:
@@ -2633,14 +2663,14 @@ paths:
         name: request
         required: true
         schema:
-          $ref: '#/definitions/pkg_server_router.ReviewSubmissionRequest'
+          $ref: '#/definitions/github.com_tbd54566975_ssi-service_pkg_server_router.ReviewSubmissionRequest'
       produces:
       - application/json
       responses:
         "200":
           description: OK
           schema:
-            $ref: '#/definitions/pkg_server_router.ReviewSubmissionResponse'
+            $ref: '#/definitions/github.com_tbd54566975_ssi-service_pkg_server_router.ReviewSubmissionResponse'
         "400":
           description: Bad request
           schema:
@@ -2663,7 +2693,7 @@ paths:
         "200":
           description: OK
           schema:
-            $ref: '#/definitions/pkg_server_router.GetSchemasResponse'
+            $ref: '#/definitions/github.com_tbd54566975_ssi-service_pkg_server_router.GetSchemasResponse'
         "500":
           description: Internal server error
           schema:
@@ -2681,14 +2711,14 @@ paths:
         name: request
         required: true
         schema:
-          $ref: '#/definitions/pkg_server_router.CreateSchemaRequest'
+          $ref: '#/definitions/github.com_tbd54566975_ssi-service_pkg_server_router.CreateSchemaRequest'
       produces:
       - application/json
       responses:
         "201":
           description: Created
           schema:
-            $ref: '#/definitions/pkg_server_router.CreateSchemaResponse'
+            $ref: '#/definitions/github.com_tbd54566975_ssi-service_pkg_server_router.CreateSchemaResponse'
         "400":
           description: Bad request
           schema:
@@ -2745,7 +2775,7 @@ paths:
         "200":
           description: OK
           schema:
-            $ref: '#/definitions/pkg_server_router.GetSchemaResponse'
+            $ref: '#/definitions/github.com_tbd54566975_ssi-service_pkg_server_router.GetSchemaResponse'
         "400":
           description: Bad request
           schema:
@@ -2764,14 +2794,14 @@ paths:
         name: request
         required: true
         schema:
-          $ref: '#/definitions/pkg_server_router.VerifySchemaRequest'
+          $ref: '#/definitions/github.com_tbd54566975_ssi-service_pkg_server_router.VerifySchemaRequest'
       produces:
       - application/json
       responses:
         "200":
           description: OK
           schema:
-            $ref: '#/definitions/pkg_server_router.VerifySchemaResponse'
+            $ref: '#/definitions/github.com_tbd54566975_ssi-service_pkg_server_router.VerifySchemaResponse'
         "400":
           description: Bad request
           schema:
diff --git a/integration/common.go b/integration/common.go
index 86705af..05c85d2 100644
--- a/integration/common.go
+++ b/integration/common.go
@@ -11,6 +11,7 @@ import (
 	manifestsdk "github.com/TBD54566975/ssi-sdk/credential/manifest"
 	"github.com/TBD54566975/ssi-sdk/crypto"
 	"github.com/goccy/go-json"
+	"github.com/google/uuid"
 	"github.com/mr-tron/base58"
 	"github.com/oliveagle/jsonpath"
 	"github.com/pkg/errors"
@@ -91,6 +92,22 @@ func CreateVerifiableCredential(issuerDID, schemaID string, revocable bool) (str
 	return output, nil
 }
 
+func CreateSubmissionCredential(issuerDID, subjectDID string) (string, error) {
+	logrus.Println("\n\nCreate a submission credential")
+
+	credentialJSON := getJSONFromFile("submission-credential-input.json")
+
+	credentialJSON = strings.ReplaceAll(credentialJSON, "<CREDISSUERID>", issuerDID)
+	credentialJSON = strings.ReplaceAll(credentialJSON, "<CREDSUBJECTID>", subjectDID)
+
+	output, err := put(endpoint+version+"credentials", credentialJSON)
+	if err != nil {
+		return "", errors.Wrapf(err, "credentials endpoint with output: %s", output)
+	}
+
+	return output, nil
+}
+
 func CreateCredentialManifest(issuerDID, schemaID string) (string, error) {
 	logrus.Println("\n\nCreate our Credential Manifest:")
 	manifestJSON := getJSONFromFile("manifest-input.json")
@@ -136,6 +153,74 @@ func CreateCredentialApplicationJWT(presentationDefinitionID, credentialJWT, man
 	return signed.String(), nil
 }
 
+func CreatePresentationDefinition() (string, error) {
+	logrus.Println("\n\nCreate our Presentation Definition:")
+	definitionJSON := getJSONFromFile("presentation-definition-input.json")
+	output, err := put(endpoint+version+"presentations/definitions", definitionJSON)
+	if err != nil {
+		return "", errors.Wrapf(err, "presentation definition endpoint with output: %s", output)
+	}
+
+	return output, nil
+}
+
+func ReviewSubmission(id string) (string, error) {
+	logrus.Println("\n\nCreate our review submission request:")
+	reviewJSON := getJSONFromFile("review-submission-input.json")
+	output, err := put(endpoint+version+"presentations/submissions/"+id+"/review", reviewJSON)
+	if err != nil {
+		return "", errors.Wrapf(err, "review submission endpoint with output: %s", output)
+	}
+
+	return output, nil
+}
+
+func CreateSubmission(definitionID, holderDID, holderPrivateKey, credentialJWT string) (string, error) {
+	logrus.Println("\n\nCreate our Submission:")
+	submissionJSON := getJSONFromFile("presentation-submission-input.json")
+
+	submissionJSON = strings.ReplaceAll(submissionJSON, "<DEFINITIONID>", definitionID)
+	submissionJSON = strings.ReplaceAll(submissionJSON, "<SUBMISSIONID>", uuid.NewString())
+	submissionJSON = strings.ReplaceAll(submissionJSON, "<HOLDERID>", holderDID)
+	submissionJSON = strings.ReplaceAll(submissionJSON, "<CREDENTIALJWT>", credentialJWT)
+
+	pkBytes, err := base58.Decode(holderPrivateKey)
+	if err != nil {
+		return "", errors.Wrap(err, "base58 decoding")
+	}
+
+	pkCrypto, err := crypto.BytesToPrivKey(pkBytes, crypto.Ed25519)
+	if err != nil {
+		return "", errors.Wrap(err, "bytes to priv key")
+	}
+
+	signer, err := keyaccess.NewJWKKeyAccess(holderDID, pkCrypto)
+	if err != nil {
+		return "", errors.Wrap(err, "creating signer")
+	}
+
+	var submission any
+	if err := json.Unmarshal([]byte(submissionJSON), &submission); err != nil {
+		return "", err
+	}
+
+	signed, err := signer.SignJSON(submission)
+	if err != nil {
+		logrus.Println("Failed signing: " + submissionJSON)
+		return "", errors.Wrap(err, "signing json")
+	}
+
+	submissionJSONWrapper := getJSONFromFile("presentation-submission-input-jwt.json")
+	submissionJSONWrapper = strings.ReplaceAll(submissionJSONWrapper, "<APPLICATIONJWT>", signed.String())
+
+	output, err := put(endpoint+version+"presentations/submissions", submissionJSONWrapper)
+	if err != nil {
+		return "", errors.Wrapf(err, "presentation submission endpoint with output: %s", output)
+	}
+
+	return output, nil
+}
+
 func SubmitApplication(credAppJWT string) (string, error) {
 
 	trueApplicationJSON := getJSONFromFile("application-input-jwt.json")
diff --git a/magefile.go b/magefile.go
index 5f9d5f7..d65c4fd 100644
--- a/magefile.go
+++ b/magefile.go
@@ -167,7 +167,6 @@ func runIntegrationTests(extraTestArgs ...string) error {
 		args = append(args, "-v")
 	}
 	args = append(args, "-tags=jwx_es256k")
-	args = append(args, "-race")
 	args = append(args, extraTestArgs...)
 	args = append(args, "./integration")
 	testEnv := map[string]string{
diff --git a/pkg/server/router/operation.go b/pkg/server/router/operation.go
index 596d0b7..52de757 100644
--- a/pkg/server/router/operation.go
+++ b/pkg/server/router/operation.go
@@ -172,7 +172,7 @@ func routerModel(op operation.Operation) Operation {
 // @Success      200  {object}  Operation  "OK"
 // @Failure      400  {string}  string  "Bad request"
 // @Failure      500  {string}  string  "Internal server error"
-// @Router       /v1/operations [get]
+// @Router       /v1/operations/cancel/{id} [get]
 func (o OperationRouter) CancelOperation(ctx context.Context, w http.ResponseWriter, r *http.Request) error {
 	id := framework.GetParam(ctx, IDParam)
 	if id == nil {
diff --git a/pkg/server/router/presentation.go b/pkg/server/router/presentation.go
index 89eb989..35a6bcc 100644
--- a/pkg/server/router/presentation.go
+++ b/pkg/server/router/presentation.go
@@ -221,7 +221,7 @@ func (r CreateSubmissionRequest) toServiceRequest() (*model.CreateSubmissionRequ
 }
 
 type Operation struct {
-	// The name of the resource related to this operation. E.g. "/presentations/submissions/<uuid>"
+	// The name of the resource related to this operation. E.g. "presentations/submissions/<uuid>"
 	ID string `json:"id"`
 
 	// Whether this operation has finished.
diff --git a/pkg/server/server.go b/pkg/server/server.go
index ad655d9..8708efb 100644
--- a/pkg/server/server.go
+++ b/pkg/server/server.go
@@ -212,9 +212,12 @@ func (s *SSIServer) OperationAPI(service svcframework.Service) (err error) {
 
 	handlerPath := V1Prefix + OperationPrefix
 
-	s.Handle(http.MethodGet, path.Join(handlerPath, "/:id"), operationRouter.GetOperation)
 	s.Handle(http.MethodGet, handlerPath, operationRouter.GetOperations)
-	s.Handle(http.MethodPut, path.Join(handlerPath, "/:id/cancel"), operationRouter.CancelOperation)
+	// See https://github.com/dimfeld/httptreemux#routing-rules for details on how the `*` works.
+	// In this case, it's used so that the operation id matches `presentations/submissions/{submission_id}` for the URL
+	// path	`/v1/operations/cancel/presentations/submissions/{id}`
+	s.Handle(http.MethodPut, path.Join(handlerPath, "/cancel/*id"), operationRouter.CancelOperation)
+	s.Handle(http.MethodGet, path.Join(handlerPath, "/*id"), operationRouter.GetOperation)
 
 	return
 }
diff --git a/pkg/service/operation/storage/namespace/namespace.go b/pkg/service/operation/storage/namespace/namespace.go
index 6198e93..55a2f2c 100644
--- a/pkg/service/operation/storage/namespace/namespace.go
+++ b/pkg/service/operation/storage/namespace/namespace.go
@@ -18,7 +18,7 @@ func FromID(id string) string {
 	return FromParent(id[:i])
 }
 
-// FromParent returns a namespace from a given parent resource name like "/presentations/submissions". Empty is returned
+// FromParent returns a namespace from a given parent resource name like "presentations/submissions". Empty is returned
 // when the parent resource cannot be resolved.
 func FromParent(parent string) string {
 	switch parent {
diff --git a/pkg/service/operation/submission/submission.go b/pkg/service/operation/submission/submission.go
index 94adf35..e938b7e 100644
--- a/pkg/service/operation/submission/submission.go
+++ b/pkg/service/operation/submission/submission.go
@@ -29,7 +29,7 @@ const (
 	// Namespace is the namespace to be used for storing submissions.
 	Namespace = "presentation_submission"
 	// ParentResource is the prefix of the submission parent resource.
-	ParentResource = "/presentations/submissions"
+	ParentResource = "presentations/submissions"
 )
 
 // Status indicates the current state of a submission.
diff --git a/sip/sips/sip6/README.md b/sip/sips/sip6/README.md
index 2215525..a846007 100644
--- a/sip/sips/sip6/README.md
+++ b/sip/sips/sip6/README.md
@@ -246,7 +246,7 @@ Upon a submission, the following steps will be performed:
 9. Check that the paths are resolvable to fields in the claims. This should follow the spec as described in [https://identity.foundation/presentation-exchange/#processing-of-submission-entries](https://identity.foundation/presentation-exchange/#processing-of-submission-entries)
 10. A `Submission` is stored in the DB.
 11. An operation ID is generated, and an operation is stored in a KV database.
-12. A response of type `Operation` is sent back to the client with `id := "/presentations/submissions/{submission_id}"`
+12. A response of type `Operation` is sent back to the client with `id := "presentations/submissions/{submission_id}"`
 13. Bask in the glory of a successful submission.
 
 ### GET
@@ -280,7 +280,7 @@ The URL is `/v1/presentations/submissions/:id/review`. This endpoint enables adm
 }
 ```
 
-Ideally, only the `TBD Admin` should have authorization to perform this. After this method is called, the operation with `id==/presentations/submissions/{submission_id}` will be updated with the result of this invocation (and the `done` field will be set to true). The `submission` object’s review state will be updated as well.
+Ideally, only the `TBD Admin` should have authorization to perform this. After this method is called, the operation with `id==presentations/submissions/{submission_id}` will be updated with the result of this invocation (and the `done` field will be set to true). The `submission` object’s review state will be updated as well.
 
 The response of this endpoint will contain a `Submission` object with `status != 'pending'`.
 
@@ -292,7 +292,7 @@ An object of type `Operation` and will look as follows
 
 ```json
 {
-  "id": "/{namespace}/{unique_id}", // example: "/presentations/submissions/a30e3b91-fb77-4d22-95fa-871689c322e2"
+  "id": "/{namespace}/{unique_id}", // example: "presentations/submissions/a30e3b91-fb77-4d22-95fa-871689c322e2"
   "done": true, // when "false", then "result" will be empty as it's still being calculated.
   "result": { // only present when "done" == true. When present only one of ["error", "response"] will be populated.
     "error": "some string with the error",
@@ -315,7 +315,7 @@ The request will look as follows
 
 ```json
 {
-  "parent": "/presentations/submissions/", // represents the name of the parent's resource
+  "parent": "presentations/submissions/", // represents the name of the parent's resource
   "filter": "done:false", // a filter expression, for any filtering needs
 }
 ```


Test patch:
diff --git a/integration/presentation_exchange_integration_test.go b/integration/presentation_exchange_integration_test.go
new file mode 100644
index 0000000..ca1e38f
--- /dev/null
+++ b/integration/presentation_exchange_integration_test.go
@@ -0,0 +1,117 @@
+package integration
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/tbd54566975/ssi-service/pkg/service/operation/submission"
+)
+
+var presentationExchangeContext = NewTestContext("PresentationExchange")
+
+func TestCreateParticipants(t *testing.T) {
+	if testing.Short() {
+		t.Skip("skipping integration test")
+	}
+
+	didKeyOutput, err := CreateDIDKey()
+	assert.NoError(t, err)
+
+	issuerDID, err := getJSONElement(didKeyOutput, "$.did.id")
+	assert.NoError(t, err)
+	assert.Contains(t, issuerDID, "did:key")
+
+	holderOutput, err := CreateDIDKey()
+	assert.NoError(t, err)
+
+	holderDID, err := getJSONElement(holderOutput, "$.did.id")
+	assert.NoError(t, err)
+	assert.Contains(t, holderDID, "did:key")
+
+	holderPrivateKey, err := getJSONElement(holderOutput, "$.privateKeyBase58")
+	assert.NoError(t, err)
+
+	SetValue(presentationExchangeContext, "issuerDID", issuerDID)
+	SetValue(presentationExchangeContext, "holderDID", holderDID)
+	SetValue(presentationExchangeContext, "holderPrivateKey", holderPrivateKey)
+}
+
+func TestCreatePresentationDefinition(t *testing.T) {
+	if testing.Short() {
+		t.Skip("skipping integration test")
+	}
+
+	definition, err := CreatePresentationDefinition()
+	assert.NoError(t, err)
+
+	definitionID, err := getJSONElement(definition, "$.presentation_definition.id")
+	assert.NoError(t, err)
+	SetValue(presentationExchangeContext, "definitionID", definitionID)
+}
+
+func TestSubmissionFlow(t *testing.T) {
+	if testing.Short() {
+		t.Skip("skipping integration test")
+	}
+
+	definitionID, err := GetValue(presentationExchangeContext, "definitionID")
+	assert.NoError(t, err)
+
+	holderDID, err := GetValue(presentationExchangeContext, "holderDID")
+	assert.NoError(t, err)
+
+	holderPrivateKey, err := GetValue(presentationExchangeContext, "holderPrivateKey")
+	assert.NoError(t, err)
+
+	issuerDID, err := GetValue(presentationExchangeContext, "issuerDID")
+	assert.NoError(t, err)
+
+	credOutput, err := CreateSubmissionCredential(issuerDID.(string), holderDID.(string))
+	assert.NoError(t, err)
+
+	credentialJWT, err := getJSONElement(credOutput, "$.credentialJwt")
+	assert.NoError(t, err)
+
+	toBeCancelledOp, err := CreateSubmission(definitionID.(string), holderDID.(string), holderPrivateKey.(string), credentialJWT)
+	assert.NoError(t, err)
+
+	cancelOpID, err := getJSONElement(toBeCancelledOp, "$.id")
+	assert.NoError(t, err)
+	cancelOutput, err := put(endpoint+version+"operations/cancel/"+cancelOpID, "{}")
+	assert.NoError(t, err)
+	cancelDone, err := getJSONElement(cancelOutput, "$.done")
+	assert.NoError(t, err)
+	assert.Equal(t, "true", cancelDone)
+
+	submissionOpOutput, err := CreateSubmission(definitionID.(string), holderDID.(string), holderPrivateKey.(string), credentialJWT)
+	assert.NoError(t, err)
+
+	opID, err := getJSONElement(submissionOpOutput, "$.id")
+	assert.NoError(t, err)
+
+	operationOutput, err := get(endpoint + version + "operations/" + opID)
+	assert.NoError(t, err)
+	done, err := getJSONElement(operationOutput, "$.done")
+	assert.NoError(t, err)
+	assert.Equal(t, "false", done)
+
+	reviewOutput, err := ReviewSubmission(submission.ID(opID))
+	assert.NoError(t, err)
+	status, err := getJSONElement(reviewOutput, "$.status")
+	assert.NoError(t, err)
+	assert.Equal(t, "approved", status)
+
+	reason, err := getJSONElement(reviewOutput, "$.reason")
+	assert.NoError(t, err)
+	assert.Equal(t, "because I want to", reason)
+
+	operationOutput, err = get(endpoint + version + "operations/" + opID)
+	assert.NoError(t, err)
+	done, err = getJSONElement(operationOutput, "$.done")
+	assert.NoError(t, err)
+	assert.Equal(t, "true", done)
+	opResponse, err := getJSONElement(operationOutput, "$.result.response")
+	assert.NoError(t, err)
+	s, _ := getJSONElement(reviewOutput, "$")
+	assert.Equal(t, s, opResponse)
+}
diff --git a/integration/testdata/presentation-definition-input.json b/integration/testdata/presentation-definition-input.json
new file mode 100644
index 0000000..51585e4
--- /dev/null
+++ b/integration/testdata/presentation-definition-input.json
@@ -0,0 +1,24 @@
+{
+  "name": "name",
+  "purpose": "purpose",
+  "inputDescriptors": [
+    {
+      "id": "wa_driver_license",
+      "name": "washington state business license",
+      "purpose": "some testing stuff",
+      "constraints": {
+        "fields": [
+          {
+            "id": "date_of_birth",
+            "path": [
+              "$.credentialSubject.dateOfBirth",
+              "$.credentialSubject.dob",
+              "$.vc.credentialSubject.dateOfBirth",
+              "$.vc.credentialSubject.dob"
+            ]
+          }
+        ]
+      }
+    }
+  ]
+}
\ No newline at end of file
diff --git a/integration/testdata/presentation-submission-input-jwt.json b/integration/testdata/presentation-submission-input-jwt.json
new file mode 100644
index 0000000..f728a72
--- /dev/null
+++ b/integration/testdata/presentation-submission-input-jwt.json
@@ -0,0 +1,3 @@
+{
+  "submissionJwt":"<APPLICATIONJWT>"
+}
\ No newline at end of file
diff --git a/integration/testdata/presentation-submission-input.json b/integration/testdata/presentation-submission-input.json
new file mode 100644
index 0000000..1583eaa
--- /dev/null
+++ b/integration/testdata/presentation-submission-input.json
@@ -0,0 +1,25 @@
+{
+  "vp": {
+    "@context": [
+      "https://www.w3.org/2018/credentials/v1"
+    ],
+    "holder": "<HOLDERID>",
+    "type": [
+      "VerifiablePresentation"
+    ],
+    "presentation_submission": {
+      "id": "<SUBMISSIONID>",
+      "definition_id": "<DEFINITIONID>",
+      "descriptor_map": [
+        {
+          "id": "wa_driver_license",
+          "format": "jwt_vp",
+          "path": "$.verifiableCredential[0]"
+        }
+      ]
+    },
+    "verifiableCredential": [
+      "<CREDENTIALJWT>"
+    ]
+  }
+}
\ No newline at end of file
diff --git a/integration/testdata/review-submission-input.json b/integration/testdata/review-submission-input.json
new file mode 100644
index 0000000..4d1974e
--- /dev/null
+++ b/integration/testdata/review-submission-input.json
@@ -0,0 +1,4 @@
+{
+  "approved": true,
+  "reason": "because I want to"
+}
\ No newline at end of file
diff --git a/integration/testdata/submission-credential-input.json b/integration/testdata/submission-credential-input.json
new file mode 100644
index 0000000..14efde7
--- /dev/null
+++ b/integration/testdata/submission-credential-input.json
@@ -0,0 +1,11 @@
+{
+  "data": {
+    "additionalName": "Mclovin",
+    "dateOfBirth": "1987-01-02",
+    "familyName": "Andres",
+    "givenName": "Uribe"
+  },
+  "issuer": "<CREDISSUERID>",
+  "subject": "<CREDSUBJECTID>",
+  "expiry": "2051-10-05T14:48:00.000Z"
+}
\ No newline at end of file
diff --git a/pkg/server/server_operation_test.go b/pkg/server/server_operation_test.go
index d48c2aa..2f91eca 100644
--- a/pkg/server/server_operation_test.go
+++ b/pkg/server/server_operation_test.go
@@ -74,7 +74,7 @@ func TestOperationsAPI(t *testing.T) {
 			var resp router.Operation
 			assert.NoError(t, json.NewDecoder(w.Body).Decode(&resp))
 			assert.False(t, resp.Done)
-			assert.Contains(t, resp.ID, "/presentations/submissions/")
+			assert.Contains(t, resp.ID, "presentations/submissions/")
 		})
 
 		t.Run("Returns error when id doesn't exist", func(tt *testing.T) {
@@ -100,7 +100,7 @@ func TestOperationsAPI(t *testing.T) {
 			opRouter := setupOperationsRouter(t, s)
 
 			request := router.GetOperationsRequest{
-				Parent: "/presentations/submissions",
+				Parent: "presentations/submissions",
 			}
 			value := newRequestValue(t, request)
 			req := httptest.NewRequest(http.MethodGet, "https://ssi-service.com/v1/operations", value)
@@ -126,7 +126,7 @@ func TestOperationsAPI(t *testing.T) {
 			submissionOp2 := createSubmission(t, pRouter, def.PresentationDefinition.ID, VerifiableCredential(), holderDID2, holderSigner2)
 
 			request := router.GetOperationsRequest{
-				Parent: "/presentations/submissions",
+				Parent: "presentations/submissions",
 			}
 			value := newRequestValue(t, request)
 			req := httptest.NewRequest(http.MethodGet, "https://ssi-service.com/v1/operations", value)
@@ -158,7 +158,7 @@ func TestOperationsAPI(t *testing.T) {
 			_ = createSubmission(t, pRouter, def.PresentationDefinition.ID, VerifiableCredential(), holderDID, holderSigner)
 
 			request := router.GetOperationsRequest{
-				Parent: "/presentations/submissions",
+				Parent: "presentations/submissions",
 				Filter: "done = false",
 			}
 			value := newRequestValue(t, request)
@@ -183,7 +183,7 @@ func TestOperationsAPI(t *testing.T) {
 			_ = createSubmission(t, pRouter, def.PresentationDefinition.ID, VerifiableCredential(), holderDID, holderSigner)
 
 			request := router.GetOperationsRequest{
-				Parent: "/presentations/submissions",
+				Parent: "presentations/submissions",
 				Filter: "done = true",
 			}
 			value := newRequestValue(t, request)
@@ -278,11 +278,11 @@ func setupTestDB(t *testing.T) storage.ServiceStorage {
 	file, err := os.CreateTemp("", "bolt")
 	require.NoError(t, err)
 	name := file.Name()
-	file.Close()
 	s, err := storage.NewStorage(storage.Bolt, name)
 	require.NoError(t, err)
 	t.Cleanup(func() {
 		_ = s.Close()
+		_ = file.Close()
 		_ = os.Remove(name)
 	})
 	return s
diff --git a/pkg/server/server_presentation_test.go b/pkg/server/server_presentation_test.go
index aabda15..3da290f 100644
--- a/pkg/server/server_presentation_test.go
+++ b/pkg/server/server_presentation_test.go
@@ -513,7 +513,7 @@ func VerifiableCredential(options ...VCOption) credential.VerifiableCredential {
 	vc := credential.VerifiableCredential{
 		Context:          []string{credential.VerifiableCredentialsLinkedDataContext},
 		ID:               uuid.NewString(),
-		Type:             []string{credential.VerifiablePresentationType},
+		Type:             []string{credential.VerifiableCredentialType},
 		Issuer:           "did:key:z4oJ8bFEFv7E3omhuK5LrAtL29Nmd8heBey9HtJCSvodSb7nrfaMrd6zb7fjYSRxrfSgBSDeM6Bs59KRKFgXSDWJcfcjs",
 		IssuanceDate:     "2022-11-07T21:28:57Z",
 		ExpirationDate:   "2051-10-05T14:48:00.000Z",