Repository: TBD54566975/ssi-service
Pull request #402 (2023-05-04 20:55:59)

Update GetDIDs to accept a deleted query parameter
# Overview
This PR introduces a `deleted` query parameter to the GetDIDs API call. 
**WIP - some sections not completed yet, more work to be done to this PR**

## Description
When calling GetDIDs, the presence of a query param `deleted="true"` should return only DIDs that have been soft deleted (has `isSoftDeleted` flag), while calling GetDIDs without the presence of the query param should return only DIDs that have not been soft deleted.  

## How Has This Been Tested?
- [x] Functionally tested in Postman 
1. Ran through steps of creating did, "soft" deleting did, getting all dids, getting all dids with query param `deleted="true"`
2. Confirmed getDIDs returned empty, getDIDs with `deleted="true"` returned the "soft" deleted did
- [x] Added unit tests where `deleted="true"` should return all deleted dids
1. Run `mage test`
2. All tests should pass

## TODO
- Add Swagger Documentation for new optional `deleted` query param


codecov-commenter: ## [Codecov](https://app.codecov.io/gh/TBD54566975/ssi-service/pull/402?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=TBD54566975) Report
> Merging [#402](https://app.codecov.io/gh/TBD54566975/ssi-service/pull/402?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=TBD54566975) (23a4b4d) into [main](https://app.codecov.io/gh/TBD54566975/ssi-service/commit/d6734674a672a772e85d3724dca1220f8b566fb1?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=TBD54566975) (d673467) will **increase** coverage by `0.02%`.
> The diff coverage is `21.42%`.

```diff
@@            Coverage Diff             @@
##             main     #402      +/-   ##
==========================================
+ Coverage   20.43%   20.46%   +0.02%     
==========================================
  Files          48       48              
  Lines        5422     5430       +8     
==========================================
+ Hits         1108     1111       +3     
- Misses       4139     4144       +5     
  Partials      175      175              
```


| [Impacted Files](https://app.codecov.io/gh/TBD54566975/ssi-service/pull/402?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=TBD54566975) | Coverage Δ | |
|---|---|---|
| [pkg/server/router/did.go](https://app.codecov.io/gh/TBD54566975/ssi-service/pull/402?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=TBD54566975#diff-cGtnL3NlcnZlci9yb3V0ZXIvZGlkLmdv) | `4.59% <0.00%> (-0.26%)` | :arrow_down: |
| [pkg/service/did/handler.go](https://app.codecov.io/gh/TBD54566975/ssi-service/pull/402?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=TBD54566975#diff-cGtnL3NlcnZpY2UvZGlkL2hhbmRsZXIuZ28=) | `0.00% <ø> (ø)` | |
| [pkg/service/did/ion.go](https://app.codecov.io/gh/TBD54566975/ssi-service/pull/402?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=TBD54566975#diff-cGtnL3NlcnZpY2UvZGlkL2lvbi5nbw==) | `49.74% <0.00%> (+0.25%)` | :arrow_up: |
| [pkg/service/did/key.go](https://app.codecov.io/gh/TBD54566975/ssi-service/pull/402?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=TBD54566975#diff-cGtnL3NlcnZpY2UvZGlkL2tleS5nbw==) | `0.00% <0.00%> (ø)` | |
| [pkg/service/did/service.go](https://app.codecov.io/gh/TBD54566975/ssi-service/pull/402?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=TBD54566975#diff-cGtnL3NlcnZpY2UvZGlkL3NlcnZpY2UuZ28=) | `0.00% <0.00%> (ø)` | |
| [pkg/service/did/web.go](https://app.codecov.io/gh/TBD54566975/ssi-service/pull/402?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=TBD54566975#diff-cGtnL3NlcnZpY2UvZGlkL3dlYi5nbw==) | `0.00% <0.00%> (ø)` | |
| [pkg/service/did/storage.go](https://app.codecov.io/gh/TBD54566975/ssi-service/pull/402?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=TBD54566975#diff-cGtnL3NlcnZpY2UvZGlkL3N0b3JhZ2UuZ28=) | `58.53% <100.00%> (+1.97%)` | :arrow_up: |



Patch:
diff --git a/pkg/server/router/did.go b/pkg/server/router/did.go
index f00ef7d..8c43f9d 100644
--- a/pkg/server/router/did.go
+++ b/pkg/server/router/did.go
@@ -4,6 +4,7 @@ import (
 	"context"
 	"fmt"
 	"net/http"
+	"strconv"
 
 	"github.com/TBD54566975/ssi-sdk/crypto"
 	didsdk "github.com/TBD54566975/ssi-sdk/did"
@@ -18,8 +19,9 @@ import (
 )
 
 const (
-	MethodParam = "method"
-	IDParam     = "id"
+	MethodParam  = "method"
+	IDParam      = "id"
+	DeletedParam = "deleted"
 )
 
 // DIDRouter represents the dependencies required to instantiate a DID-HTTP service
@@ -235,26 +237,39 @@ type GetDIDsRequest struct {
 // GetDIDsByMethod godoc
 //
 // @Summary     Get DIDs
-// @Description Get DIDs by method
+// @Description Get DIDs by method. Checks for an optional "deleted=true" query parameter, which exclusively returns DIDs that have been "Soft Deleted".
 // @Tags        DecentralizedIdentityAPI
 // @Accept      json
 // @Produce     json
+// @Param       deleted  query    boolean false "When true, returns soft-deleted DIDs. Otherwise, returns DIDs that have not been soft-deleted. Default is false."
 // @Param       request body     GetDIDsRequest true "request body"
 // @Success     200     {object} GetDIDsByMethodResponse
 // @Failure     400     {string} string "Bad request"
 // @Failure     500     {string} string "Internal server error"
-// @Router      /v1/dids [get]
-func (dr DIDRouter) GetDIDsByMethod(ctx context.Context, w http.ResponseWriter, _ *http.Request) error {
+// @Router      /v1/dids/{method} [get]
+func (dr DIDRouter) GetDIDsByMethod(ctx context.Context, w http.ResponseWriter, r *http.Request) error {
 	method := framework.GetParam(ctx, MethodParam)
+	deleted := framework.GetQueryValue(r, DeletedParam)
 	if method == nil {
 		errMsg := "get DIDs by method request missing method parameter"
 		logrus.Error(errMsg)
 		return framework.NewRequestErrorMsg(errMsg, http.StatusBadRequest)
 	}
+	getIsDeleted := false
+	if deleted != nil {
+		checkDeleted, err := strconv.ParseBool(*deleted)
+		getIsDeleted = checkDeleted
+
+		if err != nil {
+			errMsg := "get DIDs by method request encountered a problem with the `deleted` query param"
+			logrus.Error(errMsg)
+			return framework.NewRequestErrorMsg(errMsg, http.StatusBadRequest)
+		}
+	}
 
 	// TODO(gabe) check if the method is supported, to tell whether this is a bad req or internal error
 	// TODO(gabe) differentiate between internal errors and not found DIDs
-	getDIDsRequest := did.GetDIDsRequest{Method: didsdk.Method(*method)}
+	getDIDsRequest := did.GetDIDsRequest{Method: didsdk.Method(*method), Deleted: getIsDeleted}
 	gotDIDs, err := dr.service.GetDIDsByMethod(ctx, getDIDsRequest)
 	if err != nil {
 		errMsg := fmt.Sprintf("could not get DIDs for method: %s", *method)
diff --git a/pkg/service/did/handler.go b/pkg/service/did/handler.go
index 100c248..207149a 100644
--- a/pkg/service/did/handler.go
+++ b/pkg/service/did/handler.go
@@ -17,6 +17,7 @@ type MethodHandler interface {
 	// TODO(gabe): support query parameters to get soft deleted and other DIDs https://github.com/TBD54566975/ssi-service/issues/364
 	GetDID(ctx context.Context, request GetDIDRequest) (*GetDIDResponse, error)
 	GetDIDs(ctx context.Context) (*GetDIDsResponse, error)
+	GetDeletedDIDs(ctx context.Context) (*GetDIDsResponse, error)
 	SoftDeleteDID(ctx context.Context, request DeleteDIDRequest) error
 }
 
diff --git a/pkg/service/did/ion.go b/pkg/service/did/ion.go
index e0365b4..a09090d 100644
--- a/pkg/service/did/ion.go
+++ b/pkg/service/did/ion.go
@@ -262,6 +262,23 @@ func (h *ionHandler) GetDIDs(ctx context.Context) (*GetDIDsResponse, error) {
 	return &GetDIDsResponse{DIDs: dids}, nil
 }
 
+// GetDeletedDIDs returns only DIDs we have in storage for ION with SoftDeleted flag set to true
+func (h *ionHandler) GetDeletedDIDs(ctx context.Context) (*GetDIDsResponse, error) {
+	logrus.Debug("getting stored did:ion DIDs")
+
+	gotDIDs, err := h.storage.GetDIDs(ctx, did.IONMethod.String(), new(ionStoredDID))
+	if err != nil {
+		return nil, fmt.Errorf("error getting did:ion DIDs")
+	}
+	dids := make([]did.Document, 0, len(gotDIDs))
+	for _, gotDID := range gotDIDs {
+		if gotDID.IsSoftDeleted() {
+			dids = append(dids, gotDID.GetDocument())
+		}
+	}
+	return &GetDIDsResponse{DIDs: dids}, nil
+}
+
 // SoftDeleteDID soft deletes a DID from storage but has no effect on the DID's state on the network
 func (h *ionHandler) SoftDeleteDID(ctx context.Context, request DeleteDIDRequest) error {
 	logrus.Debugf("soft deleting DID: %+v", request)
diff --git a/pkg/service/did/key.go b/pkg/service/did/key.go
index 851afd7..a740d61 100644
--- a/pkg/service/did/key.go
+++ b/pkg/service/did/key.go
@@ -110,6 +110,23 @@ func (h *keyHandler) GetDIDs(ctx context.Context) (*GetDIDsResponse, error) {
 	return &GetDIDsResponse{DIDs: dids}, nil
 }
 
+// GetDeletedDIDs returns only DIDs we have in storage for Key with SoftDeleted flag set to true
+func (h *keyHandler) GetDeletedDIDs(ctx context.Context) (*GetDIDsResponse, error) {
+	logrus.Debug("getting did:key DIDs")
+
+	gotDIDs, err := h.storage.GetDIDsDefault(ctx, did.KeyMethod.String())
+	if err != nil {
+		return nil, fmt.Errorf("error getting did:key DIDs")
+	}
+	dids := make([]did.Document, 0, len(gotDIDs))
+	for _, gotDID := range gotDIDs {
+		if gotDID.IsSoftDeleted() {
+			dids = append(dids, gotDID.GetDocument())
+		}
+	}
+	return &GetDIDsResponse{DIDs: dids}, nil
+}
+
 func (h *keyHandler) SoftDeleteDID(ctx context.Context, request DeleteDIDRequest) error {
 	logrus.Debugf("soft deleting DID: %+v", request)
 
diff --git a/pkg/service/did/model.go b/pkg/service/did/model.go
index 6ac3739..e0ac279 100644
--- a/pkg/service/did/model.go
+++ b/pkg/service/did/model.go
@@ -58,7 +58,8 @@ type GetKeyFromDIDResponse struct {
 }
 
 type GetDIDsRequest struct {
-	Method didsdk.Method `json:"method" validate:"required"`
+	Method  didsdk.Method `json:"method" validate:"required"`
+	Deleted bool          `json:"deleted"`
 }
 
 // GetDIDsResponse is the JSON-serializable response for getting all DIDs for a given method
diff --git a/pkg/service/did/service.go b/pkg/service/did/service.go
index acf842a..65c3350 100644
--- a/pkg/service/did/service.go
+++ b/pkg/service/did/service.go
@@ -194,10 +194,12 @@ func (s *Service) GetKeyFromDID(ctx context.Context, request GetKeyFromDIDReques
 }
 
 func (s *Service) GetDIDsByMethod(ctx context.Context, request GetDIDsRequest) (*GetDIDsResponse, error) {
-	method := request.Method
-	handler, err := s.getHandler(method)
+	handler, err := s.getHandler(request.Method)
 	if err != nil {
-		return nil, sdkutil.LoggingErrorMsgf(err, "could not get handler for method<%s>", method)
+		return nil, sdkutil.LoggingErrorMsgf(err, "could not get handler for method<%s>", request.Method)
+	}
+	if request.Deleted {
+		return handler.GetDeletedDIDs(ctx)
 	}
 	return handler.GetDIDs(ctx)
 }
diff --git a/pkg/service/did/web.go b/pkg/service/did/web.go
index dac7c6a..01363d7 100644
--- a/pkg/service/did/web.go
+++ b/pkg/service/did/web.go
@@ -140,6 +140,21 @@ func (h *webHandler) GetDIDs(ctx context.Context) (*GetDIDsResponse, error) {
 	}
 	return &GetDIDsResponse{DIDs: dids}, nil
 }
+func (h *webHandler) GetDeletedDIDs(ctx context.Context) (*GetDIDsResponse, error) {
+	logrus.Debug("getting did:web DID")
+
+	gotDIDs, err := h.storage.GetDIDsDefault(ctx, did.WebMethod.String())
+	if err != nil {
+		return nil, errors.Wrap(err, "getting did:web DIDs")
+	}
+	dids := make([]did.Document, 0, len(gotDIDs))
+	for _, gotDID := range gotDIDs {
+		if gotDID.IsSoftDeleted() {
+			dids = append(dids, gotDID.GetDocument())
+		}
+	}
+	return &GetDIDsResponse{DIDs: dids}, nil
+}
 
 func (h *webHandler) SoftDeleteDID(ctx context.Context, request DeleteDIDRequest) error {
 	logrus.Debugf("soft deleting DID: %+v", request)


Test patch:
diff --git a/pkg/server/router/did_test.go b/pkg/server/router/did_test.go
index a879f4e..3547d22 100644
--- a/pkg/server/router/did_test.go
+++ b/pkg/server/router/did_test.go
@@ -108,6 +108,12 @@ func TestDIDRouter(t *testing.T) {
 		assert.NoError(tt, err)
 		assert.NotEmpty(tt, getDIDsResponse)
 		assert.Len(tt, getDIDsResponse.DIDs, 0)
+
+		// get deleted DIDs back
+		getDIDsResponse, err = didService.GetDIDsByMethod(context.Background(), did.GetDIDsRequest{Method: didsdk.KeyMethod, Deleted: true})
+		assert.NoError(tt, err)
+		assert.NotEmpty(tt, getDIDsResponse)
+		assert.Len(tt, getDIDsResponse.DIDs, 2)
 	})
 
 	t.Run("DID Web Service Test", func(tt *testing.T) {
diff --git a/pkg/server/server_did_test.go b/pkg/server/server_did_test.go
index ce49ab2..6a30e33 100644
--- a/pkg/server/server_did_test.go
+++ b/pkg/server/server_did_test.go
@@ -408,6 +408,16 @@ func TestDIDAPI(t *testing.T) {
 		err = json.NewDecoder(w.Body).Decode(&gotDIDsResponseAfterDelete)
 		assert.NoError(tt, err)
 		assert.Len(tt, gotDIDsResponseAfterDelete.DIDs, 0)
+
+		// get all deleted dids for method
+		req = httptest.NewRequest(http.MethodGet, "https://ssi-service.com/v1/dids/key?deleted=true", requestReader)
+		err = didService.GetDIDsByMethod(newRequestContextWithParams(params), w, req)
+		assert.NoError(tt, err)
+
+		var gotDeletedDIDsResponseAfterDelete router.GetDIDsByMethodResponse
+		err = json.NewDecoder(w.Body).Decode(&gotDeletedDIDsResponseAfterDelete)
+		assert.NoError(tt, err)
+		assert.Len(tt, gotDeletedDIDsResponseAfterDelete.DIDs, 1)
 	})
 
 	t.Run("Test Get DIDs By Method", func(tt *testing.T) {
diff --git a/pkg/service/did/ion_test.go b/pkg/service/did/ion_test.go
index 5e59224..a6f391d 100644
--- a/pkg/service/did/ion_test.go
+++ b/pkg/service/did/ion_test.go
@@ -138,6 +138,60 @@ func TestIONHandler(t *testing.T) {
 		assert.Equal(tt, created.DID.ID, gotDID.DID.ID)
 	})
 
+	t.Run("Test Get DIDs from storage", func(tt *testing.T) {
+		// create a handler
+		s := setupTestDB(tt)
+		keystoreService := testKeyStoreService(tt, s)
+		didStorage, err := NewDIDStorage(s)
+		assert.NoError(tt, err)
+		handler, err := NewIONHandler("https://test-ion-resolver.com", didStorage, keystoreService)
+		assert.NoError(tt, err)
+		assert.NotEmpty(tt, handler)
+
+		gock.New("https://test-ion-resolver.com").
+			Post("/operations").
+			Reply(200)
+		defer gock.Off()
+
+		// create a did
+		created, err := handler.CreateDID(context.Background(), CreateDIDRequest{
+			Method:  did.IONMethod,
+			KeyType: crypto.Ed25519,
+		})
+		assert.NoError(tt, err)
+		assert.NotEmpty(tt, created)
+
+		gock.New("https://test-ion-resolver.com").
+			Get("/identifiers/" + created.DID.ID).
+			Reply(200).BodyString(fmt.Sprintf(`{"didDocument": {"id": "%s"}}`, created.DID.ID))
+		defer gock.Off()
+
+		// get all DIDs
+		gotDIDs, err := handler.GetDIDs(context.Background())
+		assert.NoError(tt, err)
+		assert.NotEmpty(tt, gotDIDs)
+		assert.Len(tt, gotDIDs.DIDs, 1)
+
+		// delete a did
+		err = handler.SoftDeleteDID(context.Background(), DeleteDIDRequest{
+			Method: did.IONMethod,
+			ID:     created.DID.ID,
+		})
+		assert.NoError(tt, err)
+
+		// get all DIDs after deleting
+		gotDIDsAfterDelete, err := handler.GetDIDs(context.Background())
+		assert.NoError(tt, err)
+		assert.NotEmpty(tt, gotDIDs)
+		assert.Len(tt, gotDIDsAfterDelete.DIDs, 0)
+
+		// get all deleted DIDs after delete
+		gotDeletedDIDs, err := handler.GetDeletedDIDs(context.Background())
+		assert.NoError(tt, err)
+		assert.NotEmpty(tt, gotDIDs)
+		assert.Len(tt, gotDeletedDIDs.DIDs, 1)
+	})
+
 	t.Run("Test Get DID from resolver", func(tt *testing.T) {
 		// create a handler
 		s := setupTestDB(tt)