Skip to content

Commit

Permalink
Merge pull request #2 from Jesse0Michael/callbacks
Browse files Browse the repository at this point in the history
Callbacks
  • Loading branch information
Jesse0Michael authored Dec 19, 2017
2 parents d7cbed2 + 31f99bc commit 51dd7ee
Show file tree
Hide file tree
Showing 13 changed files with 605 additions and 76 deletions.
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ fmt:
find . -not -path "./vendor/*" -name '*.go' -type f | sed 's#\(.*\)/.*#\1#' | sort -u | xargs -n1 -I {} bash -c "cd {} && goimports -w *.go && gofmt -w -l -s *.go"
test:
if [ ! -d $(COVERAGEDIR) ]; then mkdir $(COVERAGEDIR); fi
$(GO) test -v ./assured -race -cover -coverprofile=$(COVERAGEDIR)/assured.coverprofile
$(GO) test -v ./assured -cover -coverprofile=$(COVERAGEDIR)/assured.coverprofile
cover:
if [ ! -d $(COVERAGEDIR) ]; then mkdir $(COVERAGEDIR); fi
$(GO) tool cover -html=$(COVERAGEDIR)/assured.coverprofile -o $(COVERAGEDIR)/assured.html
Expand Down
29 changes: 28 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,14 +70,40 @@ To use your assured calls hit the following endpoint with the Method/Path that w
Or..

```go
// Get the URL of the client ex: 'localhost:11011/when'
// Get the URL of the client ex: 'http://localhost:11011/when'
testServer := client.URL()
```

Go-Rest-Assured will return `404 NotFound` error response when a matching stub isn't found

As requests come in, the will be stored

## Callbacks
To include callbacks from Go-Rest-Assured when a stubbed endpoint is hit, create them by hitting the endpoint `/callbacks`
To create a callbacks you must include the HTTP header `Assured-Callback-Target` with the specified endpoint you want your callbacks to be sent to
You must also include the HTTP header `Assured-Callback-Key` with a key with the call to the `/callbacks` endpoint as well as the `/given/{path:.*}` endpoint that for the stubbed call you want the callback to be associated with
You can also set a callback delay with the HTTP Header `Assured-Callback-Delay` with a number of seconds

Or...

```go
call := assured.Call{
Path: "test/assured",
StatusCode: 201,
Method: "POST",
Response: []byte(`{"holler_back":true}`),
Callbacks: []assured.Callback{
assured.Callback{
Method: "POST",
Target: "http://localhost:8080/hit/me",
Response: []byte(`holla!!`),
},
},
}
// Stub out an assured call with callbacks
client.Given(call)
```
*You cannot clear out an individual callback when using the assured.Client, but you can `ClearAll()`*

## Verifying
To verify the calls made against your go-rest-assured service, use the endpoint `/verify/{path:.*}`
Expand All @@ -94,6 +120,7 @@ calls := client.Verify("GET", "test/assured")

## Clearing
To clear out the stubbed and made calls for a specific Method/Path, use the endpoint `/clear/{path:.*}`
*Including the HTTP Header `Assured-Callback-Key` will clear all callbacks associated with that key (independent of path)*

To clear out all stubbed calls on the server, use the endpoint `/clear`

Expand Down
54 changes: 52 additions & 2 deletions assured/bindings.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,13 @@ import (
"github.com/gorilla/mux"
)

const (
AssuredStatus = "Assured-Status"
AssuredCallbackKey = "Assured-Callback-Key"
AssuredCallbackTarget = "Assured-Callback-Target"
AssuredCallbackDelay = "Assured-Callback-Delay"
)

// StartApplicationHTTPListener creates a Go-routine that has an HTTP listener for the application endpoints
func StartApplicationHTTPListener(root context.Context, errc chan error, settings Settings) {
go func() {
Expand Down Expand Up @@ -62,6 +69,16 @@ func createApplicationRouter(ctx context.Context, settings Settings) *mux.Router
kithttp.ServerAfter(kithttp.SetResponseHeader("Access-Control-Allow-Origin", "*"))),
).Methods(assuredMethods...)

router.Handle(
"/callback",
kithttp.NewServer(
e.WrappedEndpoint(e.GivenCallbackEndpoint),
decodeAssuredCallback,
encodeAssuredCall,
kithttp.ServerErrorLogger(settings.Logger),
kithttp.ServerAfter(kithttp.SetResponseHeader("Access-Control-Allow-Origin", "*"))),
).Methods(assuredMethods...)

