From 922f6a7b419d81bd2f48c01d3da2ed603decbc69 Mon Sep 17 00:00:00 2001 From: mitchelljfs Date: Sat, 14 Jul 2018 14:58:30 -0700 Subject: [PATCH] Added 100% coverage to whats written so far; travis build script; debugged potential issues with testing --- .travis.yml | 10 ++++ Gopkg.lock | 34 ++++++++++++- router.go | 69 +++++++++++++++----------- router_test.go | 132 +++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 216 insertions(+), 29 deletions(-) create mode 100644 .travis.yml create mode 100644 router_test.go diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..387cd04 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,10 @@ +language: go +go: + - "1.x" + - "1.8" + - "1.10.x" + - master +install: + - "dep ensure" +build: + - "go test -v ./..." diff --git a/Gopkg.lock b/Gopkg.lock index 260979e..95b28b8 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -13,9 +13,41 @@ revision = "4d30d0ff60440c2d0480a15747c96ee71c3c53d4" version = "v1.2.0" +[[projects]] + branch = "master" + name = "github.com/gopherjs/gopherjs" + packages = ["js"] + revision = "0892b62f0d9fb5857760c3cfca837207185117ee" + +[[projects]] + name = "github.com/jtolds/gls" + packages = ["."] + revision = "77f18212c9c7edc9bd6a33d383a7b545ce62f064" + version = "v4.2.1" + +[[projects]] + name = "github.com/smartystreets/assertions" + packages = [ + ".", + "internal/go-render/render", + "internal/oglematchers" + ] + revision = "7678a5452ebea5b7090a6b163f844c133f523da2" + version = "1.8.3" + +[[projects]] + name = "github.com/smartystreets/goconvey" + packages = [ + "convey", + "convey/gotest", + "convey/reporting" + ] + revision = "9e8dc3f972df6c8fcc0375ef492c24d0bb204857" + version = "1.6.3" + [solve-meta] analyzer-name = "dep" analyzer-version = 1 - inputs-digest = "35d3fdde995b8cf314934259f07ffb089e30092e11fb8e404ea398ce1c76a431" + inputs-digest = "751d4fc4b7be23b18796aed082f5e83d9ba309c184255d4c63d8225cc0048152" solver-name = "gps-cdcl" solver-version = 1 diff --git a/router.go b/router.go index e9af2a0..52545fc 100644 --- a/router.go +++ b/router.go @@ -14,29 +14,34 @@ const ( post = http.MethodPost get = http.MethodGet put = http.MethodPut + patch = http.MethodPatch delete = http.MethodDelete ) -// HandlerRequest ... -type HandlerRequest struct { +// APIGRequest is used as the input of handler functions. +// The Claims, Path, and QryStr will be populated by the the APIGatewayProxyRequest. +// The Request itself is also passed through if you need further access. +type APIGRequest struct { Claims map[string]interface{} Path map[string]string QryStr map[string]string Request *events.APIGatewayProxyRequest } -// HandlerResponse ... -type HandlerResponse struct { +// APIGResponse is used as the output of handler functions. +// Populate Status and Body with your http response or populate Err with your error. +type APIGResponse struct { Status int Body []byte Err error } -// Handler ... -type Handler func(req *HandlerRequest, res *HandlerResponse) +// APIGHandler is the interface a handler function must implement to be used +// with Get, Post, Put, Patch, and Delete. +type APIGHandler func(req *APIGRequest, res *APIGResponse) -// Router ... -type Router struct { +// APIGRouter is the object that handlers build upon and is used in the end to respond. +type APIGRouter struct { request *events.APIGatewayProxyRequest endpoints map[string]*radix.Tree params map[string]string @@ -45,14 +50,15 @@ type Router struct { // NOTE: Begin router methods. -// New ... -func New(r *events.APIGatewayProxyRequest, svcprefix string) *Router { - return &Router{ +// NewAPIGRouter creates a new router using the request and a prefix to strip from your incoming requests. +func NewAPIGRouter(r *events.APIGatewayProxyRequest, svcprefix string) *APIGRouter { + return &APIGRouter{ request: r, endpoints: map[string]*radix.Tree{ post: radix.New(), get: radix.New(), put: radix.New(), + patch: radix.New(), delete: radix.New(), }, params: map[string]string{}, @@ -60,28 +66,33 @@ func New(r *events.APIGatewayProxyRequest, svcprefix string) *Router { } } -// Get ... -func (r *Router) Get(route string, handler Handler) { +// Get creates a new get endpoint. +func (r *APIGRouter) Get(route string, handler APIGHandler) { r.addEndpoint(get, route, handler) } -// Post ... -func (r *Router) Post(route string, handler Handler) { +// Post creates a new post endpoint. +func (r *APIGRouter) Post(route string, handler APIGHandler) { r.addEndpoint(post, route, handler) } -// Put ... -func (r *Router) Put(route string, handler Handler) { +// Put creates a new put endpoint. +func (r *APIGRouter) Put(route string, handler APIGHandler) { r.addEndpoint(put, route, handler) } -// Delete ... -func (r *Router) Delete(route string, handler Handler) { +// Patch creates a new patch endpoint +func (r *APIGRouter) Patch(route string, handler APIGHandler) { + r.addEndpoint(patch, route, handler) +} + +// Delete creates a new delete endpoint. +func (r *APIGRouter) Delete(route string, handler APIGHandler) { r.addEndpoint(delete, route, handler) } -// Respond ... -func (r *Router) Respond() events.APIGatewayProxyResponse { +// Respond returns an APIGatewayProxyResponse to respond to the lambda request. +func (r *APIGRouter) Respond() events.APIGatewayProxyResponse { var ( handlerInterface interface{} ok bool @@ -109,15 +120,17 @@ func (r *Router) Respond() events.APIGatewayProxyResponse { return response } - handler := handlerInterface.(Handler) + handler := handlerInterface.(APIGHandler) - req := &HandlerRequest{ - Claims: r.request.RequestContext.Authorizer["claims"].(map[string]interface{}), + req := &APIGRequest{ Path: r.request.PathParameters, QryStr: r.request.QueryStringParameters, Request: r.request, } - res := &HandlerResponse{} + if r.request.RequestContext.Authorizer["claims"] != nil { + req.Claims = r.request.RequestContext.Authorizer["claims"].(map[string]interface{}) + } + res := &APIGResponse{} handler(req, res) status, respbody, err := res.deconstruct() @@ -147,11 +160,11 @@ func stripSlashesAndSplit(s string) []string { return strings.Split(s, "/") } -func (res *HandlerResponse) deconstruct() (int, []byte, error) { +func (res *APIGResponse) deconstruct() (int, []byte, error) { return res.Status, res.Body, res.Err } -func (r *Router) addEndpoint(method string, route string, handler Handler) { +func (r *APIGRouter) addEndpoint(method string, route string, handler APIGHandler) { if _, overwrite := r.endpoints[method].Insert(route, handler); overwrite { panic("endpoint already existent") } @@ -163,5 +176,5 @@ func (r *Router) addEndpoint(method string, route string, handler Handler) { } } - log.Printf("router: %+v", *r) + log.Printf("endpoint: %+v %+v", method, route) } diff --git a/router_test.go b/router_test.go new file mode 100644 index 0000000..ceaf452 --- /dev/null +++ b/router_test.go @@ -0,0 +1,132 @@ +package lambdarouter + +import ( + "errors" + "log" + "net/http" + "testing" + + "github.com/aws/aws-lambda-go/events" + . "github.com/smartystreets/goconvey/convey" +) + +func TestRouterSpec(t *testing.T) { + + Convey("Given an instantiated router", t, func() { + request := events.APIGatewayProxyRequest{} + rtr := NewAPIGRouter(&request, "shipping") + + Convey("When the handler func does NOT return an error", func() { + hdlrfunc := func(req *APIGRequest, res *APIGResponse) { + log.Printf("claims: %+v", req.Claims) + res.Status = http.StatusOK + res.Body = []byte("hello") + res.Err = nil + + } + + Convey("And a Get handler expecting the pattern /orders/filter/by_user/{id} is defined", func() { + rtr.Get("/orders/filter/by_user/{id}", hdlrfunc) + rtr.Post("/orders", func(req *APIGRequest, res *APIGResponse) {}) + rtr.Put("/orders", func(req *APIGRequest, res *APIGResponse) {}) + rtr.Patch("/orders", func(req *APIGRequest, res *APIGResponse) {}) + rtr.Delete("/orders/{id}", func(req *APIGRequest, res *APIGResponse) {}) + + Convey("And the request matches the pattern and the path params are filled", func() { + request.HTTPMethod = http.MethodGet + request.Path = "/shipping/orders/filter/by_user/4d50ff90-66e3-4047-bf37-0ca25837e41d" + request.PathParameters = map[string]string{ + "id": "4d50ff90-66e3-4047-bf37-0ca25837e41d", + } + request.RequestContext.Authorizer = map[string]interface{}{ + "claims": map[string]interface{}{ + "cognito:username": "mitchell", + }, + } + + Convey("The router will return the expected status and body", func() { + response := rtr.Respond() + + So(response.StatusCode, ShouldEqual, http.StatusOK) + So(response.Body, ShouldEqual, "hello") + }) + }) + + Convey("And the request does NOT match the pattern", func() { + request.HTTPMethod = http.MethodGet + request.Path = "/orders/filter" + + Convey("The router will return and error body and a not found status", func() { + response := rtr.Respond() + + So(response.StatusCode, ShouldEqual, http.StatusNotFound) + So(response.Body, ShouldEqual, "{\"error\":\"no route matching path found\"}") + }) + }) + + Convey("And a Get handler expecting the pattern /orders/filter/by_user/{id} is defined AGAIN", func() { + So(func() { + rtr.Get("/orders/filter/by_user/{id}", hdlrfunc) + }, ShouldPanicWith, "endpoint already existent") + }) + }) + + }) + + Convey("When the handler func does return a record not found", func() { + hdlrfunc := func(req *APIGRequest, res *APIGResponse) { + res.Status = http.StatusBadRequest + res.Body = []byte("hello") + res.Err = errors.New("record not found") + + } + + Convey("And a Get handler expecting the pattern /orders/filter/by_user/{id} is defined", func() { + rtr.Get("/orders/filter/by_user/{id}", hdlrfunc) + + Convey("And the request matches the pattern and the path params are filled", func() { + request.HTTPMethod = http.MethodGet + request.Path = "/shipping/orders/filter/by_user/4d50ff90-66e3-4047-bf37-0ca25837e41d" + request.PathParameters = map[string]string{ + "id": "4d50ff90-66e3-4047-bf37-0ca25837e41d", + } + + Convey("The router will return the expected status and body", func() { + response := rtr.Respond() + + So(response.StatusCode, ShouldEqual, http.StatusNoContent) + So(response.Body, ShouldEqual, "{\"error\":\"record not found\"}") + }) + }) + }) + }) + + Convey("When the handler func does return a status < 400", func() { + hdlrfunc := func(req *APIGRequest, res *APIGResponse) { + res.Status = http.StatusOK + res.Body = []byte("hello") + res.Err = errors.New("bad request") + + } + + Convey("And a Get handler expecting the pattern /orders/filter/by_user/{id} is defined", func() { + rtr.Get("/orders/filter/by_user/{id}", hdlrfunc) + + Convey("And the request matches the pattern and the path params are filled", func() { + request.HTTPMethod = http.MethodGet + request.Path = "/shipping/orders/filter/by_user/4d50ff90-66e3-4047-bf37-0ca25837e41d" + request.PathParameters = map[string]string{ + "id": "4d50ff90-66e3-4047-bf37-0ca25837e41d", + } + + Convey("The router will return the expected status and body", func() { + response := rtr.Respond() + + So(response.StatusCode, ShouldEqual, http.StatusBadRequest) + So(response.Body, ShouldEqual, "{\"error\":\"bad request\"}") + }) + }) + }) + }) + }) +}