diff --git a/.travis.yml b/.travis.yml index aca28d3..4ba3eac 100644 --- a/.travis.yml +++ b/.travis.yml @@ -13,6 +13,7 @@ before_install: - go get -u -v github.com/axw/gocov/gocov - go get -u -v github.com/mattn/goveralls - go get -u -v github.com/golang/lint/golint + - go get -u -v github.com/mitchellh/mapstructure - mkdir -p $GOPATH/src/gopkg.in/h2non - ln -s $(pwd) $GOPATH/src/gopkg.in/h2non/baloo.v3 diff --git a/README.md b/README.md index 1969168..ced19db 100644 --- a/README.md +++ b/README.md @@ -143,6 +143,51 @@ func TestJSONSchema(t *testing.T) { } ``` + +#### JSON inline data assertion + +You can test JSON in body response against a function or a future. + +```go +package json_schema + +import ( + "testing" + + "gopkg.in/h2non/baloo.v3" + "github.com/mitchellh/mapstructure" +) + + +type UserAgent struct { + Value string `mapstructure:"user-agent"` +} + +// test stores the HTTP testing client preconfigured +var test = baloo.New("http://httpbin.org") + +func TestJSONSchema(t *testing.T) { + test.Get("/user-agent"). + SetHeader("Foo", "Bar"). + Expect(t). + Status(200). + Type("json"). + JSON(`{"user-agent":"baloo/` + baloo.Version + `"}`). + OnJSON(func(data interface{}) error { + var result UserAgent + err := mapstructure.Decode(data, &result) + if err != nil { + return err + } + if !strings.Contains(result.Value, "baloo") { + return fmt.Errorf("bad user-agent: %s, %s", result.Value, data) + } + return nil + }). + Done() +} +``` + #### Custom global assertion by alias ```go diff --git a/_examples/assert_json/json_test.go b/_examples/assert_json/json_test.go index 0f2c652..30c2a49 100644 --- a/_examples/assert_json/json_test.go +++ b/_examples/assert_json/json_test.go @@ -1,11 +1,18 @@ package assert_json import ( + "fmt" + "strings" "testing" + "github.com/mitchellh/mapstructure" baloo "gopkg.in/h2non/baloo.v3" ) +type UserAgent struct { + Value string `mapstructure:"user-agent"` +} + // test stores the HTTP testing client preconfigured var test = baloo.New("http://httpbin.org") @@ -18,3 +25,24 @@ func TestBalooJSONAssertion(t *testing.T) { JSON(`{"user-agent":"baloo/` + baloo.Version + `"}`). Done() } + +func TestBalooJSONCustomAssertion(t *testing.T) { + test.Get("/user-agent"). + SetHeader("Foo", "Bar"). + Expect(t). + Status(200). + Type("json"). + JSON(`{"user-agent":"baloo/` + baloo.Version + `"}`). + OnJSON(func(data interface{}) error { + var result UserAgent + err := mapstructure.Decode(data, &result) + if err != nil { + return err + } + if !strings.Contains(result.Value, "baloo") { + return fmt.Errorf("bad user-agent: %s, %s", result.Value, data) + } + return nil + }). + Done() +} diff --git a/assert/body.go b/assert/body.go index 5664873..660e957 100644 --- a/assert/body.go +++ b/assert/body.go @@ -27,8 +27,9 @@ func BodyMatchString(pattern string) Func { if err != nil { return err } - if match, _ := regexp.MatchString(pattern, string(body)); !match { - return fmt.Errorf("body mismatch: pattern not found '%s'", pattern) + sbody := string(body) + if match, _ := regexp.MatchString(pattern, sbody); !match { + return fmt.Errorf("body mismatch: pattern not found '%s' in '%s'", pattern, sbody) } return nil } diff --git a/assert/json.go b/assert/json.go index dc181a7..b84618c 100644 --- a/assert/json.go +++ b/assert/json.go @@ -9,6 +9,9 @@ import ( "reflect" ) +// FnOnJSON callable function that take an interface{}, test this data and can return an error. +type FnOnJSON func(interface{}) error + func unmarshal(buf []byte) (interface{}, error) { var data interface{} if err := json.Unmarshal(buf, &data); err != nil { @@ -22,6 +25,7 @@ func unmarshalBody(res *http.Response) (interface{}, error) { if err != nil { return nil, err } + res.Body = ioutil.NopCloser(bytes.NewBuffer(body)) if len(body) == 0 { return nil, nil } @@ -103,3 +107,17 @@ func JSON(data interface{}) Func { return compare(body, data) } } + +// OnJSON extract JSON in body and call fn with result +// write your own test function on data +func OnJSON(fn FnOnJSON) Func { + return func(res *http.Response, req *http.Request) error { + // Read and unmarshal response body as JSON + body, err := unmarshalBody(res) + if err != nil { + return err + } + + return fn(body) + } +} diff --git a/assert/json_test.go b/assert/json_test.go index f9bf7da..a1288aa 100644 --- a/assert/json_test.go +++ b/assert/json_test.go @@ -2,13 +2,21 @@ package assert import ( "bytes" + "fmt" "io/ioutil" "net/http" + "strings" "testing" + "github.com/mitchellh/mapstructure" + "github.com/nbio/st" ) +type UserAgent struct { + Value string `mapstructure:"user-agent"` +} + type items map[string]string func TestJSONString(t *testing.T) { @@ -106,3 +114,35 @@ func TestCompare(t *testing.T) { st.Expect(t, compare(map[string]interface{}{"a": []float64{1.1, 2.2}}, `{"a":[1.1, 2.2]}`), nil) st.Expect(t, compare(map[string]interface{}{"a": 5.1}, `{"a": 5.1}`), nil) } + +func TestOnJSONCustomAssertion(t *testing.T) { + body := ioutil.NopCloser(bytes.NewBufferString(`{"user-agent":"baloo/v3"}`)) + res := &http.Response{Body: body} + st.Expect(t, OnJSON(func(data interface{}) error { + var result UserAgent + err := mapstructure.Decode(data, &result) + if err != nil { + return err + } + if !strings.Contains(result.Value, "baloo") { + return fmt.Errorf("bad user-agent: %s, %s", result.Value, data) + } + return nil + })(res, nil), nil) +} + +func TestOnJSONCustomAssertionBadResponse(t *testing.T) { + body := ioutil.NopCloser(bytes.NewBufferString(`{"user-agent":"toto"}`)) + res := &http.Response{Body: body} + st.Expect(t, OnJSON(func(data interface{}) error { + var result UserAgent + err := mapstructure.Decode(data, &result) + if err != nil { + return err + } + if !strings.Contains(result.Value, "baloo") { + return fmt.Errorf("bad user-agent: %s, %s", result.Value, data) + } + return nil + })(res, nil), fmt.Errorf("bad user-agent: toto, map[user-agent:toto]")) +} diff --git a/expect.go b/expect.go index 10f73da..9227307 100644 --- a/expect.go +++ b/expect.go @@ -155,6 +155,13 @@ func (e *Expect) JSON(data interface{}) *Expect { return e } +// OnJSON asserts the response body with the given function +// write your own test on data +func (e *Expect) OnJSON(fn assert.FnOnJSON) *Expect { + e.AssertFunc(assert.OnJSON(fn)) + return e +} + // JSONSchema asserts the response body with the given // JSON schema definition. func (e *Expect) JSONSchema(schema string) *Expect { @@ -196,7 +203,9 @@ func (e *Expect) Done() error { // Run assertions err = e.run(res.RawResponse, res.RawRequest) if err != nil { - e.test.Error(err) + logerrorf(e.test, err.Error()) + Dump(e.test, res.RawResponse) + } return err diff --git a/log.go b/log.go new file mode 100644 index 0000000..f2cbc67 --- /dev/null +++ b/log.go @@ -0,0 +1,121 @@ +package baloo + +/// Come from https://github.com/emicklei/forest/ +// LICENSE MIT https://github.com/emicklei/forest/blob/master/LICENSE.txt + +import ( + "bytes" + "fmt" + "io/ioutil" + "net/http" + "runtime" + "strings" + "testing" +) + +var scanStackForFile = true +var logfFunc = logf + +const noStackOffset = 0 + +// Logf adds the actual file:line information to the log message +func Logf(t *testing.T, format string, args ...interface{}) { + logfFunc(t, noStackOffset, "\n"+format, args...) +} + +func logfatal(t *testing.T, format string, args ...interface{}) { + logfFunc(t, noStackOffset, format, args...) + t.FailNow() +} + +// Error is equivalent to Log followed by Fail. +func logerror(t *testing.T, args ...interface{}) { + logerrorf(t, "\terror: "+tabify("%s")+"\n", args) +} + +func logerrorf(t *testing.T, format string, args ...interface{}) { + logfFunc(t, noStackOffset, format, args...) + t.Fail() +} + +func logf(t *testing.T, stackOffset int, format string, args ...interface{}) { + var file string + var line int + var ok bool + if scanStackForFile { + offset := 0 + outside := false + for !outside { + _, file, line, ok = runtime.Caller(2 + offset) + outside = !strings.Contains(file, "/baloo/") + offset++ + } + } else { + _, file, line, ok = runtime.Caller(2) + } + if ok { + // Truncate file name at last file name separator. + if index := strings.LastIndex(file, "/"); index >= 0 { + file = file[index+1:] + } else if index = strings.LastIndex(file, "\\"); index >= 0 { + file = file[index+1:] + } + } else { + file = "???" + line = 1 + } + t.Logf("<-- %s:%d "+format, append([]interface{}{file, line}, args...)...) +} + +// Dump is a convenient method to log the full contents of a request and its response. +func Dump(t *testing.T, resp *http.Response) { + // dump request + var buffer bytes.Buffer + buffer.WriteString("\n") + buffer.WriteString(fmt.Sprintf("%v %v\n", resp.Request.Method, resp.Request.URL)) + for k, v := range resp.Request.Header { + if len(k) > 0 { + buffer.WriteString(fmt.Sprintf("%s : %v\n", k, strings.Join(v, ","))) + } + } + if resp == nil { + buffer.WriteString("-- no response --") + Logf(t, buffer.String()) + return + } + // dump response + buffer.WriteString(fmt.Sprintf("\n%s\n", resp.Status)) + for k, v := range resp.Header { + if len(k) > 0 { + buffer.WriteString(fmt.Sprintf("%s : %v\n", k, strings.Join(v, ","))) + } + } + if resp.Body != nil { + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + if resp.StatusCode/100 == 3 { + // redirect closes body ; nothing to read + buffer.WriteString("\n") + } else { + buffer.WriteString(fmt.Sprintf("unable to read body:%v", err)) + } + } else { + if len(body) > 0 { + buffer.WriteString("\n") + } + buffer.WriteString(string(body)) + } + resp.Body.Close() + // put the body back for re-reads + resp.Body = ioutil.NopCloser(bytes.NewReader(body)) + } + buffer.WriteString("\n") + Logf(t, buffer.String()) +} + +func tabify(format string) string { + if strings.HasPrefix(format, "\n") { + return strings.Replace(format, "\n", "\n\t\t", 1) + } + return format +}