Skip to content

Commit

Permalink
add http adaptor for pact tests (GO-7710) (#13)
Browse files Browse the repository at this point in the history
  • Loading branch information
jfallis authored May 15, 2023
1 parent 21ac178 commit f4335d6
Show file tree
Hide file tree
Showing 5 changed files with 268 additions and 0 deletions.
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,5 +147,27 @@ The new version of eris handles errors differently and produce a different JSON
eris.Wrapf(err, "failed to send offers to user id: %v", userID)
```

### Pact HTTP provider testing

The `NewHTTPHandler` function can be used to create adaptors for `g8.APIGatewayProxyHandler` lambdas and serve HTTP for pact provider testing to aid engineers and verify that an API provider adheres to a number of pacts authored by its clients.

#### Example
```go
g8.NewHTTPHandler(LambdaHandlerEndpoints{
g8.LambdaHandler{
Handler: pact.ExampleGetStub,
Method: http.MethodGet,
Path: "/full/url/path/{var1}/{var2}",
PathParams: []string{"var1", "var2"},
},
g8.LambdaHandler{
Handler: pact.ExamplePostStub,
Method: http.MethodPost,
Path: "/another/full/url/path/{var1}",
PathParams: []string{"var1"},
},
}, 8080)
```

### Requirements
* Go 1.19+
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ require (
github.com/PaesslerAG/jsonpath v0.1.1
github.com/aws/aws-lambda-go v1.34.1
github.com/gaw508/lambda-proxy-http-adapter v0.1.0
github.com/go-chi/chi/v5 v5.0.8
github.com/google/uuid v1.3.0
github.com/newrelic/go-agent v3.19.0+incompatible
github.com/rotisserie/eris v0.5.4
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gaw508/lambda-proxy-http-adapter v0.1.0 h1:0QDBKYl4OJZ5YlNdXXqNuMUjBRVqxWwApP4hbxdTfdA=
github.com/gaw508/lambda-proxy-http-adapter v0.1.0/go.mod h1:8lk+YsNGsT8W5tjyznIFP1Y+ZtL/4SxwUPBfQAknucQ=
github.com/go-chi/chi/v5 v5.0.8 h1:lD+NLqFcAi1ovnVZpsnObHGW4xb4J8lNmoYVfECH1Y0=
github.com/go-chi/chi/v5 v5.0.8/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
Expand Down
166 changes: 166 additions & 0 deletions http.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
package g8

import (
"context"
"encoding/json"
"fmt"
"github.com/aws/aws-lambda-go/events"
"github.com/go-chi/chi/v5"
"io"
"net/http"
"strings"
)

const (
WELCOME_MESSAGE = "G8 HTTP server is running on port"
UNHANDLED_ERR_MESSAGE = "unhandled error: "
)

type LambdaHandlerEndpoints []LambdaHandler

type LambdaHandler struct {
Handler interface{}
Method string
Path string
PathParams []string
}

// NewHTTPHandler creates a new HTTP server that listens on the given port.
func NewHTTPHandler(lambdaEndpoints LambdaHandlerEndpoints, portNumber int) {
fmt.Printf("\n%s %d\n\n", WELCOME_MESSAGE, portNumber)
r := chi.NewRouter()
for _, l := range lambdaEndpoints {
r.MethodFunc(l.Method, l.Path, LambdaAdapter(l))
}
if err := http.ListenAndServe(fmt.Sprintf(":%d", portNumber), r); err != nil {
panic(err)
}
}

// LambdaAdapter converts a LambdaHandler into a http.HandlerFunc.
func LambdaAdapter(l LambdaHandler) func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
switch eventHandler := l.Handler.(type) {
// APIGatewayProxyHandler
case func(context.Context, events.APIGatewayProxyRequest) (*events.APIGatewayProxyResponse, error):
fmt.Printf("%s %s: %+v \n", r.Method, r.URL.Path, l.PathParams)
request := NewAPIGatewayRequestBuilder(r, l.PathParams)
resp, eErr := eventHandler(context.Background(), request.Request())
if eErr != nil {
fmt.Printf("%s %s\n", UNHANDLED_ERR_MESSAGE, eErr.Error())
resp, eErr = unhandledError(eErr)
if eErr != nil {
panic(eErr)
}
}

w.WriteHeader(resp.StatusCode)
for k, v := range resp.Headers {
w.Header().Set(k, v)
}
for k, v := range resp.MultiValueHeaders {
w.Header().Set(k, strings.Join(v, ","))
}

if _, wErr := w.Write([]byte(resp.Body)); wErr != nil {
panic(wErr)
}
default:
panic(fmt.Sprintf("unknown type: %T", l.Handler))
}
}
}

// APIGatewayRequestBuilder is a builder for APIGatewayProxyRequest.
type APIGatewayRequestBuilder struct {
pathParams []string
request *http.Request
}

// Headers returns the headers of the request.
func (b *APIGatewayRequestBuilder) Headers() map[string]string {
headers := make(map[string]string, len(b.request.Header))
for k, v := range b.request.Header {
headers[k] = strings.Join(v, ",")
}

return headers
}

// QueryStrings returns the query strings of the request.
func (b *APIGatewayRequestBuilder) QueryStrings() (map[string]string, map[string][]string) {
query := b.request.URL.Query()
queryParams := make(map[string]string, len(query))
MultiQueryParams := make(map[string][]string, len(query))
for k, v := range query {
queryParams[k] = strings.Join(v, ",")
MultiQueryParams[k] = v
}

return queryParams, MultiQueryParams
}

// PathParams returns the path parameters of the request.
func (b *APIGatewayRequestBuilder) PathParams() map[string]string {
pathParams := make(map[string]string, len(b.pathParams))
for _, v := range b.pathParams {
pathParams[v] = chi.URLParam(b.request, v)
}

return pathParams
}

// Body returns the body of the request.
func (b *APIGatewayRequestBuilder) Body() string {
if body, err := io.ReadAll(b.request.Body); err == nil {
return string(body)
}
return ""
}

// Request returns the APIGatewayProxyRequest.
func (b *APIGatewayRequestBuilder) Request() events.APIGatewayProxyRequest {
query, multiQuery := b.QueryStrings()
return events.APIGatewayProxyRequest{
Path: b.request.URL.Path,
HTTPMethod: b.request.Method,
Headers: b.Headers(),
MultiValueHeaders: b.request.Header,
QueryStringParameters: query,
MultiValueQueryStringParameters: multiQuery,
PathParameters: b.PathParams(),
Body: b.Body(),
}
}

// NewAPIGatewayRequestBuilder creates a new APIGatewayRequestBuilder.
func NewAPIGatewayRequestBuilder(request *http.Request, pathParams []string) *APIGatewayRequestBuilder {
return &APIGatewayRequestBuilder{
request: request,
pathParams: pathParams,
}
}

// unhandledError returns an APIGatewayProxyResponse with the given error.
func unhandledError(err error) (*events.APIGatewayProxyResponse, error) {
var newErr Err
switch err := err.(type) {
case Err:
newErr = err
default:
newErr = ErrInternalServer
}

b, err := json.Marshal(newErr)
if err != nil {
return nil, err
}

r := new(events.APIGatewayProxyResponse)
r.Headers = make(map[string]string)
r.Headers["Content-Type"] = "application/json"
r.StatusCode = newErr.Status
r.Body = string(b)

return r, nil
}
77 changes: 77 additions & 0 deletions http_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package g8_test

import (
"context"
"fmt"
"github.com/aws/aws-lambda-go/events"
"github.com/stretchr/testify/assert"
"net/http"
"net/http/httptest"
"testing"

"github.com/JSainsburyPLC/g8"
)

func TestLambdaAdapter(t *testing.T) {
l := g8.LambdaHandler{
Handler: func(ctx context.Context, r events.APIGatewayProxyRequest) (*events.APIGatewayProxyResponse, error) {
return &events.APIGatewayProxyResponse{
StatusCode: http.StatusOK,
Headers: map[string]string{"Content-Type": "text/plain"},
MultiValueHeaders: map[string][]string{"Set-Cookie": {"cookie1", "cookie2"}},
Body: "success",
}, nil
},
Method: http.MethodGet,
Path: "/test/url/path/{var1}/{var2}",
PathParams: []string{"var1", "var2"},
}

w := httptest.NewRecorder()
r := httptest.NewRequest(http.MethodGet, "/test/url/path/var1/var2", nil)
r.Header.Set("Content-Type", "application/json")
g8.LambdaAdapter(l)(w, r)

assert.Equal(t, http.StatusOK, w.Code)
assert.Equal(t, "text/plain", w.Header().Get("Content-Type"))
assert.Equal(t, "cookie1,cookie2", w.Header().Get("Set-Cookie"))
assert.Equal(t, "success", w.Body.String())
}

func TestLambdaAdapter_g8_error(t *testing.T) {
l := g8.LambdaHandler{
Handler: func(ctx context.Context, r events.APIGatewayProxyRequest) (*events.APIGatewayProxyResponse, error) {
return nil, g8.ErrInternalServer
},
Method: http.MethodGet,
Path: "/test/url/path",
}

w := httptest.NewRecorder()
r := httptest.NewRequest(http.MethodGet, "/test/url/path/var1/var2", nil)
r.Header.Set("Content-Type", "application/json")
g8.LambdaAdapter(l)(w, r)

assert.Equal(t, http.StatusInternalServerError, w.Code)
assert.Equal(t, "application/json", w.Header().Get("Content-Type"))
assert.Equal(t, `{"code":"INTERNAL_SERVER_ERROR","detail":"Internal server error"}`, w.Body.String())
}

func TestLambdaAdapter_generic_error(t *testing.T) {
l := g8.LambdaHandler{
Handler: func(ctx context.Context, r events.APIGatewayProxyRequest) (*events.APIGatewayProxyResponse, error) {
return nil, fmt.Errorf("generic error")
},
Method: http.MethodGet,
Path: "/test/url/path",
}

w := httptest.NewRecorder()
r := httptest.NewRequest(http.MethodGet, "/test/url/path/var1/var2", nil)
r.Header.Set("Content-Type", "application/json")
g8.LambdaAdapter(l)(w, r)

assert.Equal(t, http.StatusInternalServerError, w.Code)
assert.Equal(t, "application/json", w.Header().Get("Content-Type"))
assert.Equal(t, `{"code":"INTERNAL_SERVER_ERROR","detail":"Internal server error"}`, w.Body.String())
}

0 comments on commit f4335d6

Please sign in to comment.