Skip to content

Commit

Permalink
Merge f80de2f into 36f0712
Browse files Browse the repository at this point in the history
  • Loading branch information
mthenw committed Feb 14, 2018
2 parents 36f0712 + f80de2f commit b27ad75
Show file tree
Hide file tree
Showing 30 changed files with 914 additions and 370 deletions.
3 changes: 2 additions & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ script:
- gometalinter --install --force
- gometalinter --vendor --fast --disable=gotype --disable=vetshadow --disable=gas --skip=mock ./...
- go get github.com/mattn/goveralls
- goveralls -race -service=travis-ci -ignore=*/mock/*,*/*/mock/*
- goveralls -race -service=travis-ci -ignore=./mock/*,./router/mock/*,
- go test ./tests -tags=integration
after_success:
- test -n "$TRAVIS_TAG" && curl -sL https://git.io/goreleaser | bash
notifications:
Expand Down
5 changes: 2 additions & 3 deletions function/function.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,12 @@ import (
// Function represents a function deployed on one of the supported providers.
type Function struct {
ID ID `json:"functionId" validate:"required,functionid"`
Space string `json:"space" validate:"required,space"`
Provider *Provider `json:"provider" validate:"required"`
}

// Functions is an array of functions.
type Functions struct {
Functions []*Function `json:"functions"`
}
type Functions []*Function

// ID uniquely identifies a function.
type ID string
Expand Down
8 changes: 4 additions & 4 deletions function/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ package function
// Service represents service for managing functions.
type Service interface {
RegisterFunction(fn *Function) (*Function, error)
UpdateFunction(fn *Function) (*Function, error)
GetFunction(id ID) (*Function, error)
GetAllFunctions() ([]*Function, error)
DeleteFunction(id ID) error
UpdateFunction(space string, fn *Function) (*Function, error)
GetFunction(space string, id ID) (*Function, error)
GetFunctions(space string) (Functions, error)
DeleteFunction(space string, id ID) error
}
7 changes: 7 additions & 0 deletions httpapi/error.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,10 @@ type ErrMalformedJSON Error
func NewErrMalformedJSON(err error) *ErrMalformedJSON {
return &ErrMalformedJSON{fmt.Sprintf("Malformed JSON payload: %s.", err.Error())}
}

// ErrSpaceMismatch occurs when function couldn't been found in the discovery.
type ErrSpaceMismatch struct{}

func (e ErrSpaceMismatch) Error() string {
return "Object space doesn't match space specified in the URL."
}
46 changes: 32 additions & 14 deletions httpapi/httpapi.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,27 +17,37 @@ type HTTPAPI struct {
Subscriptions subscription.Service
}

// FunctionsResponse is a HTTPAPI JSON response containing functions.
type FunctionsResponse struct {
Functions function.Functions `json:"functions"`
}

// SubscriptionsResponse is a HTTPAPI JSON response containing subscriptions.
type SubscriptionsResponse struct {
Subscriptions subscription.Subscriptions `json:"subscriptions"`
}

// RegisterRoutes register HTTP API routes
func (h HTTPAPI) RegisterRoutes(router *httprouter.Router) {
router.GET("/v1/status", func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {})
router.Handler("GET", "/metrics", promhttp.Handler())

router.GET("/v1/functions", h.getFunctions)
router.GET("/v1/functions/:space/:id", h.getFunction)
router.GET("/v1/functions/:space", h.getFunctions)
router.POST("/v1/functions", h.registerFunction)
router.GET("/v1/functions/:id", h.getFunction)
router.PUT("/v1/functions/:id", h.updateFunction)
router.DELETE("/v1/functions/:id", h.deleteFunction)
router.PUT("/v1/functions/:space/:id", h.updateFunction)
router.DELETE("/v1/functions/:space/:id", h.deleteFunction)

router.POST("/v1/subscriptions", h.createSubscription)
router.DELETE("/v1/subscriptions/*subscriptionID", h.deleteSubscription)
router.GET("/v1/subscriptions", h.getSubscriptions)
router.DELETE("/v1/subscriptions/:space/*subscriptionID", h.deleteSubscription)
router.GET("/v1/subscriptions/:space", h.getSubscriptions)
}

func (h HTTPAPI) getFunction(w http.ResponseWriter, r *http.Request, params httprouter.Params) {
w.Header().Set("Content-Type", "application/json")
encoder := json.NewEncoder(w)

fn, err := h.Functions.GetFunction(function.ID(params.ByName("id")))
fn, err := h.Functions.GetFunction(params.ByName("space"), function.ID(params.ByName("id")))
if err != nil {
if _, ok := err.(*function.ErrFunctionNotFound); ok {
w.WriteHeader(http.StatusNotFound)
Expand All @@ -55,12 +65,12 @@ func (h HTTPAPI) getFunctions(w http.ResponseWriter, r *http.Request, params htt
w.Header().Set("Content-Type", "application/json")
encoder := json.NewEncoder(w)

fns, err := h.Functions.GetAllFunctions()
fns, err := h.Functions.GetFunctions(params.ByName("space"))
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
encoder.Encode(&Response{Errors: []Error{{Message: err.Error()}}})
} else {
encoder.Encode(&function.Functions{Functions: fns})
encoder.Encode(&FunctionsResponse{fns})
}
}

Expand Down Expand Up @@ -91,6 +101,7 @@ func (h HTTPAPI) registerFunction(w http.ResponseWriter, r *http.Request, params
return
}

w.WriteHeader(http.StatusCreated)
encoder.Encode(output)
}

Expand All @@ -107,8 +118,15 @@ func (h HTTPAPI) updateFunction(w http.ResponseWriter, r *http.Request, params h
return
}

if params.ByName("space") != fn.Space {
w.WriteHeader(http.StatusBadRequest)
responseErr := &ErrSpaceMismatch{}
encoder.Encode(&Response{Errors: []Error{{Message: responseErr.Error()}}})
return
}

fn.ID = function.ID(params.ByName("id"))
output, err := h.Functions.UpdateFunction(fn)
output, err := h.Functions.UpdateFunction(params.ByName("space"), fn)
if err != nil {
if _, ok := err.(*function.ErrFunctionValidation); ok {
w.WriteHeader(http.StatusBadRequest)
Expand All @@ -129,7 +147,7 @@ func (h HTTPAPI) deleteFunction(w http.ResponseWriter, r *http.Request, params h
w.Header().Set("Content-Type", "application/json")
encoder := json.NewEncoder(w)

err := h.Functions.DeleteFunction(function.ID(params.ByName("id")))
err := h.Functions.DeleteFunction(params.ByName("space"), function.ID(params.ByName("id")))
if err != nil {
if _, ok := err.(*function.ErrFunctionNotFound); ok {
w.WriteHeader(http.StatusNotFound)
Expand Down Expand Up @@ -185,7 +203,7 @@ func (h HTTPAPI) deleteSubscription(w http.ResponseWriter, r *http.Request, para
segments := strings.Split(r.URL.RawPath, "/")
sid := segments[len(segments)-1]

err := h.Subscriptions.DeleteSubscription(subscription.ID(sid))
err := h.Subscriptions.DeleteSubscription(params.ByName("space"), subscription.ID(sid))
if err != nil {
if _, ok := err.(*subscription.ErrSubscriptionNotFound); ok {
w.WriteHeader(http.StatusNotFound)
Expand All @@ -202,11 +220,11 @@ func (h HTTPAPI) getSubscriptions(w http.ResponseWriter, r *http.Request, params
w.Header().Set("Content-Type", "application/json")
encoder := json.NewEncoder(w)

subs, err := h.Subscriptions.GetAllSubscriptions()
subs, err := h.Subscriptions.GetSubscriptions(params.ByName("space"))
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
encoder.Encode(&Response{Errors: []Error{{Message: err.Error()}}})
} else {
encoder.Encode(&subscription.Subscriptions{Subscriptions: subs})
encoder.Encode(&SubscriptionsResponse{subs})
}
}
218 changes: 218 additions & 0 deletions httpapi/httpapi_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
package httpapi_test

import (
"bytes"
"encoding/json"
"errors"
"net/http"
"net/http/httptest"
"testing"

"github.com/golang/mock/gomock"
"github.com/julienschmidt/httprouter"
"github.com/serverless/event-gateway/function"
"github.com/serverless/event-gateway/httpapi"
"github.com/serverless/event-gateway/mock"
"github.com/stretchr/testify/assert"
)

func TestGetFunction_OK(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
router, functions, _ := setup(ctrl)

returned := &function.Function{ID: function.ID("func1"), Space: "default"}
functions.EXPECT().GetFunction("default", function.ID("func1")).Return(returned, nil)

resp := httptest.NewRecorder()
req, _ := http.NewRequest(http.MethodGet, "/v1/functions/default/func1", nil)
router.ServeHTTP(resp, req)

fn := &function.Function{}
json.Unmarshal(resp.Body.Bytes(), fn)
assert.Equal(t, http.StatusOK, resp.Code)
assert.Equal(t, "default", fn.Space)
assert.Equal(t, function.ID("func1"), fn.ID)
}

func TestGetFunction_NotFound(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
router, functions, _ := setup(ctrl)

returned := &function.ErrFunctionNotFound{ID: function.ID("func1")}
functions.EXPECT().GetFunction("default", function.ID("func1")).Return(nil, returned)

resp := httptest.NewRecorder()
req, _ := http.NewRequest(http.MethodGet, "/v1/functions/default/func1", nil)
router.ServeHTTP(resp, req)

httpresp := &httpapi.Response{}
json.Unmarshal(resp.Body.Bytes(), httpresp)
assert.Equal(t, http.StatusNotFound, resp.Code)
assert.Equal(t, `Function "func1" not found.`, httpresp.Errors[0].Message)
}

func TestGetFunction_InternalError(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
router, functions, _ := setup(ctrl)

functions.EXPECT().GetFunction("default", function.ID("func1")).Return(nil, errors.New("processing failed"))

resp := httptest.NewRecorder()
req, _ := http.NewRequest(http.MethodGet, "/v1/functions/default/func1", nil)
router.ServeHTTP(resp, req)

httpresp := &httpapi.Response{}
json.Unmarshal(resp.Body.Bytes(), httpresp)
assert.Equal(t, http.StatusInternalServerError, resp.Code)
assert.Equal(t, "processing failed", httpresp.Errors[0].Message)
}

func TestGetFunctions_OK(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
router, functions, _ := setup(ctrl)

returned := function.Functions{{ID: function.ID("func1"), Space: "default"}}
functions.EXPECT().GetFunctions("default").Return(returned, nil)

resp := httptest.NewRecorder()
req, _ := http.NewRequest(http.MethodGet, "/v1/functions/default", nil)
router.ServeHTTP(resp, req)

fns := &httpapi.FunctionsResponse{}
json.Unmarshal(resp.Body.Bytes(), fns)
assert.Equal(t, http.StatusOK, resp.Code)
assert.Equal(t, "default", fns.Functions[0].Space)
assert.Equal(t, function.ID("func1"), fns.Functions[0].ID)
}

func TestGetFunctions_InternalError(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
router, functions, _ := setup(ctrl)

functions.EXPECT().GetFunctions("default").Return(nil, errors.New("processing failed"))

resp := httptest.NewRecorder()
req, _ := http.NewRequest(http.MethodGet, "/v1/functions/default", nil)
router.ServeHTTP(resp, req)

httpresp := &httpapi.Response{}
json.Unmarshal(resp.Body.Bytes(), httpresp)
assert.Equal(t, http.StatusInternalServerError, resp.Code)
assert.Equal(t, "processing failed", httpresp.Errors[0].Message)
}

func TestRegisterFunction_OK(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
router, functions, _ := setup(ctrl)

fn := &function.Function{
ID: function.ID("func1"),
Space: "test1",
Provider: &function.Provider{
Type: function.HTTPEndpoint,
URL: "http://example.com",
},
}
functions.EXPECT().RegisterFunction(fn).Return(fn, nil)

resp := httptest.NewRecorder()
payload := bytes.NewReader([]byte(`
{"functionID":"func1", "space":"test1", "provider":{"type":"http", "url":"http://example.com"}}
`))
req, _ := http.NewRequest(http.MethodPost, "/v1/functions", payload)
router.ServeHTTP(resp, req)

fn = &function.Function{}
json.Unmarshal(resp.Body.Bytes(), fn)
assert.Equal(t, http.StatusCreated, resp.Code)
assert.Equal(t, "application/json", resp.Header().Get("Content-Type"))
assert.Equal(t, function.ID("func1"), fn.ID)
assert.Equal(t, "test1", fn.Space)
}

func TestRegisterFunction_ValidationError(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
router, functions, _ := setup(ctrl)

fn := &function.Function{
ID: function.ID("func1"),
}
functions.EXPECT().RegisterFunction(fn).Return(nil, &function.ErrFunctionValidation{Message: "no provider"})

resp := httptest.NewRecorder()
payload := bytes.NewReader([]byte(`{"functionID":"func1"}}`))
req, _ := http.NewRequest(http.MethodPost, "/v1/functions", payload)
router.ServeHTTP(resp, req)

httpresp := &httpapi.Response{}
json.Unmarshal(resp.Body.Bytes(), httpresp)
assert.Equal(t, http.StatusBadRequest, resp.Code)
assert.Equal(t, `Function doesn't validate. Validation error: "no provider"`, httpresp.Errors[0].Message)
}

func TestRegisterFunction_AlreadyRegistered(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
router, functions, _ := setup(ctrl)

fn := &function.Function{
ID: function.ID("func1"),
}
functions.EXPECT().RegisterFunction(fn).Return(nil, &function.ErrFunctionAlreadyRegistered{ID: function.ID("func1")})

resp := httptest.NewRecorder()
payload := bytes.NewReader([]byte(`{"functionID":"func1"}}`))
req, _ := http.NewRequest(http.MethodPost, "/v1/functions", payload)
router.ServeHTTP(resp, req)

httpresp := &httpapi.Response{}
json.Unmarshal(resp.Body.Bytes(), httpresp)
assert.Equal(t, http.StatusBadRequest, resp.Code)
assert.Equal(t, `Function "func1" already registered.`, httpresp.Errors[0].Message)
}

func TestRegisterFunction_InternalError(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
router, functions, _ := setup(ctrl)

fn := &function.Function{
ID: function.ID("func1"),
}
functions.EXPECT().RegisterFunction(fn).Return(nil, errors.New("processing error"))

resp := httptest.NewRecorder()
payload := bytes.NewReader([]byte(`{"functionID":"func1"}}`))
req, _ := http.NewRequest(http.MethodPost, "/v1/functions", payload)
router.ServeHTTP(resp, req)

httpresp := &httpapi.Response{}
json.Unmarshal(resp.Body.Bytes(), httpresp)
assert.Equal(t, http.StatusInternalServerError, resp.Code)
assert.Equal(t, `processing error`, httpresp.Errors[0].Message)
}

func setup(ctrl *gomock.Controller) (
*httprouter.Router,
*mock.MockFunctionService,
*mock.MockSubscriptionService,
) {
router := httprouter.New()
functions := mock.NewMockFunctionService(ctrl)
subscriptions := mock.NewMockSubscriptionService(ctrl)

httpapi := &httpapi.HTTPAPI{
Functions: functions,
Subscriptions: subscriptions,
}
httpapi.RegisterRoutes(router)

return router, functions, subscriptions
}

0 comments on commit b27ad75

Please sign in to comment.