Skip to content

Commit

Permalink
feat: Implement Sidetree REST API for DID documents
Browse files Browse the repository at this point in the history
Add REST API endpoints according to the Sidetree spec for DID documents.

closes #54

Signed-off-by: Bob Stasyszyn <[email protected]>
  • Loading branch information
bstasyszyn committed Oct 28, 2019
1 parent c9bbc39 commit 238106c
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 238106c

Please sign in to comment.