router.Handle(
"/when/{path:.*}",
kithttp.NewServer(
Expand Down Expand Up @@ -115,7 +132,7 @@ func decodeAssuredCall(ctx context.Context, req *http.Request) (interface{}, err
}

// Set status code override
if statusCode, err := strconv.ParseInt(req.Header.Get("Assured-Status"), 10, 64); err == nil {
if statusCode, err := strconv.ParseInt(req.Header.Get(AssuredStatus), 10, 64); err == nil {
ac.StatusCode = int(statusCode)
}

Expand All @@ -137,13 +154,46 @@ func decodeAssuredCall(ctx context.Context, req *http.Request) (interface{}, err
return &ac, nil
}

// decodeAssuredCallback converts an http request into an assured Callback object
func decodeAssuredCallback(ctx context.Context, req *http.Request) (interface{}, error) {
ac := Call{
Method: req.Method,
StatusCode: http.StatusCreated,
}

// Require headers
if len(req.Header[AssuredCallbackKey]) == 0 {
return nil, fmt.Errorf("'%s' header required for callback", AssuredCallbackKey)
}
if len(req.Header[AssuredCallbackTarget]) == 0 {
return nil, fmt.Errorf("'%s' header required for callback", AssuredCallbackTarget)
}

// Set headers
headers := map[string]string{}
for key, value := range req.Header {
headers[key] = value[0]
}
ac.Headers = headers

// Set response body
if req.Body != nil {
defer req.Body.Close()
if bytes, err := ioutil.ReadAll(req.Body); err == nil {
ac.Response = bytes
}
}

return &ac, nil
}

// encodeAssuredCall writes the assured Call to the http response as it is intended to be stubbed
func encodeAssuredCall(ctx context.Context, w http.ResponseWriter, i interface{}) error {
switch resp := i.(type) {
case *Call:
w.WriteHeader(resp.StatusCode)
for key, value := range resp.Headers {
if !strings.HasPrefix(key, "Assured") {
if !strings.HasPrefix(key, "Assured-") {
w.Header().Set(key, value)
}
}
Expand Down
136 changes: 122 additions & 14 deletions assured/bindings_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,21 @@ func TestApplicationRouterGivenBinding(t *testing.T) {
}
}

func TestApplicationRouterGivenCallbackBinding(t *testing.T) {
router := createApplicationRouter(ctx, testSettings)

for _, verb := range verbs {
req, err := http.NewRequest(verb, "/callback", nil)
req.Header.Set(AssuredCallbackKey, "call-key")
req.Header.Set(AssuredCallbackTarget, "http://faketarget.com/")
require.NoError(t, err)
resp := httptest.NewRecorder()
router.ServeHTTP(resp, req)
require.Equal(t, http.StatusCreated, resp.Code)
require.Equal(t, "*", resp.Header().Get("Access-Control-Allow-Origin"))
}
}

func TestApplicationRouterWhenBinding(t *testing.T) {
router := createApplicationRouter(ctx, testSettings)

Expand Down Expand Up @@ -202,6 +217,81 @@ func TestDecodeAssuredCallStatusFailure(t *testing.T) {
require.True(t, decoded, "decode method was not hit")
}

func TestDecodeAssuredCallback(t *testing.T) {
decoded := false
expected := &Call{
Method: http.MethodPost,
StatusCode: http.StatusCreated,
Response: []byte(`{"done": true}`),
Headers: map[string]string{"Assured-Callback-Target": "http://faketarget.com/", "Assured-Callback-Key": "call-key"},
}
testDecode := func(resp http.ResponseWriter, req *http.Request) {
c, err := decodeAssuredCallback(ctx, req)

require.NoError(t, err)
require.Equal(t, expected, c)
decoded = true
}

req, err := http.NewRequest(http.MethodPost, "/callback", bytes.NewBuffer([]byte(`{"done": true}`)))
require.NoError(t, err)
req.Header.Set(AssuredCallbackKey, "call-key")
req.Header.Set(AssuredCallbackTarget, "http://faketarget.com/")

router := mux.NewRouter()
router.HandleFunc("/callback", testDecode).Methods(http.MethodPost)
resp := httptest.NewRecorder()
router.ServeHTTP(resp, req)

require.True(t, decoded, "decode method was not hit")
}

func TestDecodeAssuredCallbackMissingKey(t *testing.T) {
decoded := false
testDecode := func(resp http.ResponseWriter, req *http.Request) {
c, err := decodeAssuredCallback(ctx, req)

require.Nil(t, c)
require.Error(t, err)
require.Equal(t, "'Assured-Callback-Key' header required for callback", err.Error())
decoded = true
}

req, err := http.NewRequest(http.MethodPost, "/callback", bytes.NewBuffer([]byte(`{"done": true}`)))
req.Header.Set(AssuredCallbackTarget, "http://faketarget.com/")
require.NoError(t, err)

router := mux.NewRouter()
router.HandleFunc("/callback", testDecode).Methods(http.MethodPost)
resp := httptest.NewRecorder()
router.ServeHTTP(resp, req)

require.True(t, decoded, "decode method was not hit")
}

func TestDecodeAssuredCallbackMissingTarget(t *testing.T) {
decoded := false
testDecode := func(resp http.ResponseWriter, req *http.Request) {
c, err := decodeAssuredCallback(ctx, req)

require.Nil(t, c)
require.Error(t, err)
require.Equal(t, "'Assured-Callback-Target' header required for callback", err.Error())
decoded = true
}

req, err := http.NewRequest(http.MethodPost, "/callback", bytes.NewBuffer([]byte(`{"done": true}`)))
req.Header.Set(AssuredCallbackKey, "call-key")
require.NoError(t, err)

router := mux.NewRouter()
router.HandleFunc("/callback", testDecode).Methods(http.MethodPost)
resp := httptest.NewRecorder()
router.ServeHTTP(resp, req)

require.True(t, decoded, "decode method was not hit")
}

func TestEncodeAssuredCall(t *testing.T) {
call := &Call{
Path: "/test/assured",
Expand All @@ -227,7 +317,7 @@ func TestEncodeAssuredCalls(t *testing.T) {
resp := httptest.NewRecorder()
expected, err := ioutil.ReadFile("../testdata/calls.json")
require.NoError(t, err)
err = encodeAssuredCall(ctx, resp, []*Call{call1, call2, call3})
err = encodeAssuredCall(ctx, resp, []*Call{testCall1(), testCall2(), testCall3()})

require.NoError(t, err)
require.Equal(t, "application/json", resp.HeaderMap.Get("Content-Type"))
Expand All @@ -247,34 +337,52 @@ var (
http.MethodConnect,
http.MethodOptions,
}
call1 = &Call{
testSettings = Settings{
Logger: kitlog.NewLogfmtLogger(ioutil.Discard),
HTTPClient: *http.DefaultClient,
TrackMadeCalls: true,
}
fullAssuredCalls = &CallStore{
data: map[string][]*Call{
"GET:test/assured": {testCall1(), testCall2()},
"POST:teapot/assured": {testCall3()},
},
}
)

func testCall1() *Call {
return &Call{
Path: "test/assured",
Method: "GET",
StatusCode: http.StatusOK,
Response: []byte(`{"assured": true}`),
Headers: map[string]string{"Content-Length": "17", "User-Agent": "Go-http-client/1.1", "Accept-Encoding": "gzip"},
}
call2 = &Call{
}

func testCall2() *Call {
return &Call{
Path: "test/assured",
Method: "GET",
StatusCode: http.StatusConflict,
Response: []byte("error"),
Headers: map[string]string{"Content-Length": "5", "User-Agent": "Go-http-client/1.1", "Accept-Encoding": "gzip"},
}
call3 = &Call{
}

func testCall3() *Call {
return &Call{
Path: "teapot/assured",
Method: "POST",
StatusCode: http.StatusTeapot,
Headers: map[string]string{"Content-Length": "0", "User-Agent": "Go-http-client/1.1", "Accept-Encoding": "gzip"},
}
fullAssuredCalls = &CallStore{
data: map[string][]*Call{
"GET:test/assured": {call1, call2},
"POST:teapot/assured": {call3},
},
}
testSettings = Settings{
Logger: kitlog.NewLogfmtLogger(ioutil.Discard),
TrackMadeCalls: true,
}

func testCallback() *Call {
return &Call{
Response: []byte(`{"done": true}`),
Method: "POST",
Headers: map[string]string{"Assured-Callback-Key": "call-key", "Assured-Callback-Target": "http://faketarget.com/"},
}
)
}
10 changes: 10 additions & 0 deletions assured/call.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ type Call struct {
StatusCode int `json:"status_code"`
Headers map[string]string `json:"headers"`
Response []byte `json:"response,omitempty"`
Callbacks []Callback `json:"callbacks,omitempty"`
}

// ID is used as a key when managing stubbed and made calls
Expand All @@ -25,3 +26,12 @@ func (c Call) String() string {
// TODO: implement string replacements for special cases
return rawString
}

// Callback is a structure containing a callback that is stubbed
type Callback struct {
Target string `json:"target"`
Method string `json:"method"`
Delay int `json:"delay,omitempty"`
Headers map[string]string `json:"headers"`
Response []byte `json:"response,omitempty"`
}
6 changes: 6 additions & 0 deletions assured/call_store.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,12 @@ func (c *CallStore) Add(call *Call) {
c.Unlock()
}

func (c *CallStore) AddAt(key string, call *Call) {
c.Lock()
c.data[key] = append(c.data[key], call)
c.Unlock()
}

func (c *CallStore) Rotate(call *Call) {
c.Lock()
c.data[call.ID()] = append(c.data[call.ID()][1:], call)
Expand Down
Loading

0 comments on commit 51dd7ee

Please sign in to comment.