From 9f59dbf340a5c4e38b19a38cb76fe03287e58068 Mon Sep 17 00:00:00 2001 From: debugtalk Date: Sun, 26 Sep 2021 16:28:18 +0800 Subject: [PATCH] feat: validate response object --- examples/postman_echo/hardcode_test.go | 2 +- go.mod | 1 + go.sum | 4 + models.go | 8 +- response.go | 109 +++++++++++++++++++++++++ runner.go | 22 ++++- runner_test.go | 6 +- step_test.go | 10 ++- validate.go | 12 +-- 9 files changed, 156 insertions(+), 18 deletions(-) diff --git a/examples/postman_echo/hardcode_test.go b/examples/postman_echo/hardcode_test.go index 66837d7..bc74ae2 100644 --- a/examples/postman_echo/hardcode_test.go +++ b/examples/postman_echo/hardcode_test.go @@ -47,7 +47,7 @@ func TestCaseHardcode(t *testing.T) { }, } - err := httpboomer.Test(testcase) + err := httpboomer.Test(t, testcase) if err != nil { t.Fatalf("run testcase error: %v", err) } diff --git a/go.mod b/go.mod index 0b5ada3..df60a9f 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,7 @@ require ( github.com/asaskevich/EventBus v0.0.0-20200907212545-49d423059eef // indirect github.com/google/uuid v1.3.0 // indirect github.com/imroc/req v0.3.0 // indirect + github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/myzhan/boomer v1.6.0 github.com/olekukonko/tablewriter v0.0.5 // indirect github.com/shirou/gopsutil v3.21.8+incompatible // indirect diff --git a/go.sum b/go.sum index ce77521..8c81c37 100644 --- a/go.sum +++ b/go.sum @@ -10,6 +10,9 @@ github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/imroc/req v0.3.0 h1:3EioagmlSG+z+KySToa+Ylo3pTFZs+jh3Brl7ngU12U= github.com/imroc/req v0.3.0/go.mod h1:F+NZ+2EFSo6EFXdeIbpfE9hcC233id70kf0byW97Caw= +github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= +github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= +github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/myzhan/boomer v1.6.0 h1:xjgvmhDjgU9IEKnB7nU1HyoVEfj8SuuU3u6oY3Nugj0= @@ -42,5 +45,6 @@ golang.org/x/sys v0.0.0-20210816074244-15123e1e1f71/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210917161153-d61c044b1678 h1:J27LZFQBFoihqXoegpscI10HpjZ7B5WQLLKL2FZXQKw= golang.org/x/sys v0.0.0-20210917161153-d61c044b1678/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/models.go b/models.go index 9d8ae60..3c48d2e 100644 --- a/models.go +++ b/models.go @@ -36,10 +36,10 @@ type TRequest struct { } type TValidator struct { - Check string // get value with jmespath - Comparator string - Expect interface{} - Message string + Check string // get value with jmespath + Assert string + Expect interface{} + Message string } type TStep struct { diff --git a/response.go b/response.go index eb187cb..5d4cba9 100644 --- a/response.go +++ b/response.go @@ -1 +1,110 @@ package httpboomer + +import ( + "encoding/json" + "log" + "testing" + + "github.com/imroc/req" + "github.com/jmespath/go-jmespath" + "github.com/stretchr/testify/assert" +) + +var assertFunctionsMap = map[string]func(t assert.TestingT, expected interface{}, actual interface{}, msgAndArgs ...interface{}) bool{ + "equals": assert.EqualValues, + "equal": assert.EqualValues, // alias for equals + "greater_than": assert.Greater, + "less_than": assert.Less, + "greater_or_equals": assert.GreaterOrEqual, + "less_or_equals": assert.LessOrEqual, + "not_equal": assert.NotEqual, + "contains": assert.Contains, + "regex_match": assert.Regexp, +} + +func NewResponseObject(t *testing.T, resp *req.Resp) *ResponseObject { + // prepare response headers + headers := make(map[string]string) + for k, v := range resp.Response().Header { + if len(v) > 0 { + headers[k] = v[0] + } + } + + // prepare response cookies + cookies := make(map[string]string) + for _, cookie := range resp.Response().Cookies() { + cookies[cookie.Name] = cookie.Value + } + + // parse response body + var body interface{} + if err := json.Unmarshal(resp.Bytes(), &body); err != nil { + log.Fatalf("[NewResponseObject] json.Unmarshal response body err: %v, body: %v", + err, string(resp.Bytes())) + return nil + } + + respObjMeta := respObjMeta{ + StatusCode: resp.Response().StatusCode, + Headers: headers, + Cookies: cookies, + Body: body, + } + + // convert respObjMeta to interface{} + respObjMetaBytes, _ := json.Marshal(respObjMeta) + var data interface{} + if err := json.Unmarshal(respObjMetaBytes, &data); err != nil { + log.Fatalf("[NewResponseObject] json.Unmarshal respObjMeta err: %v, respObjMetaBytes: %v", + err, string(respObjMetaBytes)) + return nil + } + + return &ResponseObject{ + t: t, + respObjMeta: data, + } +} + +type respObjMeta struct { + StatusCode int `json:"status_code"` + Headers map[string]string `json:"headers"` + Cookies map[string]string `json:"cookies"` + Body interface{} `json:"body"` +} + +type ResponseObject struct { + t *testing.T + respObjMeta interface{} + validationResults map[string]interface{} +} + +func (v *ResponseObject) Validate(validators []TValidator) error { + for _, validator := range validators { + // parse check value + checkItem := validator.Check + checkValue := v.searchJmespath(checkItem) + // get assert method + assertMethod := validator.Assert + assertFunc := assertFunctionsMap[assertMethod] + // parse expected value + expectValue := validator.Expect + // do assertion + result := assertFunc(v.t, expectValue, checkValue) + log.Printf("assert %s %s %v => %v", checkItem, assertMethod, expectValue, result) + if !result { + v.t.Fail() + } + } + return nil +} + +func (v *ResponseObject) searchJmespath(expr string) interface{} { + checkValue, err := jmespath.Search(expr, v.respObjMeta) + if err != nil { + log.Printf("[searchJmespath] jmespath.Search error: %v", err) + return nil + } + return checkValue +} diff --git a/runner.go b/runner.go index 6fb5060..d0bb60f 100644 --- a/runner.go +++ b/runner.go @@ -3,26 +3,34 @@ package httpboomer import ( "log" "net/http" + "testing" "github.com/imroc/req" ) var defaultRunner = NewRunner() -func Test(testcases ...*TestCase) error { - return defaultRunner.Run(testcases...) +func Test(t *testing.T, testcases ...*TestCase) error { + return defaultRunner.WithTestingT(t).Run(testcases...) } func NewRunner() *Runner { return &Runner{ + t: &testing.T{}, Client: req.New(), } } type Runner struct { + t *testing.T Client *req.Req } +func (r *Runner) WithTestingT(t *testing.T) *Runner { + r.t = t + return r +} + func (r *Runner) Run(testcases ...*TestCase) error { for _, testcase := range testcases { if err := r.runCase(testcase); err != nil { @@ -62,6 +70,7 @@ func (r *Runner) runStep(step IStep, config *TConfig) error { } func (r *Runner) runStepRequest(step *TStep) error { + // prepare request args var v []interface{} v = append(v, req.Header(step.Request.Headers)) v = append(v, req.Param(step.Request.Params)) @@ -75,12 +84,21 @@ func (r *Runner) runStepRequest(step *TStep) error { }) } + // do request action req.Debug = true resp, err := r.Client.Do(string(step.Request.Method), step.Request.URL, v...) if err != nil { return err } defer resp.Response().Body.Close() + + // validate response + respObj := NewResponseObject(r.t, resp) + err = respObj.Validate(step.Validators) + if err != nil { + return err + } + return nil } diff --git a/runner_test.go b/runner_test.go index 842a8bb..8a490e2 100644 --- a/runner_test.go +++ b/runner_test.go @@ -15,12 +15,12 @@ func TestHttpRunner(t *testing.T) { GET("/headers"). Validate(). AssertEqual("status_code", 200, "check status code"). - AssertEqual("headers.Host", "httpbin.org", "check http response host"), + AssertEqual("headers.\"Content-Type\"", "application/json", "check http response Content-Type"), Step("user-agent"). GET("/user-agent"). Validate(). AssertEqual("status_code", 200, "check status code"). - AssertEqual("body.\"user-agent\"", "python-requests", "check User-Agent"), + AssertEqual("headers.\"Content-Type\"", "application/json", "check http response Content-Type"), Step("TestCase3").CallRefCase(&TestCase{Config: TConfig{Name: "TestCase3"}}), }, } @@ -31,7 +31,7 @@ func TestHttpRunner(t *testing.T) { }, } - err := Test(testcase1, testcase2) + err := Test(t, testcase1, testcase2) if err != nil { t.Fatalf("run testcase error: %v", err) } diff --git a/step_test.go b/step_test.go index c2d6337..08f0d92 100644 --- a/step_test.go +++ b/step_test.go @@ -11,7 +11,11 @@ var ( WithHeaders(map[string]string{"User-Agent": "HttpBoomer"}). WithCookies(map[string]string{"user": "debugtalk"}). Validate(). - AssertEqual("status_code", 200, "check status code") + AssertEqual("status_code", 200, "check status code"). + AssertEqual("headers.Connection", "keep-alive", "check header Connection"). + AssertEqual("headers.\"Content-Type\"", "application/json; charset=utf-8", "check header Content-Type"). + AssertEqual("body.args.foo1", "bar1", "check param foo1"). + AssertEqual("body.args.foo2", "bar2", "check param foo2") stepPOSTData = Step("post form data"). POST("/post"). WithParams(map[string]interface{}{"foo1": "bar1", "foo2": "bar2"}). @@ -73,10 +77,10 @@ func TestRunRequestRun(t *testing.T) { config := &TConfig{ BaseURL: "https://postman-echo.com", } - if err := defaultRunner.runStep(stepGET, config); err != nil { + if err := defaultRunner.WithTestingT(t).runStep(stepGET, config); err != nil { t.Fatalf("tStep.Run() error: %s", err) } - if err := defaultRunner.runStep(stepPOSTData, config); err != nil { + if err := defaultRunner.WithTestingT(t).runStep(stepPOSTData, config); err != nil { t.Fatalf("tStepPOSTData.Run() error: %s", err) } } diff --git a/validate.go b/validate.go index f385f3c..8bc1f92 100644 --- a/validate.go +++ b/validate.go @@ -1,6 +1,8 @@ package httpboomer -import "fmt" +import ( + "fmt" +) // implements IStep interface type stepRequestValidation struct { @@ -9,10 +11,10 @@ type stepRequestValidation struct { func (s *stepRequestValidation) AssertEqual(jmesPath string, expected interface{}, msg string) *stepRequestValidation { validator := TValidator{ - Check: jmesPath, - Comparator: "equals", - Expect: expected, - Message: msg, + Check: jmesPath, + Assert: "equals", + Expect: expected, + Message: msg, } s.step.Validators = append(s.step.Validators, validator) return s