Repository: TBD54566975/ssi-service
Pull request #116 (2022-09-29 16:59:17)

Support for Getting All DIDs for a Given Method


Patch:
diff --git a/pkg/server/router/did.go b/pkg/server/router/did.go
index 94b32ee..2bc1702 100644
--- a/pkg/server/router/did.go
+++ b/pkg/server/router/did.go
@@ -152,3 +152,39 @@ func (dr DIDRouter) GetDIDByMethod(ctx context.Context, w http.ResponseWriter, _
 	resp := GetDIDByMethodResponse{DID: gotDID.DID}
 	return framework.Respond(ctx, w, resp, http.StatusOK)
 }
+
+type GetDIDsByMethodResponse struct {
+	DIDs []didsdk.DIDDocument `json:"dids,omitempty"`
+}
+
+// GetDIDsByMethod godoc
+// @Summary      Get DIDs
+// @Description  Get DIDs by method
+// @Tags         DecentralizedIdentityAPI
+// @Accept       json
+// @Produce      json
+// @Param        method   path      string                    true  "Method"
+// @Success      200      {object}  GetDIDsByMethodResponse
+// @Failure      400      {string}  string  "Bad request"
+// @Router       /v1/dids/{method} [get]
+func (dr DIDRouter) GetDIDsByMethod(ctx context.Context, w http.ResponseWriter, _ *http.Request) error {
+	method := framework.GetParam(ctx, MethodParam)
+	if method == nil {
+		errMsg := "get DIDs by method request missing method parameter"
+		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: did.Method(*method)}
+	gotDIDs, err := dr.service.GetDIDsByMethod(getDIDsRequest)
+	if err != nil {
+		errMsg := fmt.Sprintf("could not get DIDs for method: %s", *method)
+		logrus.WithError(err).Error(errMsg)
+		return framework.NewRequestError(errors.Wrap(err, errMsg), http.StatusBadRequest)
+	}
+
+	resp := GetDIDsByMethodResponse{DIDs: gotDIDs.DIDs}
+	return framework.Respond(ctx, w, resp, http.StatusOK)
+}
diff --git a/pkg/server/server.go b/pkg/server/server.go
index b9f5c6d..60267f2 100644
--- a/pkg/server/server.go
+++ b/pkg/server/server.go
@@ -115,6 +115,7 @@ func (s *SSIServer) DecentralizedIdentityAPI(service svcframework.Service) (err
 
 	s.Handle(http.MethodGet, handlerPath, didRouter.GetDIDMethods)
 	s.Handle(http.MethodPut, path.Join(handlerPath, "/:method"), didRouter.CreateDIDByMethod)
+	s.Handle(http.MethodGet, path.Join(handlerPath, "/:method"), didRouter.GetDIDsByMethod)
 	s.Handle(http.MethodGet, path.Join(handlerPath, "/:method/:id"), didRouter.GetDIDByMethod)
 	return
 }
