diff --git a/go.mod b/go.mod index 311707a..53076a3 100644 --- a/go.mod +++ b/go.mod @@ -7,3 +7,5 @@ require ( github.com/fogfish/it/v2 v2.0.1 golang.org/x/net v0.7.0 ) + +require github.com/google/go-cmp v0.5.9 diff --git a/go.sum b/go.sum index bf12fb1..4eb215a 100644 --- a/go.sum +++ b/go.sum @@ -2,5 +2,7 @@ github.com/ajg/form v1.5.2-0.20200323032839-9aeb3cf462e1 h1:8Qzi+0Uch1VJvdrOhJ8U github.com/ajg/form v1.5.2-0.20200323032839-9aeb3cf462e1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= github.com/fogfish/it/v2 v2.0.1 h1:vu3kV2xzYDPHoMHMABxXeu5CoMcTfRc4gkWkzOUkRJY= github.com/fogfish/it/v2 v2.0.1/go.mod h1:h5FdKaEQT4sUEykiVkB8VV4jX27XabFVeWhoDZaRZtE= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= diff --git a/http/recv/arrows.go b/http/recv/arrows.go index c046df5..8091211 100644 --- a/http/recv/arrows.go +++ b/http/recv/arrows.go @@ -20,6 +20,7 @@ import ( "github.com/ajg/form" "github.com/fogfish/gurl/v2" "github.com/fogfish/gurl/v2/http" + "github.com/google/go-cmp/cmp" ) //------------------------------------------------------------------- @@ -39,7 +40,13 @@ func Code(code ...http.StatusCode) http.Arrow { status := cat.Response.StatusCode if !hasCode(code, status) { - return http.NewStatusCode(status, code[0]) + return &gurl.NoMatch{ + ID: "http.Code", + Diff: fmt.Sprintf("+ Status Code: %d\n- Status Code: %d", status, code[0]), + Protocol: "StatusCode", + Expect: code[0], + Actual: status, + } } return nil } @@ -82,7 +89,13 @@ func (StatusCode) eval(code http.StatusCode, cat *http.Context) error { status := cat.Response.StatusCode if !hasCode([]http.StatusCode{code}, status) { - return http.NewStatusCode(status, code) + return &gurl.NoMatch{ + ID: "http.Code", + Diff: fmt.Sprintf("+ Status Code: %d\n- Status Code: %d", status, code), + Protocol: "StatusCode", + Expect: code, + Actual: status, + } } return nil @@ -311,15 +324,21 @@ func match(ctx *http.Context, header string, value string) error { h := ctx.Response.Header.Get(string(header)) if h == "" { return &gurl.NoMatch{ - Diff: fmt.Sprintf("- %s: %s", string(header), value), - Payload: nil, + ID: "http.Header", + Diff: fmt.Sprintf("- %s: %s", string(header), value), + Protocol: header, + Expect: value, + Actual: nil, } } if value != "*" && !strings.HasPrefix(h, value) { return &gurl.NoMatch{ - Diff: fmt.Sprintf("+ %s: %s\n- %s: %s", string(header), h, string(header), value), - Payload: map[string]string{string(header): h}, + ID: "http.Header", + Diff: fmt.Sprintf("+ %s: %s\n- %s: %s", string(header), h, string(header), value), + Protocol: header, + Expect: value, + Actual: h, } } @@ -331,8 +350,9 @@ func liftString(ctx *http.Context, header string, value *string) error { val := ctx.Response.Header.Get(string(header)) if val == "" { return &gurl.NoMatch{ - Diff: fmt.Sprintf("- %s: *", string(header)), - Payload: nil, + ID: "http.Header", + Diff: fmt.Sprintf("- %s: *", string(header)), + Protocol: header, } } @@ -344,8 +364,9 @@ func liftInt(ctx *http.Context, header string, value *int) error { val := ctx.Response.Header.Get(string(header)) if val == "" { return &gurl.NoMatch{ - Diff: fmt.Sprintf("- %s: *", string(header)), - Payload: nil, + ID: "http.Header", + Diff: fmt.Sprintf("- %s: *", string(header)), + Protocol: header, } } @@ -362,8 +383,9 @@ func liftTime(ctx *http.Context, header string, value *time.Time) error { val := ctx.Response.Header.Get(string(header)) if val == "" { return &gurl.NoMatch{ - Diff: fmt.Sprintf("- %s: *", string(header)), - Payload: nil, + ID: "http.Header", + Diff: fmt.Sprintf("- %s: *", string(header)), + Protocol: header, } } @@ -611,15 +633,41 @@ const ( // native Go data structure. The Content-Type header give a hint to decoder. // Supply the pointer to data target data structure. func Recv[T any](out *T) http.Arrow { - return func(cat *http.Context) (err error) { - err = decode( + return func(cat *http.Context) error { + err := decode( cat.Response.Header.Get("Content-Type"), cat.Response.Body, out, ) cat.Response.Body.Close() cat.Response = nil - return + return err + } +} + +func Expect[T any](expect T) http.Arrow { + return func(cat *http.Context) error { + var actual T + err := decode( + cat.Response.Header.Get("Content-Type"), + cat.Response.Body, + &actual, + ) + cat.Response.Body.Close() + cat.Response = nil + + diff := cmp.Diff(actual, expect) + if diff != "" { + return &gurl.NoMatch{ + ID: "http.Recv", + Diff: diff, + Protocol: "body", + Expect: expect, + Actual: actual, + } + } + + return err } } @@ -631,8 +679,10 @@ func decode[T any](content string, stream io.ReadCloser, data *T) error { return form.NewDecoder(stream).Decode(data) default: return &gurl.NoMatch{ - Diff: fmt.Sprintf("- Content-Type: application/*\n+ Content-Type: %s", content), - Payload: map[string]string{"Content-Type": content}, + ID: "http.Recv", + Diff: fmt.Sprintf("- Content-Type: application/{json | www-form}\n+ Content-Type: %s", content), + Protocol: "codec", + Actual: content, } } } @@ -649,13 +699,13 @@ func Bytes(val *[]byte) http.Arrow { // Match received payload to defined pattern func Match(val string) http.Arrow { - var pat map[string]any + var pat any if err := json.Unmarshal([]byte(val), &pat); err != nil { panic(err) } return func(cat *http.Context) (err error) { - var val map[string]any + var val any err = decode( cat.Response.Header.Get("Content-Type"), @@ -665,8 +715,13 @@ func Match(val string) http.Arrow { cat.Response.Body.Close() cat.Response = nil - if !equiv(pat, val) { - return &gurl.NoMatch{} + if !equivVal(pat, val) { + return &gurl.NoMatch{ + ID: "http.Match", + Protocol: "body", + Expect: pat, + Actual: val, + } } return diff --git a/http/recv/arrows_test.go b/http/recv/arrows_test.go index 08bfbff..ba1e51c 100644 --- a/http/recv/arrows_test.go +++ b/http/recv/arrows_test.go @@ -50,12 +50,10 @@ func TestCodeNoMatch(t *testing.T) { ƒ.Status.OK, ) cat := µ.New() - var err interface{ StatusCode() int } - f := func() error { return cat.IO(context.Background(), req) } + err := cat.IO(context.Background(), req) it.Then(t).Should( - it.Fail(f).With(&err), - it.Equal(err.(µ.StatusCode).StatusCode(), µ.StatusBadRequest.StatusCode()), + it.Equal(err.Error(), "+ Status Code: 400\n- Status Code: 200"), ) } @@ -300,6 +298,52 @@ func TestRecvForm(t *testing.T) { ) } +func TestExpectJSON(t *testing.T) { + type Site struct { + Site string `json:"site"` + } + + ts := mock() + defer ts.Close() + + req := µ.GET( + ø.URI("%s/json", ø.Authority(ts.URL)), + ƒ.Status.OK, + ƒ.ContentType.ApplicationJSON, + ƒ.ContentType.JSON, + ƒ.Expect(Site{"example.com"}), + ) + cat := µ.New() + err := cat.IO(context.Background(), req) + + it.Then(t).Should( + it.Nil(err), + ) +} + +func TestExpectJSONFailed(t *testing.T) { + type Site struct { + Site string `json:"site"` + } + + ts := mock() + defer ts.Close() + + req := µ.GET( + ø.URI("%s/json", ø.Authority(ts.URL)), + ƒ.Status.OK, + ƒ.ContentType.ApplicationJSON, + ƒ.ContentType.JSON, + ƒ.Expect(Site{"some.com"}), + ) + cat := µ.New() + err := cat.IO(context.Background(), req) + + it.Then(t).ShouldNot( + it.Equal(err.Error(), ""), + ) +} + func TestRecvBytes(t *testing.T) { ts := mock() defer ts.Close() diff --git a/http/types.go b/http/types.go index 313c89a..01d0244 100644 --- a/http/types.go +++ b/http/types.go @@ -120,8 +120,10 @@ func decode[T any](content string, stream io.ReadCloser, data *T) error { return form.NewDecoder(stream).Decode(data) default: return &gurl.NoMatch{ - Diff: fmt.Sprintf("- Content-Type: application/*\n+ Content-Type: %s", content), - Payload: map[string]string{"Content-Type": content}, + ID: "http.Recv", + Diff: fmt.Sprintf("- Content-Type: application/{json | www-form}\n+ Content-Type: %s", content), + Protocol: "codec", + Actual: content, } } } diff --git a/types.go b/types.go index c412c4d..9b26363 100644 --- a/types.go +++ b/types.go @@ -21,10 +21,11 @@ func (e *NotSupported) Error() string { // Mismatch is returned by api if expectation at body value is failed type NoMatch struct { - Diff string - Payload interface{} + ID string // unique ID of failed combinator + Protocol any // protocol primitive caused failure + Diff string // human readable difference between expected & actual values + Expect any // expected value + Actual any // actual value } -func (e *NoMatch) Error() string { - return e.Diff -} +func (e *NoMatch) Error() string { return e.Diff }