Skip to content

Commit

Permalink
Merge pull request #60 from bstasyszyn/54-2
Browse files Browse the repository at this point in the history
feat: Implement Sidetree REST API for DID documents
  • Loading branch information
bstasyszyn authored Oct 28, 2019
2 parents c9bbc39 + 238106c commit a14a3c6
Show file tree
Hide file tree
Showing 6 changed files with 323 additions and 1 deletion.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[![Release](https://img.shields.io/github/release/trustbloc/sidetree-core-go.svg?style=flat-square)](https://github.com/trustbloc/sidetree-core-go/releases/latest)
[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://raw.githubusercontent.com/trustbloc/sidetree-core-go/master/LICENSE)

[![Build Status](https://dev.azure.com/trustbloc/sidetree-core-go/_apis/build/status/trustbloc.sidetree-core-go?branchName=master)](https://dev.azure.com/trustbloc/sidetree-core-go/_build/latest?definitionId=2&branchName=master)
[![Build Status](https://dev.azure.com/trustbloc/sidetree/_apis/build/status/trustbloc.sidetree-core-go?branchName=master)](https://dev.azure.com/trustbloc/sidetree/_build/latest?definitionId=12&branchName=master)
[![GolangCI](https://golangci.com/badges/github.com/trustbloc/sidetree-core-go.svg)](https://golangci.com/r/github.com/trustbloc/sidetree-core-go)
[![codecov](https://codecov.io/gh/trustbloc/sidetree-core-go/branch/master/graph/badge.svg)](https://codecov.io/gh/trustbloc/sidetree-core-go)
[![Go Report Card](https://goreportcard.com/badge/github.com/trustbloc/sidetree-core-go?style=flat-square)](https://goreportcard.com/report/github.com/trustbloc/sidetree-core-go)
Expand Down
46 changes: 46 additions & 0 deletions pkg/restapi/diddochandler/resolvehandler.go
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)
}
30 changes: 30 additions & 0 deletions pkg/restapi/diddochandler/resolvehandler_test.go
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")
}
148 changes: 148 additions & 0 deletions pkg/restapi/diddochandler/restapi_test.go
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))
}
}
51 changes: 51 additions & 0 deletions pkg/restapi/diddochandler/updatehandler.go
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)
}
47 changes: 47 additions & 0 deletions pkg/restapi/diddochandler/updatehandler_test.go
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)
})
}

0 comments on commit a14a3c6

Please sign in to comment.