From 430d4d35ac90a94652984161967e224a320bd91c Mon Sep 17 00:00:00 2001 From: Gwendal Leclerc Date: Thu, 5 Oct 2023 00:17:00 +0200 Subject: [PATCH 1/2] feat: add ShouldNotEqualJSON, ShouldEqualUnorderedJSON and ShouldNotEqualUnorderedJSON matchers --- go.mod | 47 +++++--- go.sum | 19 +--- server/admin_server.go | 2 +- server/handlers/utils.go | 4 +- server/middlewares.go | 2 +- server/templates/go_template.go | 6 +- server/templates/utils.go | 4 +- server/types/encoding.go | 2 +- server/types/history.go | 8 +- server/types/matchers.go | 181 ++++++++++++++++++++++++++----- server/types/mock.go | 2 +- tests/data/matcher_mock_list.yml | 25 +++++ tests/features/set_mocks.yml | 31 +++--- tests/features/use_mocks.yml | 69 +++++++++++- 14 files changed, 314 insertions(+), 88 deletions(-) diff --git a/go.mod b/go.mod index 2b73d9a..07ba96c 100644 --- a/go.mod +++ b/go.mod @@ -1,36 +1,55 @@ module github.com/Thiht/smocker -go 1.15 +go 1.21 require ( github.com/Masterminds/sprig/v3 v3.2.2 + github.com/facebookgo/grace v0.0.0-20180706040059-75cf19382434 + github.com/kinbiko/jsonassert v1.1.1 + github.com/labstack/echo/v4 v4.7.0 + github.com/layeh/gopher-json v0.0.0-20190114024228-97fed8db8427 + github.com/namsral/flag v1.7.4-pre + github.com/sirupsen/logrus v1.4.2 + github.com/smarty/assertions v1.15.1 + github.com/stretchr/objx v0.2.0 + github.com/teris-io/shortid v0.0.0-20171029131806-771a37caa5cf + github.com/yuin/gluamapper v0.0.0-20150323120927-d836955830e7 + github.com/yuin/gopher-lua v0.0.0-20191220021717-ab39c6098bdb + golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a + gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b + layeh.com/gopher-luar v1.0.7 +) + +require ( + github.com/Masterminds/goutils v1.1.1 // indirect + github.com/Masterminds/semver/v3 v3.1.1 // indirect github.com/facebookgo/clock v0.0.0-20150410010913-600d898af40a // indirect github.com/facebookgo/ensure v0.0.0-20200202191622-63f1cf65ac4c // indirect github.com/facebookgo/freeport v0.0.0-20150612182905-d4adf43b75b9 // indirect - github.com/facebookgo/grace v0.0.0-20180706040059-75cf19382434 github.com/facebookgo/httpdown v0.0.0-20180706035922-5979d39b15c2 // indirect github.com/facebookgo/stack v0.0.0-20160209184415-751773369052 // indirect github.com/facebookgo/stats v0.0.0-20151006221625-1b76add642e4 // indirect github.com/facebookgo/subset v0.0.0-20200203212716-c811ad88dec4 // indirect + github.com/golang-jwt/jwt v3.2.2+incompatible // indirect + github.com/google/uuid v1.1.1 // indirect + github.com/huandu/xstrings v1.3.1 // indirect + github.com/imdario/mergo v0.3.11 // indirect github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect github.com/kr/pretty v0.1.0 // indirect - github.com/labstack/echo/v4 v4.7.0 - github.com/layeh/gopher-json v0.0.0-20190114024228-97fed8db8427 + github.com/labstack/gommon v0.3.1 // indirect github.com/mattn/go-colorable v0.1.12 // indirect + github.com/mattn/go-isatty v0.0.14 // indirect + github.com/mitchellh/copystructure v1.0.0 // indirect github.com/mitchellh/mapstructure v1.1.2 // indirect github.com/mitchellh/reflectwalk v1.0.1 // indirect - github.com/namsral/flag v1.7.4-pre - github.com/sirupsen/logrus v1.4.2 - github.com/smartystreets/assertions v1.0.1 - github.com/stretchr/objx v0.2.0 - github.com/teris-io/shortid v0.0.0-20171029131806-771a37caa5cf - github.com/yuin/gluamapper v0.0.0-20150323120927-d836955830e7 - github.com/yuin/gopher-lua v0.0.0-20191220021717-ab39c6098bdb + github.com/shopspring/decimal v1.2.0 // indirect + github.com/spf13/cast v1.3.1 // indirect + github.com/valyala/bytebufferpool v1.0.0 // indirect + github.com/valyala/fasttemplate v1.2.1 // indirect golang.org/x/crypto v0.0.0-20220307211146-efcb8507fb70 // indirect golang.org/x/net v0.0.0-20220225172249-27dd8689420f // indirect - golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a golang.org/x/sys v0.0.0-20220307203707-22a9840ba4d7 // indirect + golang.org/x/text v0.3.7 // indirect + golang.org/x/time v0.0.0-20201208040808-7e3f01d25324 // indirect gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect - gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b - layeh.com/gopher-luar v1.0.7 ) diff --git a/go.sum b/go.sum index b5550c2..360c4ab 100644 --- a/go.sum +++ b/go.sum @@ -34,6 +34,8 @@ github.com/huandu/xstrings v1.3.1 h1:4jgBlKK6tLKFvO8u5pmYjG91cqytmDCDvGh7ECVFfFs github.com/huandu/xstrings v1.3.1/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= github.com/imdario/mergo v0.3.11 h1:3tnifQM4i+fbajXKBHXWEH+KvNHqojZ778UH75j3bGA= github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= +github.com/kinbiko/jsonassert v1.1.1 h1:DB12divY+YB+cVpHULLuKePSi6+ui4M/shHSzJISkSE= +github.com/kinbiko/jsonassert v1.1.1/go.mod h1:NO4lzrogohtIdNUNzx8sdzB55M4R4Q1bsrWVdqQ7C+A= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s= github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= @@ -68,8 +70,8 @@ github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXY github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= -github.com/smartystreets/assertions v1.0.1 h1:voD4ITNjPL5jjBfgR/r8fPIIBrliWrWHeiJApdr3r4w= -github.com/smartystreets/assertions v1.0.1/go.mod h1:kHHU4qYBaI3q23Pp3VPrmWhuIUrLW/7eUrw0BU5VaoM= +github.com/smarty/assertions v1.15.1 h1:812oFiXI+G55vxsFf+8bIZ1ux30qtkdqzKbEFwyX3Tk= +github.com/smarty/assertions v1.15.1/go.mod h1:yABtdzeQs6l1brC900WlRNwj6ZR55d7B+E8C6HtKdec= github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng= github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -94,13 +96,9 @@ github.com/yuin/gopher-lua v0.0.0-20191220021717-ab39c6098bdb h1:ZkM6LRnq40pR1Ox github.com/yuin/gopher-lua v0.0.0-20191220021717-ab39c6098bdb/go.mod h1:gqRgreBUhTSL0GeU64rtZ3Uq3wtjOa/TB2YfrtkCbVQ= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20200414173820-0848c9571904/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220307211146-efcb8507fb70 h1:syTAU9FwmvzEoIYMqcPHOcVm4H3U5u90WsvuYgwpETU= golang.org/x/crypto v0.0.0-20220307211146-efcb8507fb70/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220225172249-27dd8689420f h1:oA4XRj0qtSt8Yo1Zms0CUlsT3KG69V2UGQWPBxujDmc= golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a h1:WXEvlFVvvGxCJLG6REjsT03iWnKLEWinaScsxF2Vm2o= @@ -109,25 +107,16 @@ golang.org/x/sys v0.0.0-20190204203706-41f3e6584952/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220307203707-22a9840ba4d7 h1:8IVLkfbr2cLhv0a/vKq4UFUcJym8RmDoDboxCFWEjYE= golang.org/x/sys v0.0.0-20220307203707-22a9840ba4d7/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/time v0.0.0-20201208040808-7e3f01d25324 h1:Hir2P/De0WpUhtrKGGjvSb2YxUgyZ7EFOSLIcSSpiwE= golang.org/x/time v0.0.0-20201208040808-7e3f01d25324/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/server/admin_server.go b/server/admin_server.go index 0123849..4d9a049 100644 --- a/server/admin_server.go +++ b/server/admin_server.go @@ -22,7 +22,7 @@ type TemplateRenderer struct { } // Render renders a template document -func (t *TemplateRenderer) Render(w io.Writer, name string, data interface{}, c echo.Context) error { +func (t *TemplateRenderer) Render(w io.Writer, name string, data any, c echo.Context) error { return t.ExecuteTemplate(w, name, data) } diff --git a/server/handlers/utils.go b/server/handlers/utils.go index 568a369..d796d72 100644 --- a/server/handlers/utils.go +++ b/server/handlers/utils.go @@ -11,7 +11,7 @@ import ( const MIMEApplicationXYaml = "application/x-yaml" -func bindAccordingAccept(c echo.Context, res interface{}) error { +func bindAccordingAccept(c echo.Context, res any) error { if err := c.Bind(res); err != nil { if err != echo.ErrUnsupportedMediaType { log.WithError(err).Error("Failed to parse payload") @@ -32,7 +32,7 @@ func bindAccordingAccept(c echo.Context, res interface{}) error { return nil } -func respondAccordingAccept(c echo.Context, body interface{}) error { +func respondAccordingAccept(c echo.Context, body any) error { accept := c.Request().Header.Get(echo.HeaderAccept) if strings.Contains(strings.ToLower(accept), MIMEApplicationXYaml) { c.Response().Header().Set(echo.HeaderContentType, MIMEApplicationXYaml) diff --git a/server/middlewares.go b/server/middlewares.go index af744ee..b803f19 100644 --- a/server/middlewares.go +++ b/server/middlewares.go @@ -73,7 +73,7 @@ func HistoryMiddleware(s services.Mocks) echo.MiddlewareFunc { } } - var body interface{} + var body any if err := json.Unmarshal(responseBytes, &body); err != nil { body = string(responseBytes) } diff --git a/server/templates/go_template.go b/server/templates/go_template.go index 4fa2741..3207976 100644 --- a/server/templates/go_template.go +++ b/server/templates/go_template.go @@ -26,7 +26,7 @@ func (*goTemplateYamlEngine) Execute(request types.Request, script string) (*typ } buffer := new(bytes.Buffer) - if err = tmpl.Execute(buffer, map[string]interface{}{"Request": request}); err != nil { + if err = tmpl.Execute(buffer, map[string]any{"Request": request}); err != nil { log.WithError(err).Error("Failed to execute dynamic template") return nil, fmt.Errorf("failed to execute dynamic template: %w", err) } @@ -53,12 +53,12 @@ func (*goTemplateJsonEngine) Execute(request types.Request, script string) (*typ } buffer := new(bytes.Buffer) - if err = tmpl.Execute(buffer, map[string]interface{}{"Request": request}); err != nil { + if err = tmpl.Execute(buffer, map[string]any{"Request": request}); err != nil { log.WithError(err).Error("Failed to execute dynamic template") return nil, fmt.Errorf("failed to execute dynamic template: %w", err) } - var tmplResult map[string]interface{} + var tmplResult map[string]any if err = json.Unmarshal(buffer.Bytes(), &tmplResult); err != nil { log.WithError(err).Error("Failed to unmarshal response from dynamic template") return nil, fmt.Errorf("failed to unmarshal response from dynamic template: %w", err) diff --git a/server/templates/utils.go b/server/templates/utils.go index bfe9fbc..0e84eee 100644 --- a/server/templates/utils.go +++ b/server/templates/utils.go @@ -2,12 +2,12 @@ package templates import "encoding/json" -func StructToMSI(s interface{}) (map[string]interface{}, error) { +func StructToMSI(s any) (map[string]any, error) { bytes, err := json.Marshal(s) if err != nil { return nil, err } - msi := map[string]interface{}{} + msi := map[string]any{} err = json.Unmarshal(bytes, &msi) if err != nil { return nil, err diff --git a/server/types/encoding.go b/server/types/encoding.go index a342549..8c4b393 100644 --- a/server/types/encoding.go +++ b/server/types/encoding.go @@ -26,7 +26,7 @@ func (ss *StringSlice) UnmarshalJSON(data []byte) error { return nil } -func (ss *StringSlice) UnmarshalYAML(unmarshal func(interface{}) error) error { +func (ss *StringSlice) UnmarshalYAML(unmarshal func(any) error) error { var str string if err := unmarshal(&str); err == nil { *ss = append(*ss, str) diff --git a/server/types/history.go b/server/types/history.go index f526bf4..8e3e3a2 100644 --- a/server/types/history.go +++ b/server/types/history.go @@ -38,7 +38,7 @@ type Request struct { Method string `json:"method"` Origin string `json:"origin"` BodyString string `json:"body_string" yaml:"body_string"` - Body interface{} `json:"body,omitempty" yaml:"body,omitempty"` + Body any `json:"body,omitempty" yaml:"body,omitempty"` QueryParams url.Values `json:"query_params,omitempty" yaml:"query_params,omitempty"` Headers http.Header `json:"headers,omitempty" yaml:"headers,omitempty"` Date time.Time `json:"date" yaml:"date"` @@ -46,7 +46,7 @@ type Request struct { type Response struct { Status int `json:"status"` - Body interface{} `json:"body,omitempty" yaml:"body,omitempty"` + Body any `json:"body,omitempty" yaml:"body,omitempty"` Headers http.Header `json:"headers,omitempty" yaml:"headers,omitempty"` Date time.Time `json:"date" yaml:"date"` } @@ -61,8 +61,8 @@ func HTTPRequestToRequest(req *http.Request) Request { } } req.Body = io.NopCloser(bytes.NewBuffer(bodyBytes)) - var body interface{} - var tmp map[string]interface{} + var body any + var tmp map[string]any if err := json.Unmarshal(bodyBytes, &tmp); err != nil { body = string(bodyBytes) } else { diff --git a/server/types/matchers.go b/server/types/matchers.go index 620b714..8cbae55 100644 --- a/server/types/matchers.go +++ b/server/types/matchers.go @@ -1,45 +1,83 @@ package types import ( + "bytes" "encoding/json" "fmt" "net/http" "net/url" "regexp" + "github.com/kinbiko/jsonassert" log "github.com/sirupsen/logrus" - "github.com/smartystreets/assertions" + "github.com/smarty/assertions" "github.com/stretchr/objx" ) +type buffer struct { + bb bytes.Buffer +} + +func (b *buffer) Errorf(msg string, args ...interface{}) { + b.bb.WriteString(fmt.Sprintf(msg, args...)) +} + +func (b *buffer) String() string { + return b.bb.String() +} + const ( DefaultMatcher = "ShouldEqual" ) -type Assertion func(actual interface{}, expected ...interface{}) string +type Assertion func(actual any, expected ...any) string var asserts = map[string]Assertion{ - "ShouldResemble": assertions.ShouldResemble, - "ShouldAlmostEqual": assertions.ShouldAlmostEqual, - "ShouldContainSubstring": assertions.ShouldContainSubstring, - "ShouldEndWith": assertions.ShouldEndWith, - "ShouldEqual": assertions.ShouldEqual, - "ShouldEqualJSON": assertions.ShouldEqualJSON, - "ShouldStartWith": assertions.ShouldStartWith, - "ShouldBeEmpty": ShouldBeEmpty, - "ShouldMatch": ShouldMatch, - - "ShouldNotResemble": assertions.ShouldNotResemble, - "ShouldNotAlmostEqual": assertions.ShouldNotAlmostEqual, - "ShouldNotContainSubstring": assertions.ShouldNotContainSubstring, - "ShouldNotEndWith": assertions.ShouldNotEndWith, - "ShouldNotEqual": assertions.ShouldNotEqual, - "ShouldNotStartWith": assertions.ShouldNotStartWith, - "ShouldNotBeEmpty": ShouldNotBeEmpty, - "ShouldNotMatch": ShouldNotMatch, + "ShouldResemble": assertions.ShouldResemble, + "ShouldAlmostEqual": assertions.ShouldAlmostEqual, + "ShouldContainSubstring": assertions.ShouldContainSubstring, + "ShouldEndWith": assertions.ShouldEndWith, + "ShouldEqual": assertions.ShouldEqual, + "ShouldEqualJSON": assertions.ShouldEqualJSON, + "ShouldEqualUnorderedJSON": ShouldEqualUnorderedJSON, + "ShouldStartWith": assertions.ShouldStartWith, + "ShouldBeEmpty": ShouldBeEmpty, + "ShouldMatch": ShouldMatch, + + "ShouldNotResemble": assertions.ShouldNotResemble, + "ShouldNotAlmostEqual": assertions.ShouldNotAlmostEqual, + "ShouldNotContainSubstring": assertions.ShouldNotContainSubstring, + "ShouldNotEndWith": assertions.ShouldNotEndWith, + "ShouldNotEqual": assertions.ShouldNotEqual, + "ShouldNotEqualJSON": ShouldNotEqualJSON, + "ShouldNotEqualUnorderedJSON": ShouldNotEqualUnorderedJSON, + "ShouldNotStartWith": assertions.ShouldNotStartWith, + "ShouldNotBeEmpty": ShouldNotBeEmpty, + "ShouldNotMatch": ShouldNotMatch, } -func ShouldMatch(value interface{}, patterns ...interface{}) string { +func prepareUnorderedJSON(value any) any { + switch v := value.(type) { + case map[string]any: + for key, val := range v { + v[key] = prepareUnorderedJSON(val) + } + return v + case []any: + if len(v) < 2 { + return v + } + res := make([]any, 0, len(v)+1) + res = append(res, "<>") // Key element used by jsonassert to allow unordered lists + for _, val := range v { + res = append(res, prepareUnorderedJSON(val)) + } + return res + } + return value +} + +func ShouldMatch(value any, patterns ...any) string { valueString, ok := value.(string) if !ok { return "ShouldMatch works only with strings" @@ -59,15 +97,44 @@ func ShouldMatch(value interface{}, patterns ...interface{}) string { return "" } -func ShouldBeEmpty(value interface{}, patterns ...interface{}) string { +func ShouldBeEmpty(value any, patterns ...any) string { return assertions.ShouldBeEmpty(value) } -func ShouldNotBeEmpty(value interface{}, patterns ...interface{}) string { +func ShouldEqualUnorderedJSON(value any, expected ...any) string { + valueString, ok := value.(string) + if !ok { + return "ShouldEqualUnorderedJSON works only with strings" + } + if len(expected) != 1 { + return "ShouldEqualUnorderedJSON requires exactly one comparison value" + } + expectedString, ok := expected[0].(string) + if !ok { + return "ShouldEqualUnorderedJSON works only with strings" + } + var v any + if err := json.Unmarshal([]byte(valueString), &v); err != nil { + return "Received value is not valid JSON" + } + if err := json.Unmarshal([]byte(expectedString), &v); err != nil { + return "Expected value is not valid JSON" + } + b, err := json.Marshal(prepareUnorderedJSON(v)) + if err != nil { + return "ShouldEqualUnorderedJSON failed to prepare expected JSON" + } + buf := buffer{} + ja := jsonassert.New(&buf) + ja.Assertf(valueString, string(b)) + return buf.String() +} + +func ShouldNotBeEmpty(value any, patterns ...any) string { return assertions.ShouldNotBeEmpty(value) } -func ShouldNotMatch(value interface{}, patterns ...interface{}) string { +func ShouldNotMatch(value any, patterns ...any) string { valueString, ok := value.(string) if !ok { return "ShouldNotMatch works only with strings" @@ -87,6 +154,64 @@ func ShouldNotMatch(value interface{}, patterns ...interface{}) string { return "" } +func ShouldNotEqualJSON(value any, expected ...any) string { + valueString, ok := value.(string) + if !ok { + return "ShouldNotEqualJSON works only with strings" + } + if len(expected) != 1 { + return "ShouldNotEqualJSON requires exactly one comparison value" + } + expectedString, ok := expected[0].(string) + if !ok { + return "ShouldNotEqualJSON works only with strings" + } + var v any + if err := json.Unmarshal([]byte(valueString), &v); err != nil { + return "Received value is not valid JSON" + } + if err := json.Unmarshal([]byte(expectedString), &v); err != nil { + return "Expected value is not valid JSON" + } + + if res := assertions.ShouldEqualJSON(value, expected...); res == "" { + return fmt.Sprintf("Expected %q to not be equal to %q (but it did)!", valueString, expectedString) + } + return "" +} + +func ShouldNotEqualUnorderedJSON(value any, expected ...any) string { + valueString, ok := value.(string) + if !ok { + return "ShouldNotEqualUnorderedJSON works only with strings" + } + if len(expected) != 1 { + return "ShouldNotEqualUnorderedJSON requires exactly one comparison value" + } + expectedString, ok := expected[0].(string) + if !ok { + return "ShouldNotEqualUnorderedJSON works only with strings" + } + var v any + if err := json.Unmarshal([]byte(valueString), &v); err != nil { + return "Received value is not valid JSON" + } + if err := json.Unmarshal([]byte(expectedString), &v); err != nil { + return "Expected value is not valid JSON" + } + b, err := json.Marshal(prepareUnorderedJSON(v)) + if err != nil { + return "ShouldNotEqualUnorderedJSON failed to prepare expected JSON" + } + buf := buffer{} + ja := jsonassert.New(&buf) + ja.Assertf(valueString, string(b)) + if buf.String() == "" { + return fmt.Sprintf("Expected JSON value %q to not be equivalent to JSON value %q regardless of list order (but it did)!", valueString, expectedString) + } + return "" +} + type StringMatcher struct { Matcher string `json:"matcher" yaml:"matcher,flow"` Value string `json:"value" yaml:"value,flow"` @@ -143,7 +268,7 @@ func (sm *StringMatcher) UnmarshalJSON(data []byte) error { return sm.Validate() } -func (sm *StringMatcher) UnmarshalYAML(unmarshal func(interface{}) error) error { +func (sm *StringMatcher) UnmarshalYAML(unmarshal func(any) error) error { var s string if err := unmarshal(&s); err == nil { sm.Matcher = DefaultMatcher @@ -210,7 +335,7 @@ func (sms *StringMatcherSlice) UnmarshalJSON(data []byte) error { return nil } -func (sms *StringMatcherSlice) UnmarshalYAML(unmarshal func(interface{}) error) error { +func (sms *StringMatcherSlice) UnmarshalYAML(unmarshal func(any) error) error { var s string if err := unmarshal(&s); err == nil { *sms = []StringMatcher{{ @@ -310,14 +435,14 @@ func (bm *BodyMatcher) UnmarshalJSON(data []byte) error { return nil } -func (bm BodyMatcher) MarshalYAML() (interface{}, error) { +func (bm BodyMatcher) MarshalYAML() (any, error) { if bm.bodyString != nil { return bm.bodyString, nil } return bm.bodyJson, nil } -func (bm *BodyMatcher) UnmarshalYAML(unmarshal func(interface{}) error) error { +func (bm *BodyMatcher) UnmarshalYAML(unmarshal func(any) error) error { var s StringMatcher if err := unmarshal(&s); err == nil { if _, ok := asserts[s.Matcher]; ok { diff --git a/server/types/mock.go b/server/types/mock.go index 83e62a8..1b16551 100644 --- a/server/types/mock.go +++ b/server/types/mock.go @@ -181,7 +181,7 @@ func (d *Delay) UnmarshalJSON(data []byte) error { return d.validate() } -func (d *Delay) UnmarshalYAML(unmarshal func(interface{}) error) error { +func (d *Delay) UnmarshalYAML(unmarshal func(any) error) error { var s time.Duration if err := unmarshal(&s); err == nil { d.Min = s diff --git a/tests/data/matcher_mock_list.yml b/tests/data/matcher_mock_list.yml index 86f1c03..4892b2e 100644 --- a/tests/data/matcher_mock_list.yml +++ b/tests/data/matcher_mock_list.yml @@ -95,3 +95,28 @@ response: body: > {"message": "test13"} +- request: + path: /test14 + body: + matcher: ShouldNotEqualJSON + value: > + {"id": 1} + response: + body: > + {"message": "test15"} +- request: + path: /test16 + body: + matcher: ShouldEqualUnorderedJSON + value: '[{"messages": ["message4", "message3"]}, {"messages": ["message2", "message1"]}]' + response: + body: > + {"message": "test17"} +- request: + path: /test18 + body: + matcher: ShouldNotEqualUnorderedJSON + value: '[{"messages": ["message4", "message3"]}, {"messages": ["message2", "message1"]}]' + response: + body: > + {"message": "test19"} diff --git a/tests/features/set_mocks.yml b/tests/features/set_mocks.yml index 0ae1818..9f4da71 100644 --- a/tests/features/set_mocks.yml +++ b/tests/features/set_mocks.yml @@ -105,20 +105,23 @@ testcases: url: http://localhost:8081/mocks assertions: - result.statuscode ShouldEqual 200 - - result.bodyjson.__len__ ShouldEqual 9 - - result.bodyjson.bodyjson8.request.path.matcher ShouldEqual "ShouldMatch" - - result.bodyjson.bodyjson8.request.path.value ShouldEqual "/.*" - - result.bodyjson.bodyjson7.request.method.matcher ShouldEqual "ShouldContainSubstring" - - result.bodyjson.bodyjson7.request.method.value ShouldEqual "PO" - - result.bodyjson.bodyjson6.request.body.matcher ShouldEqual "ShouldEqualJSON" - - result.bodyjson.bodyjson6.request.body.value ShouldContainSubstring id - - result.bodyjson.bodyjson5.request.headers.content-type.content-type0.matcher ShouldEqual "ShouldMatch" - - result.bodyjson.bodyjson5.request.headers.content-type.content-type0.value ShouldEqual application/.* - - result.bodyjson.bodyjson4.request.query_params.test.test0.value ShouldEqual true - - result.bodyjson.bodyjson3.request.body.matcher ShouldEqual "ShouldNotBeEmpty" - - result.bodyjson.bodyjson2.request.query_params.test.test0.value ShouldEqual true - - result.bodyjson.bodyjson1.request.body.key1.value ShouldEqual test - - result.bodyjson.bodyjson0.request.body.key1[0].value ShouldEqual test + - result.bodyjson.__len__ ShouldEqual 12 + - result.bodyjson.bodyjson0.request.body.matcher ShouldEqual ShouldNotEqualUnorderedJSON + - result.bodyjson.bodyjson1.request.body.matcher ShouldEqual ShouldEqualUnorderedJSON + - result.bodyjson.bodyjson2.request.body.matcher ShouldEqual ShouldNotEqualJSON + - result.bodyjson.bodyjson3.request.body.key1[0].value ShouldEqual test + - result.bodyjson.bodyjson4.request.body.key1.value ShouldEqual test + - result.bodyjson.bodyjson5.request.query_params.test.test0.value ShouldEqual true + - result.bodyjson.bodyjson6.request.body.matcher ShouldEqual "ShouldNotBeEmpty" + - result.bodyjson.bodyjson7.request.query_params.test.test0.value ShouldEqual true + - result.bodyjson.bodyjson8.request.headers.content-type.content-type0.matcher ShouldEqual "ShouldMatch" + - result.bodyjson.bodyjson8.request.headers.content-type.content-type0.value ShouldEqual application/.* + - result.bodyjson.bodyjson9.request.body.matcher ShouldEqual "ShouldEqualJSON" + - result.bodyjson.bodyjson9.request.body.value ShouldContainSubstring id + - result.bodyjson.bodyjson10.request.method.matcher ShouldEqual "ShouldContainSubstring" + - result.bodyjson.bodyjson10.request.method.value ShouldEqual "PO" + - result.bodyjson.bodyjson11.request.path.matcher ShouldEqual "ShouldMatch" + - result.bodyjson.bodyjson11.request.path.value ShouldEqual "/.*" - name: Add dynamic mocks steps: diff --git a/tests/features/use_mocks.yml b/tests/features/use_mocks.yml index 8b4c236..9c55623 100644 --- a/tests/features/use_mocks.yml +++ b/tests/features/use_mocks.yml @@ -157,7 +157,6 @@ testcases: assertions: - result.statuscode ShouldEqual 200 - result.bodyjson.message ShouldEqual test11 - - type: http method: POST url: http://localhost:8080/test10 @@ -175,7 +174,6 @@ testcases: assertions: - result.statuscode ShouldEqual 200 - result.bodyjson.message ShouldEqual test13 - - type: http method: POST url: http://localhost:8080/test12 @@ -185,6 +183,73 @@ testcases: assertions: - result.statuscode ShouldEqual 666 + - type: http + method: POST + url: http://localhost:8080/test14 + body: '{"id": 2}' + assertions: + - result.statuscode ShouldEqual 200 + - result.bodyjson.message ShouldEqual test15 + - type: http + method: POST + url: http://localhost:8080/test14 + body: '{"id": 1}' + assertions: + - result.statuscode ShouldEqual 666 + - type: http + method: POST + url: http://localhost:8080/test14 + body: test # The ShouldNotEqualJSON matcher should not work with invalid JSON + assertions: + - result.statuscode ShouldEqual 666 + + - type: http + method: POST + url: http://localhost:8080/test16 + body: '[{"messages": ["message1", "message2"]}, {"messages": ["message3", "message4"]}]' + assertions: + - result.statuscode ShouldEqual 200 + - result.bodyjson.message ShouldEqual test17 + - type: http + method: POST + url: http://localhost:8080/test16 + body: '[{"messages": ["message4", "message3"]}, {"messages": ["message2", "message1"]}]' + assertions: + - result.statuscode ShouldEqual 200 + - result.bodyjson.message ShouldEqual test17 + - type: http + method: POST + url: http://localhost:8080/test16 + body: '{"id": 1}' + assertions: + - result.statuscode ShouldEqual 666 + + - type: http + method: POST + url: http://localhost:8080/test18 + body: '{"id": 1}' + assertions: + - result.statuscode ShouldEqual 200 + - result.bodyjson.message ShouldEqual test19 + - type: http + method: POST + url: http://localhost:8080/test18 + body: '[{"messages": ["message1", "message2"]}, {"messages": ["message3", "message4"]}]' + assertions: + - result.statuscode ShouldEqual 666 + - type: http + method: POST + url: http://localhost:8080/test18 + body: '[{"messages": ["message4", "message3"]}, {"messages": ["message2", "message1"]}]' + assertions: + - result.statuscode ShouldEqual 666 + - type: http + method: POST + url: http://localhost:8080/test18 + body: test # The ShouldNotEqualUnorderedJSON matcher should not work with invalid JSON + assertions: + - result.statuscode ShouldEqual 666 + - name: Use dynamic mock list steps: - type: http From 57f6e51680610f1e17a174acb8324874ecf72753 Mon Sep 17 00:00:00 2001 From: Gwendal Leclerc Date: Thu, 5 Oct 2023 00:25:08 +0200 Subject: [PATCH 2/2] fix: lint errors --- server/handlers/mocks.go | 4 ++-- server/types/mock.go | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/server/handlers/mocks.go b/server/handlers/mocks.go index aa1fb4d..80f387c 100644 --- a/server/handlers/mocks.go +++ b/server/handlers/mocks.go @@ -130,9 +130,9 @@ func (m *Mocks) GenericHandler(c echo.Context) error { // Delay var delay time.Duration if response.Delay.Min != response.Delay.Max { - rand.Seed(time.Now().Unix()) + random := rand.New(rand.NewSource(time.Now().Unix())) var n int64 = int64(response.Delay.Max - response.Delay.Min) - delay = time.Duration(rand.Int63n(n) + int64(response.Delay.Min)) + delay = time.Duration(random.Int63n(n) + int64(response.Delay.Min)) } else { delay = response.Delay.Min } diff --git a/server/types/mock.go b/server/types/mock.go index 1b16551..657752a 100644 --- a/server/types/mock.go +++ b/server/types/mock.go @@ -5,7 +5,7 @@ import ( "encoding/json" "errors" "fmt" - "io/ioutil" + "io" "net/http" "net/url" "strings" @@ -250,7 +250,7 @@ func (mp MockProxy) Redirect(req Request) (*MockResponse, error) { return nil, err } defer resp.Body.Close() - body, err := ioutil.ReadAll(resp.Body) + body, err := io.ReadAll(resp.Body) if err != nil { return nil, err }