-
Notifications
You must be signed in to change notification settings - Fork 17
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #60 from bstasyszyn/54-2
feat: Implement Sidetree REST API for DID documents
- Loading branch information
Showing
6 changed files
with
323 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
/* | ||
Copyright SecureKey Technologies Inc. All Rights Reserved. | ||
SPDX-License-Identifier: Apache-2.0 | ||
*/ | ||
|
||
package diddochandler | ||
|
||
import ( | ||
"net/http" | ||
|
||
"github.com/trustbloc/sidetree-core-go/pkg/restapi/common" | ||
"github.com/trustbloc/sidetree-core-go/pkg/restapi/dochandler" | ||
) | ||
|
||
// ResolveHandler resolves DID documents | ||
type ResolveHandler struct { | ||
*dochandler.ResolveHandler | ||
} | ||
|
||
// NewResolveHandler returns a new DID document resolve handler | ||
func NewResolveHandler(resolver dochandler.Resolver) *ResolveHandler { | ||
return &ResolveHandler{ | ||
ResolveHandler: dochandler.NewResolveHandler(resolver), | ||
} | ||
} | ||
|
||
// Path returns the context path | ||
func (o *ResolveHandler) Path() string { | ||
return Path + "/{id}" | ||
} | ||
|
||
// Method returns the HTTP method | ||
func (o *ResolveHandler) Method() string { | ||
return http.MethodGet | ||
} | ||
|
||
// Handler returns the handler | ||
func (o *ResolveHandler) Handler() common.HTTPRequestHandler { | ||
return o.Resolve | ||
} | ||
|
||
// Resolve resolves a DID document by ID or DID document | ||
func (o *ResolveHandler) Resolve(rw http.ResponseWriter, req *http.Request) { | ||
o.ResolveHandler.Resolve(rw, req) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
/* | ||
Copyright SecureKey Technologies Inc. All Rights Reserved. | ||
SPDX-License-Identifier: Apache-2.0 | ||
*/ | ||
|
||
package diddochandler | ||
|
||
import ( | ||
"net/http" | ||
"net/http/httptest" | ||
"testing" | ||
|
||
"github.com/stretchr/testify/require" | ||
"github.com/trustbloc/sidetree-core-go/pkg/mocks" | ||
) | ||
|
||
func TestResolveHandler_Resolve(t *testing.T) { | ||
docHandler := mocks.NewMockDocumentHandler().WithNamespace(namespace) | ||
handler := NewResolveHandler(docHandler) | ||
require.Equal(t, Path+"/{id}", handler.Path()) | ||
require.Equal(t, http.MethodGet, handler.Method()) | ||
require.NotNil(t, handler.Handler()) | ||
|
||
rw := httptest.NewRecorder() | ||
req := httptest.NewRequest(http.MethodGet, "/document", nil) | ||
handler.Resolve(rw, req) | ||
require.Equal(t, http.StatusBadRequest, rw.Code) | ||
require.Contains(t, rw.Body.String(), "must start with supported namespace") | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,148 @@ | ||
/* | ||
Copyright SecureKey Technologies Inc. All Rights Reserved. | ||
SPDX-License-Identifier: Apache-2.0 | ||
*/ | ||
|
||
package diddochandler | ||
|
||
import ( | ||
"bytes" | ||
"context" | ||
"encoding/json" | ||
"fmt" | ||
"io/ioutil" | ||
"net/http" | ||
"strings" | ||
"testing" | ||
|
||
"github.com/gorilla/mux" | ||
"github.com/stretchr/testify/require" | ||
"github.com/trustbloc/sidetree-core-go/pkg/mocks" | ||
"github.com/trustbloc/sidetree-core-go/pkg/restapi/common" | ||
"github.com/trustbloc/sidetree-core-go/pkg/restapi/model" | ||
) | ||
|
||
const ( | ||
url = "localhost:4656" | ||
clientURL = "http://" + url | ||
) | ||
|
||
const ( | ||
didID = namespace + "EiDOQXC2GnoVyHwIRbjhLx_cNc6vmZaS04SZjZdlLLAPRg==" | ||
) | ||
|
||
func TestRESTAPI(t *testing.T) { | ||
didDocHandler := mocks.NewMockDocumentHandler().WithNamespace(namespace) | ||
|
||
s := newRESTService( | ||
url, | ||
NewUpdateHandler(didDocHandler), | ||
NewResolveHandler(didDocHandler), | ||
) | ||
s.start() | ||
defer s.stop() | ||
|
||
t.Run("Create DID doc", func(t *testing.T) { | ||
request := &model.Request{} | ||
err := json.Unmarshal([]byte(createRequest), request) | ||
require.NoError(t, err) | ||
|
||
resp, err := httpPut(t, clientURL+Path, request) | ||
require.NoError(t, err) | ||
require.NotNil(t, resp) | ||
require.NotNil(t, resp.Body) | ||
|
||
doc, ok := resp.Body.(map[string]interface{}) | ||
require.True(t, ok) | ||
require.Equal(t, didID, doc["id"]) | ||
}) | ||
t.Run("Resolve DID doc", func(t *testing.T) { | ||
resp, err := httpGet(t, clientURL+Path+"/"+didID) | ||
require.NoError(t, err) | ||
require.NotNil(t, resp) | ||
require.NotNil(t, resp.Body) | ||
|
||
doc, ok := resp.Body.(map[string]interface{}) | ||
require.True(t, ok) | ||
require.Equal(t, didID, doc["id"]) | ||
}) | ||
} | ||
|
||
// httpPut sends a regular POST request to the sidetree-node | ||
// - If post request has operation "create" then return sidetree document else no response | ||
func httpPut(t *testing.T, url string, req *model.Request) (*model.Response, error) { | ||
client := &http.Client{} | ||
b, err := json.Marshal(req) | ||
require.NoError(t, err) | ||
|
||
httpReq, err := http.NewRequest("POST", url, bytes.NewReader(b)) | ||
require.NoError(t, err) | ||
|
||
httpReq.Header.Set("Content-Type", "application/json") | ||
|
||
resp, err := client.Do(httpReq) | ||
require.NoError(t, err) | ||
|
||
return handleHttpResp(t, resp) | ||
} | ||
|
||
// httpGet send a regular GET request to the sidetree-node and expects 'side tree document' argument as a response | ||
func httpGet(t *testing.T, url string) (*model.Response, error) { | ||
client := &http.Client{} | ||
resp, err := client.Get(url) | ||
require.NoError(t, err) | ||
return handleHttpResp(t, resp) | ||
} | ||
|
||
func handleHttpResp(t *testing.T, resp *http.Response) (*model.Response, error) { | ||
if status := resp.StatusCode; status != http.StatusOK { | ||
r := &model.Error{} | ||
decode(t, resp, r) | ||
return nil, fmt.Errorf(r.Message) | ||
} | ||
|
||
r := &model.Response{} | ||
decode(t, resp, r) | ||
return r, nil | ||
} | ||
|
||
func decode(t *testing.T, response *http.Response, v interface{}) { | ||
respBytes, err := ioutil.ReadAll(response.Body) | ||
require.NoError(t, err) | ||
err = json.NewDecoder(strings.NewReader(string(respBytes))).Decode(v) | ||
require.NoError(t, err) | ||
} | ||
|
||
type restService struct { | ||
httpServer *http.Server | ||
} | ||
|
||
func newRESTService(url string, handlers ...common.HTTPHandler) *restService { | ||
router := mux.NewRouter() | ||
for _, handler := range handlers { | ||
router.HandleFunc(handler.Path(), handler.Handler()).Methods(handler.Method()) | ||
} | ||
return &restService{ | ||
httpServer: &http.Server{ | ||
Addr: url, | ||
Handler: router, | ||
}, | ||
} | ||
} | ||
|
||
func (s *restService) start() { | ||
go func() { | ||
err := s.httpServer.ListenAndServe() | ||
if err != nil && err != http.ErrServerClosed { | ||
panic(fmt.Sprintf("failed to start Sidetree REST service on [%s]: %s", s.httpServer.Addr, err)) | ||
} | ||
}() | ||
} | ||
|
||
func (s *restService) stop() { | ||
err := s.httpServer.Shutdown(context.Background()) | ||
if err != nil { | ||
panic(fmt.Sprintf("failed to stop Sidetree REST service on [%s]: %s", s.httpServer.Addr, err)) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
/* | ||
Copyright SecureKey Technologies Inc. All Rights Reserved. | ||
SPDX-License-Identifier: Apache-2.0 | ||
*/ | ||
|
||
package diddochandler | ||
|
||
import ( | ||
"net/http" | ||
|
||
"github.com/trustbloc/sidetree-core-go/pkg/restapi/common" | ||
"github.com/trustbloc/sidetree-core-go/pkg/restapi/dochandler" | ||
) | ||
|
||
const ( | ||
// Path is the context path for the DID document REST service | ||
Path = "/document" | ||
) | ||
|
||
// UpdateHandler handles the creation and update of DID documents | ||
type UpdateHandler struct { | ||
*dochandler.UpdateHandler | ||
} | ||
|
||
// NewUpdateHandler returns a new DID document update handler | ||
func NewUpdateHandler(processor dochandler.Processor) *UpdateHandler { | ||
return &UpdateHandler{ | ||
UpdateHandler: dochandler.NewUpdateHandler(processor), | ||
} | ||
} | ||
|
||
// Path returns the context path | ||
func (h *UpdateHandler) Path() string { | ||
return Path | ||
} | ||
|
||
// Method returns the HTTP method | ||
func (h *UpdateHandler) Method() string { | ||
return http.MethodPost | ||
} | ||
|
||
// Handler returns the handler | ||
func (h *UpdateHandler) Handler() common.HTTPRequestHandler { | ||
return h.Update | ||
} | ||
|
||
// Update updates/creates a DID document. | ||
func (h *UpdateHandler) Update(rw http.ResponseWriter, req *http.Request) { | ||
h.UpdateHandler.Update(rw, req) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
/* | ||
Copyright SecureKey Technologies Inc. All Rights Reserved. | ||
SPDX-License-Identifier: Apache-2.0 | ||
*/ | ||
|
||
package diddochandler | ||
|
||
import ( | ||
"bytes" | ||
"net/http" | ||
"net/http/httptest" | ||
"testing" | ||
|
||
"github.com/stretchr/testify/require" | ||
"github.com/trustbloc/sidetree-core-go/pkg/mocks" | ||
) | ||
|
||
const ( | ||
namespace string = "did:sidetree:" | ||
|
||
createRequest = `{ | ||
"header": { | ||
"operation": "create", | ||
"kid": "#key1", | ||
"alg": "ES256K" | ||
}, | ||
"payload": "ewogICJAY29udGV4dCI6ICJodHRwczovL3czaWQub3JnL2RpZC92MSIsCiAgInB1YmxpY0tleSI6IFt7CiAgICAiaWQiOiAiI2tleTEiLAogICAgInR5cGUiOiAiU2VjcDI1NmsxVmVyaWZpY2F0aW9uS2V5MjAxOCIsCiAgICAicHVibGljS2V5SGV4IjogIjAyZjQ5ODAyZmIzZTA5YzZkZDQzZjE5YWE0MTI5M2QxZTBkYWQwNDRiNjhjZjgxY2Y3MDc5NDk5ZWRmZDBhYTlmMSIKICB9XSwKICAic2VydmljZSI6IFt7CiAgICAiaWQiOiAiSWRlbnRpdHlIdWIiLAogICAgInR5cGUiOiAiSWRlbnRpdHlIdWIiLAogICAgInNlcnZpY2VFbmRwb2ludCI6IHsKICAgICAgIkBjb250ZXh0IjogInNjaGVtYS5pZGVudGl0eS5mb3VuZGF0aW9uL2h1YiIsCiAgICAgICJAdHlwZSI6ICJVc2VyU2VydmljZUVuZHBvaW50IiwKICAgICAgImluc3RhbmNlIjogWyJkaWQ6YmFyOjQ1NiIsICJkaWQ6emF6Ojc4OSJdCiAgICB9CiAgfV0KfQo=", | ||
"signature": "mAJp4ZHwY5UMA05OEKvoZreRo0XrYe77s3RLyGKArG85IoBULs4cLDBtdpOToCtSZhPvCC2xOUXMGyGXDmmEHg" | ||
} | ||
` | ||
) | ||
|
||
func TestUpdateHandler_Update(t *testing.T) { | ||
t.Run("Success", func(t *testing.T) { | ||
docHandler := mocks.NewMockDocumentHandler().WithNamespace(namespace) | ||
handler := NewUpdateHandler(docHandler) | ||
require.Equal(t, Path, handler.Path()) | ||
require.Equal(t, http.MethodPost, handler.Method()) | ||
require.NotNil(t, handler.Handler()) | ||
|
||
rw := httptest.NewRecorder() | ||
req := httptest.NewRequest(http.MethodPost, "/document", bytes.NewReader([]byte(createRequest))) | ||
handler.Update(rw, req) | ||
require.Equal(t, http.StatusOK, rw.Code) | ||
}) | ||
} |