diff --git a/pkg/service/did/did.go b/pkg/service/did/did.go
index 449732f..3571c7d 100644
--- a/pkg/service/did/did.go
+++ b/pkg/service/did/did.go
@@ -33,6 +33,7 @@ type Service struct {
 type MethodHandler interface {
 	CreateDID(request CreateDIDRequest) (*CreateDIDResponse, error)
 	GetDID(request GetDIDRequest) (*GetDIDResponse, error)
+	GetDIDs(method Method) (*GetDIDsResponse, error)
 }
 
 func NewDIDService(config config.DIDServiceConfig, s storage.ServiceStorage, keyStore *keystore.Service) (*Service, error) {
@@ -116,6 +117,16 @@ func (s *Service) GetDIDByMethod(request GetDIDRequest) (*GetDIDResponse, error)
 	return handler.GetDID(request)
 }
 
+func (s *Service) GetDIDsByMethod(request GetDIDsRequest) (*GetDIDsResponse, error) {
+	method := request.Method
+	handler, err := s.getHandler(method)
+	if err != nil {
+		errMsg := fmt.Sprintf("could not get handler for method<%s>", method)
+		return nil, util.LoggingErrorMsg(err, errMsg)
+	}
+	return handler.GetDIDs(method)
+}
+
 func (s *Service) getHandler(method Method) (MethodHandler, error) {
 	handler, ok := s.handlers[method]
 	if !ok {
diff --git a/pkg/service/did/key.go b/pkg/service/did/key.go
index 822a557..e56de59 100644
--- a/pkg/service/did/key.go
+++ b/pkg/service/did/key.go
@@ -87,6 +87,21 @@ func (h *keyDIDHandler) GetDID(request GetDIDRequest) (*GetDIDResponse, error) {
 	return &GetDIDResponse{DID: gotDID.DID}, nil
 }
 
+func (h *keyDIDHandler) GetDIDs(method Method) (*GetDIDsResponse, error) {
+
+	logrus.Debugf("getting DIDs for method: %s", method)
+
+	gotDIDs, err := h.storage.GetDIDs(string(method))
+	if err != nil {
+		return nil, fmt.Errorf("error getting DIDs for method: %s", method)
+	}
+	var dids []did.DIDDocument
+	for _, did := range gotDIDs {
+		dids = append(dids, did.DID)
+	}
+	return &GetDIDsResponse{DIDs: dids}, nil
+}
+
 func privateKeyToBase58(privKey interface{}) (string, error) {
 	if haveBytes, ok := privKey.([]byte); ok {
 		return base58.Encode(haveBytes), nil
diff --git a/pkg/service/did/model.go b/pkg/service/did/model.go
index 8625d27..ea49ec1 100644
--- a/pkg/service/did/model.go
+++ b/pkg/service/did/model.go
@@ -31,3 +31,12 @@ type GetDIDRequest struct {
 type GetDIDResponse struct {
 	DID didsdk.DIDDocument `json:"did"`
 }
+
+type GetDIDsRequest struct {
+	Method Method `json:"method" validate:"required"`
+}
+
+// GetDIDsResponse is the JSON-serializable response for getting all DIDs for a given method
+type GetDIDsResponse struct {
+	DIDs []didsdk.DIDDocument `json:"dids"`
+}
diff --git a/pkg/service/did/storage/bolt.go b/pkg/service/did/storage/bolt.go
index df2b9b1..2f26044 100644
--- a/pkg/service/did/storage/bolt.go
+++ b/pkg/service/did/storage/bolt.go
@@ -5,6 +5,7 @@ import (
 
 	"github.com/goccy/go-json"
 	"github.com/pkg/errors"
+	"github.com/sirupsen/logrus"
 
 	"github.com/tbd54566975/ssi-service/internal/util"
 	"github.com/tbd54566975/ssi-service/pkg/storage"
@@ -79,8 +80,8 @@ func (b BoltDIDStorage) GetDIDs(method string) ([]StoredDID, error) {
 		return nil, util.LoggingErrorMsg(err, couldNotGetDIDsErr)
 	}
 	if len(gotDIDs) == 0 {
-		err := fmt.Errorf("no DIDs found for method: %s", method)
-		return nil, util.LoggingErrorMsg(err, "could not get stored DIDs")
+		logrus.Infof("no DIDs found for method: %s", method)
+		return nil, nil
 	}
 	var stored []StoredDID
 	for _, didBytes := range gotDIDs {


Test patch:
diff --git a/pkg/server/router/did_test.go b/pkg/server/router/did_test.go
index 33c5122..3799ed9 100644
--- a/pkg/server/router/did_test.go
+++ b/pkg/server/router/did_test.go
@@ -79,5 +79,26 @@ func TestDIDRouter(t *testing.T) {
 
 		// make sure it's the same value
 		assert.Equal(tt, createDIDResponse.DID.ID, getDIDResponse.DID.ID)
+
+		// create a second DID
+		createDIDResponse2, err := didService.CreateDIDByMethod(did.CreateDIDRequest{Method: did.KeyMethod, KeyType: crypto.Ed25519})
+		assert.NoError(tt, err)
+		assert.NotEmpty(tt, createDIDResponse2)
+
+		// get all DIDs back
+		getDIDsResponse, err := didService.GetDIDsByMethod(did.GetDIDsRequest{Method: did.KeyMethod})
+		assert.NoError(tt, err)
+		assert.NotEmpty(tt, getDIDsResponse)
+		assert.Len(tt, getDIDsResponse.DIDs, 2)
+
+		knownDIDs := map[string]bool{createDIDResponse.DID.ID: true, createDIDResponse2.DID.ID: true}
+		for _, did := range getDIDsResponse.DIDs {
+			if _, ok := knownDIDs[did.ID]; !ok {
+				tt.Error("got unknown DID")
+			} else {
+				delete(knownDIDs, did.ID)
+			}
+		}
+		assert.Len(tt, knownDIDs, 0)
 	})
 }
diff --git a/pkg/server/server_test.go b/pkg/server/server_test.go
index b2eaf55..4dcb68b 100644
--- a/pkg/server/server_test.go
+++ b/pkg/server/server_test.go
@@ -146,21 +146,25 @@ func TestDIDAPI(t *testing.T) {
 		assert.Error(tt, err)
 		assert.Contains(tt, err.Error(), "invalid create DID request")
 
+		// reset recorder between calls
+		w.Flush()
+
 		// with body, bad key type
 		createDIDRequest := router.CreateDIDByMethodRequest{KeyType: "bad"}
 		requestReader := newRequestValue(tt, createDIDRequest)
 		req = httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/dids/key", requestReader)
-		w = httptest.NewRecorder()
 
 		err = didService.CreateDIDByMethod(newRequestContextWithParams(params), w, req)
 		assert.Error(tt, err)
 		assert.Contains(tt, err.Error(), "could not create DID for method<key> with key type: bad")
 
+		// reset recorder between calls
+		w.Flush()
+
 		// with body, good key type
 		createDIDRequest = router.CreateDIDByMethodRequest{KeyType: crypto.Ed25519}
 		requestReader = newRequestValue(tt, createDIDRequest)
 		req = httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/dids/key", requestReader)
-		w = httptest.NewRecorder()
 
 		err = didService.CreateDIDByMethod(newRequestContextWithParams(params), w, req)
 		assert.NoError(tt, err)
@@ -197,6 +201,9 @@ func TestDIDAPI(t *testing.T) {
 		assert.Error(tt, err)
 		assert.Contains(tt, err.Error(), "could not get DID for method<bad>")
 
+		// reset recorder between calls
+		w.Flush()
+
 		// good method, bad id
 		badParams1 := map[string]string{
 			"method": "key",
@@ -206,12 +213,13 @@ func TestDIDAPI(t *testing.T) {
 		assert.Error(tt, err)
 		assert.Contains(tt, err.Error(), "could not get DID for method<key> with id: worse")
 
+		// reset recorder between calls
+		w.Flush()
 		// store a DID
 		createDIDRequest := router.CreateDIDByMethodRequest{KeyType: crypto.Ed25519}
 		requestReader := newRequestValue(tt, createDIDRequest)
 		params := map[string]string{"method": "key"}
 		req = httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/dids/key", requestReader)
-		w = httptest.NewRecorder()
 
 		err = didService.CreateDIDByMethod(newRequestContextWithParams(params), w, req)
 		assert.NoError(tt, err)
@@ -220,11 +228,13 @@ func TestDIDAPI(t *testing.T) {
 		err = json.NewDecoder(w.Body).Decode(&createdDID)
 		assert.NoError(tt, err)
 
+		// reset recorder between calls
+		w.Flush()
+
 		// get it back
 		createdID := createdDID.DID.ID
 		getDIDPath := fmt.Sprintf("https://ssi-service.com/v1/dids/key/%s", createdID)
 		req = httptest.NewRequest(http.MethodGet, getDIDPath, nil)
-		w = httptest.NewRecorder()
 
 		// good params
 		goodParams := map[string]string{
@@ -239,6 +249,94 @@ func TestDIDAPI(t *testing.T) {
 		assert.NoError(tt, err)
 		assert.Equal(tt, createdID, resp.DID.ID)
 	})
+
+	t.Run("Test Get DIDs By Method", func(tt *testing.T) {
+		bolt, err := storage.NewBoltDB()
+
+		// remove the db file after the test
+		tt.Cleanup(func() {
+			_ = bolt.Close()
+			_ = os.Remove(storage.DBFile)
+		})
+
+		_, keyStore := testKeyStore(tt, bolt)
+		didService := testDIDRouter(tt, bolt, keyStore)
+
+		// get DIDs by method
+		req := httptest.NewRequest(http.MethodGet, "https://ssi-service.com/v1/dids/bad", nil)
+		w := httptest.NewRecorder()
+
+		// bad params
+		badParams := map[string]string{
+			"method": "bad",
+		}
+		err = didService.GetDIDsByMethod(newRequestContextWithParams(badParams), w, req)
+		assert.Error(tt, err)
+		assert.Contains(tt, err.Error(), "could not get DIDs for method: bad")
+
+		// good method
+		goodParams := map[string]string{
+			"method": "key",
+		}
+		err = didService.GetDIDsByMethod(newRequestContextWithParams(goodParams), w, req)
+		assert.NoError(tt, err)
+		var gotDIDs router.GetDIDByMethodResponse
+		err = json.NewDecoder(w.Body).Decode(&gotDIDs)
+		assert.NoError(tt, err)
+		assert.Empty(tt, gotDIDs)
+
+		// reset recorder between calls
+		w.Flush()
+
+		// store two DIDs
+		createDIDRequest := router.CreateDIDByMethodRequest{KeyType: crypto.Ed25519}
+		requestReader := newRequestValue(tt, createDIDRequest)
+		params := map[string]string{"method": "key"}
+		req = httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/dids/key", requestReader)
+
+		err = didService.CreateDIDByMethod(newRequestContextWithParams(params), w, req)
+		assert.NoError(tt, err)
+
+		var createdDID router.CreateDIDByMethodResponse
+		err = json.NewDecoder(w.Body).Decode(&createdDID)
+		assert.NoError(tt, err)
+
+		// reset recorder between calls
+		w.Flush()
+
+		requestReader = newRequestValue(tt, createDIDRequest)
+		req = httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/dids/key", requestReader)
+
+		err = didService.CreateDIDByMethod(newRequestContextWithParams(params), w, req)
+		assert.NoError(tt, err)
+		
+		var createdDID2 router.CreateDIDByMethodResponse
+		err = json.NewDecoder(w.Body).Decode(&createdDID2)
+		assert.NoError(tt, err)
+
+		// reset recorder between calls
+		w.Flush()
+
+		// get all dids for method
+
+		req = httptest.NewRequest(http.MethodGet, "https://ssi-service.com/v1/dids/key", requestReader)
+		err = didService.GetDIDsByMethod(newRequestContextWithParams(params), w, req)
+		assert.NoError(tt, err)
+
+		var gotDIDsResponse router.GetDIDsByMethodResponse
+		err = json.NewDecoder(w.Body).Decode(&gotDIDsResponse)
+		assert.NoError(tt, err)
+
+		knownDIDs := map[string]bool{createdDID.DID.ID: true, createdDID2.DID.ID: true}
+		for _, did := range gotDIDsResponse.DIDs {
+			if _, ok := knownDIDs[did.ID]; !ok {
+				tt.Error("got unknown DID")
+			} else {
+				delete(knownDIDs, did.ID)
+			}
+		}
+		assert.Len(tt, knownDIDs, 0)
+	})
 }
 
 func TestSchemaAPI(t *testing.T) {