Skip to content

Commit

Permalink
feat(test): expect test stage 1 - assert
Browse files Browse the repository at this point in the history
  • Loading branch information
ahuigo committed Oct 28, 2024
1 parent 9a0e05c commit cde30ff
Show file tree
Hide file tree
Showing 12 changed files with 345 additions and 15 deletions.
2 changes: 1 addition & 1 deletion demo/httpreq/post-file_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
10 changes: 5 additions & 5 deletions demo/httpreq/post_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand All @@ -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)
}
Expand All @@ -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)
}
Expand All @@ -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)
}
Expand All @@ -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)
}
Expand Down
15 changes: 14 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
18 changes: 18 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -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=
20 changes: 14 additions & 6 deletions httpreq/curl.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand All @@ -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)))
}

Expand All @@ -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{}
Expand Down
110 changes: 110 additions & 0 deletions httpreq/expect.go
Original file line number Diff line number Diff line change
@@ -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
}
21 changes: 21 additions & 0 deletions httpreq/expect_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
85 changes: 85 additions & 0 deletions httpreq/jq.go
Original file line number Diff line number Diff line change
@@ -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
}
30 changes: 30 additions & 0 deletions httpreq/jq_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
}

}
Loading

0 comments on commit cde30ff

Please sign in to comment.