Repository: TBD54566975/ssi-service
Pull request #187 (2022-11-30 15:13:22)

[OSE-171][OSE-172] Implement Get and List submissions
This PR implements the list and get endpoints according to SIP6. Please note that there was a big refactoring of the tests file in order to:
* Provide more clarity into what's being created before exercising the behavior under test. 
* Cleaning up after every test, so the DB is in a clean state.


codecov-commenter: # [Codecov](https://codecov.io/gh/TBD54566975/ssi-service/pull/187?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=TBD54566975) Report
> Merging [#187](https://codecov.io/gh/TBD54566975/ssi-service/pull/187?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=TBD54566975) (9787bb7) into [main](https://codecov.io/gh/TBD54566975/ssi-service/commit/ac7dde30567dfcd6ef2fb2afe4d2f27ab6d78d4f?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=TBD54566975) (ac7dde3) will **decrease** coverage by `0.36%`.
> The diff coverage is `20.68%`.

```diff
@@            Coverage Diff             @@
##             main     #187      +/-   ##
==========================================
- Coverage   24.98%   24.61%   -0.37%     
==========================================
  Files          21       23       +2     
  Lines        1781     1832      +51     
==========================================
+ Hits          445      451       +6     
- Misses       1264     1309      +45     
  Partials       72       72              
```


| [Impacted Files](https://codecov.io/gh/TBD54566975/ssi-service/pull/187?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=TBD54566975) | Coverage Δ | |
|---|---|---|
| [pkg/server/router/presentation.go](https://codecov.io/gh/TBD54566975/ssi-service/pull/187/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=TBD54566975#diff-cGtnL3NlcnZlci9yb3V0ZXIvcHJlc2VudGF0aW9uLmdv) | `4.79% <0.00%> (-0.69%)` | :arrow_down: |
| [pkg/service/operation/model.go](https://codecov.io/gh/TBD54566975/ssi-service/pull/187/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=TBD54566975#diff-cGtnL3NlcnZpY2Uvb3BlcmF0aW9uL21vZGVsLmdv) | `100.00% <100.00%> (ø)` | |
| [pkg/service/operation/service.go](https://codecov.io/gh/TBD54566975/ssi-service/pull/187/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=TBD54566975#diff-cGtnL3NlcnZpY2Uvb3BlcmF0aW9uL3NlcnZpY2UuZ28=) | `0.00% <0.00%> (ø)` | |

Help us with your feedback. Take ten seconds to tell us [how you rate us](https://about.codecov.io/nps?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=TBD54566975). Have a feature suggestion? [Share it here.](https://app.codecov.io/gh/feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=TBD54566975)


Patch:
diff --git a/doc/swagger.yaml b/doc/swagger.yaml
index 205f14c..16ca0c6 100644
--- a/doc/swagger.yaml
+++ b/doc/swagger.yaml
@@ -753,7 +753,7 @@ definitions:
   github.com_tbd54566975_ssi-service_pkg_server_router.GetSubmissionResponse:
     properties:
       submission:
-        $ref: '#/definitions/exchange.PresentationSubmission'
+        $ref: '#/definitions/presentation.Submission'
     type: object
   github.com_tbd54566975_ssi-service_pkg_server_router.ListSubmissionRequest:
     properties:
@@ -764,7 +764,7 @@ definitions:
     properties:
       submissions:
         items:
-          $ref: '#/definitions/exchange.PresentationSubmission'
+          $ref: '#/definitions/presentation.Submission'
         type: array
     type: object
   github.com_tbd54566975_ssi-service_pkg_server_router.Operation:
@@ -1277,7 +1277,7 @@ definitions:
   pkg_server_router.GetSubmissionResponse:
     properties:
       submission:
-        $ref: '#/definitions/exchange.PresentationSubmission'
+        $ref: '#/definitions/presentation.Submission'
     type: object
   pkg_server_router.ListSubmissionRequest:
     properties:
@@ -1288,7 +1288,7 @@ definitions:
     properties:
       submissions:
         items:
-          $ref: '#/definitions/exchange.PresentationSubmission'
+          $ref: '#/definitions/presentation.Submission'
         type: array
     type: object
   pkg_server_router.Operation:
@@ -1405,6 +1405,25 @@ definitions:
       verified:
         type: boolean
     type: object
+  presentation.Submission:
+    properties:
+      definition_id:
+        type: string
+      descriptor_map:
+        items:
+          $ref: '#/definitions/exchange.SubmissionDescriptor'
+        type: array
+      id:
+        type: string
+      status:
+        description: One of {`pending`, `done`}.
+        type: string
+    required:
+    - definition_id
+    - descriptor_map
+    - id
+    - status
+    type: object
   rendering.ColorResource:
     properties:
       color:
@@ -1931,7 +1950,7 @@ paths:
         name: request
         required: true
         schema:
-          $ref: '#/definitions/pkg_server_router.StoreKeyRequest'
+          $ref: '#/definitions/github.com_tbd54566975_ssi-service_pkg_server_router.StoreKeyRequest'
       produces:
       - application/json
       responses:
@@ -1965,7 +1984,7 @@ paths:
         "200":
           description: OK
           schema:
-            $ref: '#/definitions/pkg_server_router.GetKeyDetailsResponse'
+            $ref: '#/definitions/github.com_tbd54566975_ssi-service_pkg_server_router.GetKeyDetailsResponse'
         "400":
           description: Bad request
           schema:
@@ -1998,7 +2017,7 @@ paths:
         "200":
           description: OK
           schema:
-            $ref: '#/definitions/pkg_server_router.GetManifestsResponse'
+            $ref: '#/definitions/github.com_tbd54566975_ssi-service_pkg_server_router.GetManifestsResponse'
         "400":
           description: Bad request
           schema:
@@ -2020,14 +2039,14 @@ paths:
         name: request
         required: true
         schema:
-          $ref: '#/definitions/pkg_server_router.CreateManifestRequest'
+          $ref: '#/definitions/github.com_tbd54566975_ssi-service_pkg_server_router.CreateManifestRequest'
       produces:
       - application/json
       responses:
         "201":
           description: Created
           schema:
-            $ref: '#/definitions/pkg_server_router.CreateManifestResponse'
+            $ref: '#/definitions/github.com_tbd54566975_ssi-service_pkg_server_router.CreateManifestResponse'
         "400":
           description: Bad request
           schema:
@@ -2084,7 +2103,7 @@ paths:
         "200":
           description: OK
           schema:
-            $ref: '#/definitions/pkg_server_router.GetManifestResponse'
+            $ref: '#/definitions/github.com_tbd54566975_ssi-service_pkg_server_router.GetManifestResponse'
         "400":
           description: Bad request
           schema:
@@ -2104,7 +2123,7 @@ paths:
         "200":
           description: OK
           schema:
-            $ref: '#/definitions/pkg_server_router.GetApplicationsResponse'
+            $ref: '#/definitions/github.com_tbd54566975_ssi-service_pkg_server_router.GetApplicationsResponse'
         "400":
           description: Bad request
           schema:
@@ -2127,14 +2146,14 @@ paths:
         name: request
         required: true
         schema:
-          $ref: '#/definitions/pkg_server_router.SubmitApplicationRequest'
+          $ref: '#/definitions/github.com_tbd54566975_ssi-service_pkg_server_router.SubmitApplicationRequest'
       produces:
       - application/json
       responses:
         "201":
           description: Created
           schema:
-            $ref: '#/definitions/pkg_server_router.SubmitApplicationResponse'
+            $ref: '#/definitions/github.com_tbd54566975_ssi-service_pkg_server_router.SubmitApplicationResponse'
         "400":
           description: Bad request
           schema:
@@ -2191,7 +2210,7 @@ paths:
         "200":
           description: OK
           schema:
-            $ref: '#/definitions/pkg_server_router.GetApplicationResponse'
+            $ref: '#/definitions/github.com_tbd54566975_ssi-service_pkg_server_router.GetApplicationResponse'
         "400":
           description: Bad request
           schema:
@@ -2211,7 +2230,7 @@ paths:
         "200":
           description: OK
           schema:
-            $ref: '#/definitions/pkg_server_router.GetResponsesResponse'
+            $ref: '#/definitions/github.com_tbd54566975_ssi-service_pkg_server_router.GetResponsesResponse'
         "400":
           description: Bad request
           schema:
@@ -2268,7 +2287,7 @@ paths:
         "200":
           description: OK
           schema:
-            $ref: '#/definitions/pkg_server_router.GetResponseResponse'
+            $ref: '#/definitions/github.com_tbd54566975_ssi-service_pkg_server_router.GetResponseResponse'
         "400":
           description: Bad request
           schema:
diff --git a/pkg/server/router/presentation.go b/pkg/server/router/presentation.go
index 0111c5a..4070bb7 100644
--- a/pkg/server/router/presentation.go
+++ b/pkg/server/router/presentation.go
@@ -265,7 +265,7 @@ func (pr PresentationRouter) CreateSubmission(ctx context.Context, w http.Respon
 }
 
 type GetSubmissionResponse struct {
-	Submission exchange.PresentationSubmission `json:"submission"`
+	*presentation.Submission
 }
 
 // GetSubmission godoc
@@ -279,7 +279,21 @@ type GetSubmissionResponse struct {
 // @Failure      400  {string}  string  "Bad request"
 // @Router       /v1/presentations/submission/{id} [get]
 func (pr PresentationRouter) GetSubmission(ctx context.Context, w http.ResponseWriter, r *http.Request) error {
-	var resp GetSubmissionResponse
+	id := framework.GetParam(ctx, IDParam)
+	if id == nil {
+		return framework.NewRequestError(
+			util.LoggingNewError("get submission request requires id"), http.StatusBadRequest)
+	}
+
+	submission, err := pr.service.GetSubmission(presentation.GetSubmissionRequest{ID: *id})
+
+	if err != nil {
+		return framework.NewRequestError(
+			util.LoggingErrorMsg(err, "failed getting submission"), http.StatusBadRequest)
+	}
+	resp := GetSubmissionResponse{
+		Submission: &submission.Submission,
+	}
 	return framework.Respond(ctx, w, resp, http.StatusOK)
 }
 
@@ -288,7 +302,7 @@ type ListSubmissionRequest struct {
 }
 
 type ListSubmissionResponse struct {
-	Submissions []exchange.PresentationSubmission `json:"submissions"`
+	Submissions []presentation.Submission `json:"submissions"`
 }
 
 // ListSubmissions godoc
@@ -303,7 +317,12 @@ type ListSubmissionResponse struct {
 // @Failure      500  {string}  string  "Internal server error"
 // @Router       /v1/presentations/submissions [get]
 func (pr PresentationRouter) ListSubmissions(ctx context.Context, w http.ResponseWriter, r *http.Request) error {
-	return framework.Respond(ctx, w, ListSubmissionResponse{}, http.StatusOK)
+	resp, err := pr.service.ListSubmissions(presentation.ListSubmissionRequest{})
+	if err != nil {
+		return framework.NewRequestError(
+			util.LoggingNewError("failed listing submissions"), http.StatusInternalServerError)
+	}
+	return framework.Respond(ctx, w, ListSubmissionResponse{Submissions: resp.Submissions}, http.StatusOK)
 }
 
 type ReviewSubmissionRequest struct {
diff --git a/pkg/service/operation/model.go b/pkg/service/operation/model.go
index 4dc1f35..7d538a0 100644
--- a/pkg/service/operation/model.go
+++ b/pkg/service/operation/model.go
@@ -1,5 +1,7 @@
 package operation
 
+import "strings"
+
 type Result struct {
 	Error    string      `json:"error,omitempty"`
 	Response interface{} `json:"response,omitempty"`
@@ -10,3 +12,13 @@ type Operation struct {
 	Done   bool   `json:"done"`
 	Result Result `json:"result,omitempty"`
 }
+
+// SubmissionID attempts to parse the submission id from the ID of the operation. This is done by taking the last word
+// that results from splitting the id by "/". On failures, the empty string is returned.
+func SubmissionID(opID string) string {
+	i := strings.LastIndex(opID, "/")
+	if i == -1 {
+		return ""
+	}
+	return opID[(i + 1):]
+}
diff --git a/pkg/service/presentation/model.go b/pkg/service/presentation/model.go
index 9d540bc..42e71a0 100644
--- a/pkg/service/presentation/model.go
+++ b/pkg/service/presentation/model.go
@@ -53,9 +53,22 @@ type GetSubmissionRequest struct {
 }
 
 type GetSubmissionResponse struct {
-	Submission exchange.PresentationSubmission `json:"submission"`
+	Submission Submission `json:"submission"`
 }
 
 type DeleteSubmissionRequest struct {
 	ID string `json:"id" validate:"required"`
 }
+
+type ListSubmissionRequest struct {
+}
+
+type Submission struct {
+	// One of {`pending`, `done`}.
+	Status string `json:"status" validate:"required"`
+	*exchange.PresentationSubmission
+}
+
+type ListSubmissionResponse struct {
+	Submissions []Submission `json:"submissions"`
+}
diff --git a/pkg/service/presentation/service.go b/pkg/service/presentation/service.go
index e2fd623..29ae843 100644
--- a/pkg/service/presentation/service.go
+++ b/pkg/service/presentation/service.go
@@ -174,7 +174,10 @@ func (s Service) CreateSubmission(request CreateSubmissionRequest) (*operation.O
 		return nil, errors.Wrap(err, "verifying presentation submission vp")
 	}
 
-	storedSubmission := presentationstorage.StoredSubmission{Submission: request.Submission}
+	storedSubmission := presentationstorage.StoredSubmission{
+		Status:     presentationstorage.StatusPending,
+		Submission: request.Submission,
+	}
 
 	// TODO(andres): IO requests should be done in parallel, once we have context wired up.
 	if err := s.storage.StoreSubmission(storedSubmission); err != nil {
@@ -201,10 +204,32 @@ func (s Service) GetSubmission(request GetSubmissionRequest) (*GetSubmissionResp
 
 	storedSubmission, err := s.storage.GetSubmission(request.ID)
 	if err != nil {
-		return nil, util.LoggingNewErrorf("error getting presentation submission: %s", request.ID)
+		return nil, errors.Wrap(err, "fetching from storage")
 	}
-	if storedSubmission == nil {
-		return nil, util.LoggingNewErrorf("presentation submission with id<%s> could not be found", request.ID)
+	return &GetSubmissionResponse{
+		Submission: Submission{
+			Status:                 storedSubmission.Status.String(),
+			PresentationSubmission: &storedSubmission.Submission,
+		},
+	}, nil
+}
+
+func (s Service) ListSubmissions(request ListSubmissionRequest) (*ListSubmissionResponse, error) {
+	logrus.Debug("listing presentation submissions")
+
+	subs, err := s.storage.ListSubmissions()
+	if err != nil {
+		return nil, errors.Wrap(err, "fetching submissions from storage")
+	}
+
+	resp := &ListSubmissionResponse{Submissions: make([]Submission, 0, len(subs))}
+	for _, sub := range subs {
+		sub := sub // What's this?? see https://github.com/golang/go/wiki/CommonMistakes#using-reference-to-loop-iterator-variable
+		resp.Submissions = append(resp.Submissions, Submission{
+			Status:                 sub.Status.String(),
+			PresentationSubmission: &sub.Submission,
+		})
 	}
-	return &GetSubmissionResponse{Submission: storedSubmission.Submission}, nil
+
+	return resp, nil
 }
diff --git a/pkg/service/presentation/storage/bolt.go b/pkg/service/presentation/storage/bolt.go
index 2c6b7c7..516672a 100644
--- a/pkg/service/presentation/storage/bolt.go
+++ b/pkg/service/presentation/storage/bolt.go
@@ -20,6 +20,23 @@ type BoltPresentationStorage struct {
 	db *storage.BoltDB
 }
 
+func (b BoltPresentationStorage) ListSubmissions() ([]StoredSubmission, error) {
+	allData, err := b.db.ReadAll(submissionNamespace)
+	if err != nil {
+		return nil, errors.Wrap(err, "reading all data")
+	}
+
+	storedSubmissions := make([]StoredSubmission, len(allData))
+	i := 0
+	for key, data := range allData {
+		if err := json.Unmarshal(data, &storedSubmissions[i]); err != nil {
+			logrus.WithError(err).WithField("key", key).Errorf("unmarshalling submission")
+		}
+		i++
+	}
+	return storedSubmissions, nil
+}
+
 func NewBoltPresentationStorage(db *storage.BoltDB) (*BoltPresentationStorage, error) {
 	if db == nil {
 		return nil, errors.New("bolt db reference is nil")
@@ -85,7 +102,7 @@ func (b BoltPresentationStorage) GetSubmission(id string) (*StoredSubmission, er
 		return nil, util.LoggingNewErrorf("could not get submission definition: %s", id)
 	}
 	if len(jsonBytes) == 0 {
-		return nil, util.LoggingErrorMsgf(ErrSubmissionNotFound, "submission not found with id: %s", id)
+		return nil, util.LoggingErrorMsgf(ErrSubmissionNotFound, "reading submission with id: %s", id)
 	}
 	var stored StoredSubmission
 	if err := json.Unmarshal(jsonBytes, &stored); err != nil {
diff --git a/pkg/service/presentation/storage/storage.go b/pkg/service/presentation/storage/storage.go
index fb49eef..8526de6 100644
--- a/pkg/service/presentation/storage/storage.go
+++ b/pkg/service/presentation/storage/storage.go
@@ -18,6 +18,7 @@ type Storage interface {
 }
 
 type DefinitionStorage interface {
+	// TODO: rename to Definition
 	StorePresentation(schema StoredPresentation) error
 	GetPresentation(id string) (*StoredPresentation, error)
 	DeletePresentation(id string) error
@@ -41,13 +42,34 @@ func NewPresentationStorage(s storage.ServiceStorage) (Storage, error) {
 	}
 }
 
+type Status uint8
+
+func (s Status) String() string {
+	switch s {
+	case StatusDone:
+		return "done"
+	case StatusPending:
+		return "pending"
+	default:
+		return "unknown"
+	}
+}
+
+const (
+	StatusUnknown Status = iota
+	StatusPending
+	StatusDone
+)
+
 type StoredSubmission struct {
+	Status     Status                          `json:"status"`
 	Submission exchange.PresentationSubmission `json:"submission"`
 }
 
 type SubmissionStorage interface {
 	StoreSubmission(schema StoredSubmission) error
 	GetSubmission(id string) (*StoredSubmission, error)
+	ListSubmissions() ([]StoredSubmission, error)
 }
 
 var ErrSubmissionNotFound = errors.New("submission not found")


Test patch:
diff --git a/pkg/server/server_presentation_test.go b/pkg/server/server_presentation_test.go
index 442dc1e..e016b12 100644
--- a/pkg/server/server_presentation_test.go
+++ b/pkg/server/server_presentation_test.go
@@ -6,14 +6,17 @@ import (
 	"github.com/TBD54566975/ssi-sdk/credential/exchange"
 	"github.com/TBD54566975/ssi-sdk/credential/signing"
 	"github.com/TBD54566975/ssi-sdk/crypto"
+	"github.com/TBD54566975/ssi-sdk/did"
 	"github.com/goccy/go-json"
 	"github.com/google/go-cmp/cmp"
 	"github.com/google/go-cmp/cmp/cmpopts"
+	"github.com/google/uuid"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 	"github.com/tbd54566975/ssi-service/config"
 	"github.com/tbd54566975/ssi-service/internal/keyaccess"
 	"github.com/tbd54566975/ssi-service/pkg/server/router"
+	"github.com/tbd54566975/ssi-service/pkg/service/operation"
 	"github.com/tbd54566975/ssi-service/pkg/service/presentation"
 	"github.com/tbd54566975/ssi-service/pkg/storage"
 	"net/http"
@@ -41,25 +44,9 @@ func TestPresentationAPI(t *testing.T) {
 	pd, err := builder.Build()
 	assert.NoError(t, err)
 
-	s, err := storage.NewStorage(storage.Bolt)
-	assert.NoError(t, err)
-
-	keyStoreService := testKeyStoreService(t, s)
-	didService := testDIDService(t, s, keyStoreService)
-	schemaService := testSchemaService(t, s, keyStoreService, didService)
-
-	service, err := presentation.NewPresentationService(config.PresentationServiceConfig{}, s, didService.GetResolver(), schemaService)
-	assert.NoError(t, err)
-
-	pRouter, err := router.NewPresentationRouter(service)
-	assert.NoError(t, err)
-
-	t.Cleanup(func() {
-		_ = s.Close()
-		_ = os.Remove(storage.DBFile)
-	})
-
 	t.Run("Create, Get, and Delete PresentationDefinition", func(t *testing.T) {
+		pRouter := setupRouter(t)
+
 		var createdID string
 		{
 			// Create returns the expected PD.
@@ -106,7 +93,6 @@ func TestPresentationAPI(t *testing.T) {
 			req := httptest.NewRequest(http.MethodDelete, fmt.Sprintf("https://ssi-service.com/v1/presentations/definitions/%s", createdID), nil)
 			w := httptest.NewRecorder()
 			assert.NoError(t, pRouter.DeletePresentationDefinition(newRequestContextWithParams(map[string]string{"id": createdID}), w, req))
-
 			w.Flush()
 		}
 		{
@@ -114,12 +100,11 @@ func TestPresentationAPI(t *testing.T) {
 			req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("https://ssi-service.com/v1/presentations/definitions/%s", createdID), nil)
 			w := httptest.NewRecorder()
 			assert.Error(t, pRouter.GetPresentationDefinition(newRequestContextWithParams(map[string]string{"id": createdID}), w, req))
-
-			w.Flush()
 		}
 	})
 
 	t.Run("Create returns error without input descriptors", func(t *testing.T) {
+		pRouter := setupRouter(t)
 		request := router.CreatePresentationDefinitionRequest{}
 		value := newRequestValue(t, request)
 		req := httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/presentations/definitions", value)
@@ -128,94 +113,279 @@ func TestPresentationAPI(t *testing.T) {
 		err = pRouter.CreatePresentationDefinition(newRequestContext(), w, req)
 
 		assert.Error(t, err)
-		w.Flush()
 	})
 
 	t.Run("Get without an ID returns error", func(t *testing.T) {
+		pRouter := setupRouter(t)
 		req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("https://ssi-service.com/v1/presentations/definitions/%s", pd.ID), nil)
 		w := httptest.NewRecorder()
+
 		assert.Error(t, pRouter.GetPresentationDefinition(newRequestContext(), w, req))
-		w.Flush()
 	})
 
 	t.Run("Delete without an ID returns error", func(t *testing.T) {
+		pRouter := setupRouter(t)
+
 		req := httptest.NewRequest(http.MethodDelete, fmt.Sprintf("https://ssi-service.com/v1/presentations/definitions/%s", pd.ID), nil)
 		w := httptest.NewRecorder()
 		assert.Error(t, pRouter.DeletePresentationDefinition(newRequestContext(), w, req))
 		w.Flush()
 	})
 
-	t.Run("well formed submission returns operation", func(t *testing.T) {
-
-		definition := createPresentationDefinition(t, pRouter)
-		vc := credential.VerifiableCredential{
-			Context:          []string{credential.VerifiableCredentialsLinkedDataContext},
-			ID:               "7035a7ec-66c8-4aec-9191-a34e8cf1e82b",
-			Type:             []string{credential.VerifiablePresentationType},
-			Issuer:           "did:key:z4oJ8bFEFv7E3omhuK5LrAtL29Nmd8heBey9HtJCSvodSb7nrfaMrd6zb7fjYSRxrfSgBSDeM6Bs59KRKFgXSDWJcfcjs",
-			IssuanceDate:     "2022-11-07T21:28:57Z",
-			ExpirationDate:   "2051-10-05T14:48:00.000Z",
-			CredentialStatus: nil,
-			CredentialSubject: credential.CredentialSubject{
-				"additionalName": "Mclovin",
-				"dateOfBirth":    "1987-01-02",
-				"familyName":     "Andres",
-				"givenName":      "Uribe",
-				"id":             "did:web:andresuribe.com",
-			},
-			CredentialSchema: nil,
-			RefreshService:   nil,
-			TermsOfUse:       nil,
-			Evidence:         nil,
-			Proof:            nil,
-		}
+	t.Run("Submission endpoints", func(t *testing.T) {
+
+		t.Run("Get non-existing ID returns error", func(t *testing.T) {
+			pRouter := setupRouter(t)
 
-		signer0 := getTestVectorKey0Signer(t)
-		vcData, err := signing.SignVerifiableCredentialJWT(signer0, vc)
-		assert.NoError(t, err)
-		ps := exchange.PresentationSubmission{
-			ID:           "a30e3b91-fb77-4d22-95fa-871689c322e2",
-			DefinitionID: definition.PresentationDefinition.ID,
-			DescriptorMap: []exchange.SubmissionDescriptor{
+			req := httptest.NewRequest(http.MethodGet, "https://ssi-service.com/v1/presentations/submissions/myrandomid", nil)
+			w := httptest.NewRecorder()
+			assert.Error(t, pRouter.GetSubmission(newRequestContext(), w, req))
+		})
+
+		t.Run("Get returns submission after creation", func(t *testing.T) {
+			pRouter := setupRouter(t)
+
+			holderSigner, holderDID := getSigner(t)
+			definition := createPresentationDefinition(t, pRouter)
+			op := createSubmission(t, pRouter, definition.PresentationDefinition.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", operation.SubmissionID(op.ID)), nil)
+			w := httptest.NewRecorder()
+
+			assert.NoError(t, pRouter.GetSubmission(newRequestContextWithParams(map[string]string{"id": operation.SubmissionID(op.ID)}), w, req))
+
+			var resp router.GetSubmissionResponse
+			assert.NoError(t, json.NewDecoder(w.Body).Decode(&resp))
+			assert.Equal(t, operation.SubmissionID(op.ID), resp.Submission.ID)
+			assert.Equal(t, definition.PresentationDefinition.ID, resp.Submission.DefinitionID)
+			assert.Equal(t, "pending", resp.Submission.Status)
+		})
+
+		t.Run("Create well formed submission returns operation", func(t *testing.T) {
+			pRouter := setupRouter(t)
+
+			holderSigner, holderDID := getSigner(t)
+			definition := createPresentationDefinition(t, pRouter)
+			request := createSubmissionRequest(t, definition.PresentationDefinition.ID, VerifiableCredential(
+				WithCredentialSubject(credential.CredentialSubject{
+					"additionalName": "Mclovin",
+					"dateOfBirth":    "1987-01-02",
+					"familyName":     "Andres",
+					"givenName":      "Uribe",
+					"id":             "did:web:andresuribe.com",
+				})), holderSigner, holderDID)
+
+			value := newRequestValue(t, request)
+			req := httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/presentations/submissions", value)
+			w := httptest.NewRecorder()
+
+			err = pRouter.CreateSubmission(newRequestContext(), w, req)
+
+			require.NoError(t, err)
+			var resp router.Operation
+			assert.NoError(t, json.NewDecoder(w.Body).Decode(&resp))
+			assert.Contains(t, resp.ID, "presentations/submissions/")
+			assert.False(t, resp.Done)
+			assert.Zero(t, resp.Result)
+		})
+
+		t.Run("List submissions returns empty when there are none", func(t *testing.T) {
+			pRouter := setupRouter(t)
+
+			request := router.ListSubmissionRequest{}
+
+			value := newRequestValue(t, request)
+			req := httptest.NewRequest(http.MethodGet, "https://ssi-service.com/v1/presentations/submissions", value)
+			w := httptest.NewRecorder()
+
+			err = pRouter.ListSubmissions(newRequestContext(), w, req)
+
+			require.NoError(t, err)
+			var resp router.ListSubmissionResponse
+			assert.NoError(t, json.NewDecoder(w.Body).Decode(&resp))
+			assert.Empty(t, resp.Submissions)
+		})
+
+		t.Run("List submissions returns many submissions", func(t *testing.T) {
+			pRouter := setupRouter(t)
+
+			holderSigner, holderDID := getSigner(t)
+			definition := createPresentationDefinition(t, pRouter)
+			op := createSubmission(t, pRouter, definition.PresentationDefinition.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(t)
+			op2 := createSubmission(t, pRouter, definition.PresentationDefinition.ID, VerifiableCredential(
+				WithCredentialSubject(credential.CredentialSubject{
+					"additionalName": "Mr. T",
+					"dateOfBirth":    "1999-01-02",
+					"familyName":     "Mister",
+					"givenName":      "Tee",
+					"id":             "did:web:mrt.com"})), mrTeeDID, mrTeeSigner)
+
+			request := router.ListSubmissionRequest{}
+
+			value := newRequestValue(t, request)
+			req := httptest.NewRequest(http.MethodGet, "https://ssi-service.com/v1/presentations/submissions", value)
+			w := httptest.NewRecorder()
+
+			err = pRouter.ListSubmissions(newRequestContext(), w, req)
+
+			require.NoError(t, err)
+			var resp router.ListSubmissionResponse
+			assert.NoError(t, json.NewDecoder(w.Body).Decode(&resp))
+			assert.Len(t, resp.Submissions, 2)
+
+			expectedSubmissions := []presentation.Submission{
 				{
-					ID:         "wa_driver_license",
-					Format:     string(exchange.JWTVPTarget),
-					Path:       "$.verifiableCredential[0]",
-					PathNested: nil,
+					Status: "pending",
+					PresentationSubmission: &exchange.PresentationSubmission{
+						ID:           operation.SubmissionID(op.ID),
+						DefinitionID: definition.PresentationDefinition.ID,
+					},
 				},
-			},
-		}
-		vp := credential.VerifiablePresentation{
-			Context:                []string{credential.VerifiableCredentialsLinkedDataContext},
-			ID:                     "a9b575c7-bac2-47e7-a925-c432815ebb4c",
-			Holder:                 "did:key:z4oJ8eRi73fvkrXBgqTHZRTropESXLc7Vet8XpJrGUSBZAT2UHvQBYpBEPdAUiyKBi2XC2iFjgtn5Gw2Qd4WXHyj1LxjU",
-			Type:                   []string{credential.VerifiablePresentationType},
-			PresentationSubmission: ps,
-			VerifiableCredential:   []interface{}{keyaccess.JWT(vcData)},
-			Proof:                  nil,
-		}
+				{
+					Status: "pending",
+					PresentationSubmission: &exchange.PresentationSubmission{
+						ID:           operation.SubmissionID(op2.ID),
+						DefinitionID: definition.PresentationDefinition.ID,
+					},
+				},
+			}
+			diff := cmp.Diff(expectedSubmissions, resp.Submissions,
+				cmpopts.IgnoreFields(exchange.PresentationSubmission{}, "DescriptorMap"),
+				cmpopts.SortSlices(func(l, r presentation.Submission) bool {
+					return l.PresentationSubmission.ID < r.PresentationSubmission.ID
+				}),
+			)
+			if diff != "" {
+				t.Errorf("Mismatch on submissions (-want +got):\n%s", diff)
+			}
+		})
+	})
 
-		signer1 := getTestVectorKey1Signer(t)
-		signed, err := signing.SignVerifiablePresentationJWT(signer1, vp)
-		assert.NoError(t, err)
+}
 
-		request := router.CreateSubmissionRequest{SubmissionJWT: keyaccess.JWT(signed)}
+func setupRouter(t *testing.T) *router.PresentationRouter {
+	s, err := storage.NewStorage(storage.Bolt)
+	assert.NoError(t, err)
 
-		value := newRequestValue(t, request)
-		req := httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/presentations/submissions", value)
-		w := httptest.NewRecorder()
+	keyStoreService := testKeyStoreService(t, s)
+	didService := testDIDService(t, s, keyStoreService)
+	schemaService := testSchemaService(t, s, keyStoreService, didService)
 
-		err = pRouter.CreateSubmission(newRequestContext(), w, req)
+	service, err := presentation.NewPresentationService(config.PresentationServiceConfig{}, s, didService.GetResolver(), schemaService)
+	assert.NoError(t, err)
+
+	pRouter, err := router.NewPresentationRouter(service)
+	assert.NoError(t, err)
 
-		require.NoError(t, err)
-		var resp router.Operation
-		assert.NoError(t, json.NewDecoder(w.Body).Decode(&resp))
-		assert.Contains(t, resp.ID, "presentations/submissions/")
-		assert.False(t, resp.Done)
-		assert.Zero(t, resp.Result)
+	t.Cleanup(func() {
+		_ = s.Close()
+		_ = os.Remove(storage.DBFile)
 	})
+	return pRouter
+}
+
+func createSubmission(t *testing.T, pRouter *router.PresentationRouter, definitionID string, vc credential.VerifiableCredential, holderDID did.DIDKey, holderSigner crypto.JWTSigner) router.Operation {
+	request := createSubmissionRequest(t, definitionID, vc, holderSigner, holderDID)
+
+	value := newRequestValue(t, request)
+	req := httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/presentations/submissions", value)
+	w := httptest.NewRecorder()
+
+	err := pRouter.CreateSubmission(newRequestContext(), w, req)
+
+	require.NoError(t, err)
+	var resp router.Operation
+	assert.NoError(t, json.NewDecoder(w.Body).Decode(&resp))
+	return resp
 }
 
+func createSubmissionRequest(t *testing.T, definitionID string, vc credential.VerifiableCredential, holderSigner crypto.JWTSigner, holderDID did.DIDKey) router.CreateSubmissionRequest {
+	issuerSigner, didKey := getSigner(t)
+	vc.Issuer = didKey.String()
+	vcData, err := signing.SignVerifiableCredentialJWT(issuerSigner, vc)
+	assert.NoError(t, err)
+	ps := exchange.PresentationSubmission{
+		ID:           uuid.NewString(),
+		DefinitionID: definitionID,
+		DescriptorMap: []exchange.SubmissionDescriptor{
+			{
+				ID:         "wa_driver_license",
+				Format:     string(exchange.JWTVPTarget),
+				Path:       "$.verifiableCredential[0]",
+				PathNested: nil,
+			},
+		},
+	}
+
+	vp := credential.VerifiablePresentation{
+		Context:                []string{credential.VerifiableCredentialsLinkedDataContext},
+		ID:                     uuid.NewString(),
+		Holder:                 holderDID.String(),
+		Type:                   []string{credential.VerifiablePresentationType},
+		PresentationSubmission: ps,
+		VerifiableCredential:   []interface{}{keyaccess.JWT(vcData)},
+		Proof:                  nil,
+	}
+
+	signed, err := signing.SignVerifiablePresentationJWT(holderSigner, vp)
+	assert.NoError(t, err)
+
+	request := router.CreateSubmissionRequest{SubmissionJWT: keyaccess.JWT(signed)}
+	return request
+}
+
+func VerifiableCredential(options ...VCOption) credential.VerifiableCredential {
+	vc := credential.VerifiableCredential{
+		Context:          []string{credential.VerifiableCredentialsLinkedDataContext},
+		ID:               uuid.NewString(),
+		Type:             []string{credential.VerifiablePresentationType},
+		Issuer:           "did:key:z4oJ8bFEFv7E3omhuK5LrAtL29Nmd8heBey9HtJCSvodSb7nrfaMrd6zb7fjYSRxrfSgBSDeM6Bs59KRKFgXSDWJcfcjs",
+		IssuanceDate:     "2022-11-07T21:28:57Z",
+		ExpirationDate:   "2051-10-05T14:48:00.000Z",
+		CredentialStatus: nil,
+		CredentialSubject: credential.CredentialSubject{
+			"additionalName": "Mclovin",
+			"dateOfBirth":    "1987-01-02",
+			"familyName":     "Andres",
+			"givenName":      "Uribe",
+			"id":             "did:web:andresuribe.com",
+		},
+		CredentialSchema: nil,
+		RefreshService:   nil,
+		TermsOfUse:       nil,
+		Evidence:         nil,
+		Proof:            nil,
+	}
+	for _, o := range options {
+		o(&vc)
+	}
+	return vc
+}
+
+func WithCredentialSubject(subject credential.CredentialSubject) VCOption {
+	return func(vc *credential.VerifiableCredential) {
+		vc.CredentialSubject = subject
+	}
+}
+
+type VCOption func(verifiableCredential *credential.VerifiableCredential)
+
 func createPresentationDefinition(t *testing.T, pRouter *router.PresentationRouter) router.CreatePresentationDefinitionResponse {
 	request := router.CreatePresentationDefinitionRequest{
 		Name:                   "name",
@@ -254,34 +424,12 @@ func createPresentationDefinition(t *testing.T, pRouter *router.PresentationRout
 	return resp
 }
 
-func getTestVectorKey0Signer(t *testing.T) crypto.JWTSigner {
-	// AKA the issuers key
-	// AKA did:key:z4oJ8bFEFv7E3omhuK5LrAtL29Nmd8heBey9HtJCSvodSb7nrfaMrd6zb7fjYSRxrfSgBSDeM6Bs59KRKFgXSDWJcfcjs
-	knownJWK := crypto.PrivateKeyJWK{
-		KTY: "EC",
-		CRV: "P-256",
-		X:   "SVqB4JcUD6lsfvqMr-OKUNUphdNn64Eay60978ZlL74",
-		Y:   "lf0u0pMj4lGAzZix5u4Cm5CMQIgMNpkwy163wtKYVKI",
-		D:   "0g5vAEKzugrXaRbgKG0Tj2qJ5lMP4Bezds1_sTybkfk",
-	}
-
-	signer, err := crypto.NewJWTSignerFromJWK(knownJWK.KID, knownJWK)
+func getSigner(t *testing.T) (crypto.JWTSigner, did.DIDKey) {
+	private, didKey, err := did.GenerateDIDKey(crypto.P256)
 	assert.NoError(t, err)
-	return *signer
-}
 
-func getTestVectorKey1Signer(t *testing.T) crypto.JWTSigner {
-	// AKA the submitter of the presentation
-	// AKA did:key:z4oJ8eRi73fvkrXBgqTHZRTropESXLc7Vet8XpJrGUSBZAT2UHvQBYpBEPdAUiyKBi2XC2iFjgtn5Gw2Qd4WXHyj1LxjU
-	knownJWK := crypto.PrivateKeyJWK{
-		KTY: "EC",
-		CRV: "P-256",
-		X:   "6HEz8SLP7NgHPGp0bElryiD7u3_cO1EmX-ngsV_yLsI",
-		Y:   "QlIYaYyDLxLkybDan9LOSkfGvjzZsrdgAb_nQr_Li5M",
-		D:   "7m6c2Axy9OWi7-d9hFVhmMe22vQTfQDL_pG-3WFsjzc",
-	}
-
-	signer, err := crypto.NewJWTSignerFromJWK(knownJWK.KID, knownJWK)
+	signer, err := crypto.NewJWTSigner(didKey.String(), private)
 	assert.NoError(t, err)
-	return *signer
+
+	return *signer, *didKey
 }
diff --git a/pkg/service/operation/model_test.go b/pkg/service/operation/model_test.go
new file mode 100644
index 0000000..8903db2
--- /dev/null
+++ b/pkg/service/operation/model_test.go
@@ -0,0 +1,35 @@
+package operation
+
+import (
+	"github.com/stretchr/testify/assert"
+	"testing"
+)
+
+func TestSubmissionID(t *testing.T) {
+	tests := []struct {
+		name string
+		opID string
+		want string
+	}{
+		{
+			name: "normal op id returns last word",
+			opID: "presentations/submissions/a30e3b91-fb77-4d22-95fa-871689c322e2",
+			want: "a30e3b91-fb77-4d22-95fa-871689c322e2",
+		},
+		{
+			name: "op id with no id returns empty",
+			opID: "presentations/submissions/",
+			want: "",
+		},
+		{
+			name: "crazy id returns empty",
+			opID: "some crazy string",
+			want: "",
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			assert.Equal(t, tt.want, SubmissionID(tt.opID))
+		})
+	}
+}