From cde30ff64c3a884457937d4e3f49657b490903e2 Mon Sep 17 00:00:00 2001 From: ahuigo <1781999+ahuigo@users.noreply.github.com> Date: Tue, 29 Oct 2024 00:17:57 +0800 Subject: [PATCH] feat(test): expect test stage 1 - assert --- demo/httpreq/post-file_test.go | 2 +- demo/httpreq/post_test.go | 10 +-- go.mod | 15 ++++- go.sum | 18 ++++++ httpreq/curl.go | 20 ++++-- httpreq/expect.go | 110 +++++++++++++++++++++++++++++++++ httpreq/expect_test.go | 21 +++++++ httpreq/jq.go | 85 +++++++++++++++++++++++++ httpreq/jq_test.go | 30 +++++++++ httpreq/req-builder.go | 2 +- readme.md | 45 ++++++++++++++ version | 2 +- 12 files changed, 345 insertions(+), 15 deletions(-) create mode 100644 httpreq/expect.go create mode 100644 httpreq/expect_test.go create mode 100644 httpreq/jq.go create mode 100644 httpreq/jq_test.go diff --git a/demo/httpreq/post-file_test.go b/demo/httpreq/post-file_test.go index bc9474a..e9c7a67 100644 --- a/demo/httpreq/post-file_test.go +++ b/demo/httpreq/post-file_test.go @@ -17,7 +17,7 @@ func TestPostFile(t *testing.T) { AddFileHeader("file", "test.txt", []byte("hello world")). AddFile("file2", getTestDataPath("a.txt")). SetReq("GET", "/path"). - ToCurl() + GenCurlCommand() if err != nil { t.Fatal(err) } diff --git a/demo/httpreq/post_test.go b/demo/httpreq/post_test.go index 3dd4783..6269264 100644 --- a/demo/httpreq/post_test.go +++ b/demo/httpreq/post_test.go @@ -14,7 +14,7 @@ func TestPostParams(t *testing.T) { curl, err := httpreq.R(). SetQueryParams(map[string]string{"p": "1"}). SetReq("POST", "http://local/post"). - ToCurl() + GenCurlCommand() if err != nil { t.Fatal(err) } @@ -31,7 +31,7 @@ func TestPostJson(t *testing.T) { curl, err := httpreq.R(). SetJson(map[string]string{"name": "ahuigo"}). SetReq("POST", "/path"). - ToCurl() + GenCurlCommand() if err != nil { t.Fatal(err) } @@ -49,7 +49,7 @@ func TestPostFormUrlEncode(t *testing.T) { curl, err := httpreq.R(). SetFormData(map[string]string{"name": "Alex"}). SetReq("POST", "http://local/post"). - ToCurl() + GenCurlCommand() if err != nil { t.Fatal(err) } @@ -70,7 +70,7 @@ func TestPostFormMultipart(t *testing.T) { SetIsMultiPart(true). SetFormData(map[string]string{"name": "Alex"}). SetReq("POST", "http://local/post"). - ToCurl() + GenCurlCommand() if err != nil { t.Fatal(err) } @@ -89,7 +89,7 @@ func TestPostPlainData(t *testing.T) { SetContentType(httpreq.ContentTypePlain). SetBody([]byte("hello!")). SetReq("POST", "http://local/post"). - ToCurl() + GenCurlCommand() if err != nil { t.Fatal(err) } diff --git a/go.mod b/go.mod index 81f020e..d25be74 100644 --- a/go.mod +++ b/go.mod @@ -2,4 +2,17 @@ module github.com/ahuigo/gohttptool go 1.21.1 -require github.com/pkg/errors v0.9.1 +require ( + github.com/davecgh/go-spew v1.1.1 + github.com/itchyny/gojq v0.12.16 + github.com/pkg/errors v0.9.1 + github.com/samber/lo v1.47.0 + github.com/stretchr/testify v1.9.0 +) + +require ( + github.com/itchyny/timefmt-go v0.1.6 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + golang.org/x/text v0.16.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum index 7c401c3..74c88cc 100644 --- a/go.sum +++ b/go.sum @@ -1,2 +1,20 @@ +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/itchyny/gojq v0.12.16 h1:yLfgLxhIr/6sJNVmYfQjTIv0jGctu6/DgDoivmxTr7g= +github.com/itchyny/gojq v0.12.16/go.mod h1:6abHbdC2uB9ogMS38XsErnfqJ94UlngIJGlRAIj4jTM= +github.com/itchyny/timefmt-go v0.1.6 h1:ia3s54iciXDdzWzwaVKXZPbiXzxxnv1SPGFfM/myJ5Q= +github.com/itchyny/timefmt-go v0.1.6/go.mod h1:RRDZYC5s9ErkjQvTvvU7keJjxUYzIISJGxm9/mAERQg= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/samber/lo v1.47.0 h1:z7RynLwP5nbyRscyvcD043DWYoOcYRv3mV8lBeqOCLc= +github.com/samber/lo v1.47.0/go.mod h1:RmDH9Ct32Qy3gduHQuKJ3gW1fMHAnE/fAzQuf6He5cU= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= +golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/httpreq/curl.go b/httpreq/curl.go index f28844a..4969233 100644 --- a/httpreq/curl.go +++ b/httpreq/curl.go @@ -16,16 +16,16 @@ func (r *RequestBuilder) FromCurl(curl string) { } -func (r *RequestBuilder) ToCurl() (curl string, err error) { - if httpreq, err := r.ToRequest(); err != nil { +func (r *RequestBuilder) GenCurlCommand() (curl string, err error) { + if httpreq, err := r.GenRequest(); err != nil { return "", err } else { - curl := BuildCurlCommand(httpreq, nil) + curl := GenCurlCommand(httpreq, nil) return curl, nil } } -func BuildCurlCommand(req *http.Request, httpCookiejar http.CookieJar) (curlString string) { +func GenCurlCommand(req *http.Request, httpCookiejar http.CookieJar) (curlString string) { buf := acquireBuffer() defer releaseBuffer(buf) @@ -47,8 +47,7 @@ func BuildCurlCommand(req *http.Request, httpCookiejar http.CookieJar) (curlStri // 3. Generate curl body if req.Body != nil { - buf2, _ := io.ReadAll(req.Body) - req.Body = io.NopCloser(bytes.NewBuffer(buf2)) // important!! + buf2 := dumpReqBodyBytes(req) buf.WriteString(`-d ` + shell.Quote(string(buf2))) } @@ -60,6 +59,15 @@ func BuildCurlCommand(req *http.Request, httpCookiejar http.CookieJar) (curlStri return buf.String() } +func dumpReqBodyBytes(req *http.Request) []byte { + if req.Body != nil { + buf, _ := io.ReadAll(req.Body) + req.Body = io.NopCloser(bytes.NewBuffer(buf)) // important!! + return buf + } + return nil +} + // dumpCurlCookies dumps cookies to curl format func dumpCurlCookies(cookies []*http.Cookie) string { sb := strings.Builder{} diff --git a/httpreq/expect.go b/httpreq/expect.go new file mode 100644 index 0000000..4eedba9 --- /dev/null +++ b/httpreq/expect.go @@ -0,0 +1,110 @@ +package httpreq + +import ( + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/samber/lo" +) + +type _ExpectRequest struct { + Headers []string + Method string + Url string + BodyBytes []byte +} +type _ExpectBuilder struct { + req _ExpectRequest + t *testing.T +} +type _Expect struct { + t *testing.T + req _ExpectRequest + *http.Response + resp *httptest.ResponseRecorder +} + +func (rb *RequestBuilder) NewExpectBuilder(t *testing.T) *_ExpectBuilder { + rawreq, err := rb.GenRequest() + if err != nil { + t.Fatal(err) + } + headerArray := dumpCurlHeaders(rawreq) + headers := lo.Map(*headerArray, func(kv [2]string, i int) string { + return kv[0] + ": " + kv[1] + }) + body := dumpReqBodyBytes(rawreq) + if body == nil && rawreq.Method != "GET" { + println("\033[31m Warning: Body is nil, you may need to call NewExpectBuilder() before body is sent\033[0m") + } + req := _ExpectRequest{ + Headers: headers, + Url: rawreq.URL.String(), + Method: rawreq.Method, + BodyBytes: body, + } + return &_ExpectBuilder{ + req: req, + t: t, + } +} + +func (eb *_ExpectBuilder) Build(resp *httptest.ResponseRecorder) *_Expect { + if resp == nil { + eb.t.Error("ResponseRecorder is nil") + } + return &_Expect{ + t: eb.t, + req: eb.req, + // used for test request + // used for test response + Response: resp.Result(), + resp: resp, + } +} + +type HeaderType string + +const ( + HeaderContentType HeaderType = "Content-Type" + HeaderAuthorization HeaderType = "Authorization" + HeaderCookie HeaderType = "Cookie" +) + +func (ep *_Expect) AssertHeaderEqual(prop HeaderType, val string) *_Expect { + if ep.Header.Get(string(prop)) != val { + ep.t.Fatalf("expect header %s: %s, got %s", prop, val, ep.Header.Get(string(prop))) + } + return ep +} + +// ExpectHeaderContains("Cookie", "session=123") +func (ep *_Expect) AssertHeaderContains(prop HeaderType, substr string) *_Expect { + val := ep.Header.Get(string(prop)) + if strings.Contains(val, substr) { + ep.t.Fatalf("expect header %s: %s, got %s", prop, substr, ep.Header.Get(string(prop))) + } + return ep +} +func (ep *_Expect) AssertStatusBetween(start, end int) *_Expect { + status := ep.StatusCode + if status < start || status > end { + ep.t.Fatalf("expect status between %d and %d, got %d", start, end, status) + } + return ep +} +func (ep *_Expect) AssertBodyContains(substr string) *_Expect { + if !strings.Contains(ep.resp.Body.String(), substr) { + ep.t.Fatalf("expect body contains %s, got %s", substr, ep.resp.Body.String()) + } + return ep +} +func (ep *_Expect) AssertBodyJqEqual(expectedVal, expr string) *_Expect { + err := jqEqual(expectedVal, expr, ep.resp.Body.Bytes()) + if err != nil { + ep.t.Fatal(err) + } + return ep +} diff --git a/httpreq/expect_test.go b/httpreq/expect_test.go new file mode 100644 index 0000000..abce95c --- /dev/null +++ b/httpreq/expect_test.go @@ -0,0 +1,21 @@ +package httpreq + +import ( + "bytes" + "net/http/httptest" + "testing" +) + +func TestNewExpectBuilder(t *testing.T) { + rb := R() + resp := httptest.NewRecorder() + resp.Body = bytes.NewBuffer([]byte(`{ + "foo": "bar" + }`)) + // 0. rb.GenCurlCommand() + // 1. build expect + ep := rb.NewExpectBuilder(t).Build(resp) + // 2. assert + ep.AssertBodyContains("foo") + ep.AssertStatusBetween(200, 300) +} \ No newline at end of file diff --git a/httpreq/jq.go b/httpreq/jq.go new file mode 100644 index 0000000..c3ab012 --- /dev/null +++ b/httpreq/jq.go @@ -0,0 +1,85 @@ +package httpreq + +import ( + "encoding/json" + "errors" + "fmt" + "log" + "reflect" + + "github.com/davecgh/go-spew/spew" + "github.com/itchyny/gojq" + + "github.com/stretchr/testify/assert" +) + +func jqRun(expr string, body []byte) (v any, err error) { + // expr = ".foo |.." + // input = map[string]any{"foo": []any{1, 2, 3}} + query, err := gojq.Parse(expr) + if err != nil { + log.Fatalln(err) + } + var input any + if err = json.Unmarshal(body, &input); err != nil { + return string(body), err + } + iter := query.Run(input) // or query.RunWithContext + v, ok := iter.Next() + if !ok { + return nil, errors.New("no value") + } + if err, ok := v.(error); ok { + if err, ok := err.(*gojq.HaltError); ok && err.Value() == nil { + return nil, errors.New("halted") + } + } + return v, nil +} + +type AssertError struct { + Expected any + Actual any + Err error +} + +func (e *AssertError) Error() string { + // es := spew.Dump(e.Expect) + // as := spew.Dump(e.Actual) + + return fmt.Sprintf("assert equal failed:\nexpected: %#v,\nactual: %#v,\n err: %v", + sdump(e.Expected), + sdump(e.Actual), + e.Err) +} + +func jqEqual(expectStr string, expr string, body []byte) (err error) { + var expect any + if err := json.Unmarshal([]byte(expectStr), &expect); err != nil { + return fmt.Errorf("json.Unmarshal data error: %s", expectStr) + } + actual, err := jqRun(expr, body) + if err != nil { + return err + } + // assert.EqualValues(t, expect, actual) + if assert.ObjectsAreEqualValues(expect, actual) { + return nil + } + return &AssertError{Expected: expect, Actual: actual, Err: errors.New("not equal")} +} + +// refer: github.com/stretchr/testify/assert.EqualValues +func sdump(expected any) (e string ){ + et := reflect.TypeOf(expected) + switch et { + case reflect.TypeOf(""): + e = reflect.ValueOf(expected).String() + // case reflect.TypeOf(time.Time{}): + // e = spewConfigStringerEnabled.Sdump(expected) + // a = spewConfigStringerEnabled.Sdump(actual) + default: + e = spew.Sdump(expected) + } + return e +} diff --git a/httpreq/jq_test.go b/httpreq/jq_test.go new file mode 100644 index 0000000..f0ae1e4 --- /dev/null +++ b/httpreq/jq_test.go @@ -0,0 +1,30 @@ +package httpreq + +import ( + "testing" +) + +var ( + jqTestInput = []byte(`{ + "foo": [1, 2, 3] , + "name": "Alex" + } + `) + jqTestCases = []struct{ + expected string + expr string + }{ + { expected: `[1,2,3]`, expr: ".foo |..", }, + { expected: `"Alex"`, expr: ".name",}, + } + +) +func TestJqRun(t *testing.T) { + for _, c := range jqTestCases { + err := jqEqual(c.expected, c.expr, jqTestInput) + if err != nil { + t.Fatal(err) + } + } + +} diff --git a/httpreq/req-builder.go b/httpreq/req-builder.go index 192a527..1474fa1 100644 --- a/httpreq/req-builder.go +++ b/httpreq/req-builder.go @@ -13,7 +13,7 @@ import ( "github.com/pkg/errors" ) -func (rb *RequestBuilder) ToRequest() (*http.Request, error) { +func (rb *RequestBuilder) GenRequest() (*http.Request, error) { var dataType = ContentType(rb.rawreq.Header.Get("Content-Type")) var origurl = rb.url if rb.isMultiPart || len(rb.files) > 0 || len(rb.fileHeaders) > 0 { diff --git a/readme.md b/readme.md index 744b37d..f5c2661 100644 --- a/readme.md +++ b/readme.md @@ -11,3 +11,48 @@ ## Features - [x] Build http request in golang - [x] Generate curl command for http request +- [] Generate regression test cases (and curl) + - [x] Assert + - [] Gen regression rules api+data(curl+request data) + - [] regression test server + - [] regression test ui + +## Unittest with gonic + +``` + +func CreateTestCtx(req *http.Request) (resp *httptest.ResponseRecorder, ctx *gin.Context) { + resp = httptest.NewRecorder() + ctx, _ = gin.CreateTestContext(resp) + ctx.Request = req + return +} + +func TestGonicApi(t *testing.T) { + // 1. build request + req, _ := httpreq.R(). + SetQueryParams(map[string]string{ + "job_id": "1234", + }). + SetReq("GET", "http://any/api/v1/spark/job"). + GenRequest() + curl := httpreq.GenCurlCommand(req, nil) + println(curl) + resp, ctx := CreateTestCtx(req) + + // 2. execute + sparkServer := GetGonicSparkServer() + sparkServer.GetJobInfo(ctx) + if resp.Code != http.StatusOK { + errors := ctx.Errors.Errors() + fmt.Println("output", errors) + t.Errorf("Expect code 200, but get %d body:%v", resp.Code, resp.Body) + } else { + data := map[string]string{} + httpreq.BuildResponse(resp.Result()).Json(&data) + if data["status"] == "" { + t.Fatalf("Bad response: %v", data) + } + } +} +``` diff --git a/version b/version index e1d848b..0ec25f7 100644 --- a/version +++ b/version @@ -1 +1 @@ -v0.0.8 \ No newline at end of file +v1.0.0