diff --git a/Readme.md b/Readme.md index bbabbab4..cd031197 100644 --- a/Readme.md +++ b/Readme.md @@ -122,7 +122,7 @@ Stub will respond with the expected response only if the request matches any rul So if you do a `curl -X POST -d '{"service":"Greeter","method":"SayHello","data":{"name":"gripmock"}}' localhost:4771/find` stub service will find a match from listed stubs stored there. ### Input Matching Rule -Input matching has 3 rules to match an input: **equals**,**contains** and **regex** +Input matching has 4 rules to match an input: **equals**, **equals_unordered**, **contains** and **regex**
Nested fields are allowed for input matching too for all JSON data types. (`string`, `bool`, `array`, etc.)
@@ -151,6 +151,31 @@ Nested fields are allowed for input matching too for all JSON data types. (`stri } ``` +**equals_unordered** will match the exact field name and value of input into expected stub, except lists (which are compared as sets). example stub JSON: + + +``` +{ + . + . + "input":{ + "equals_unordered":{ + "name":"gripmock", + "greetings": { + "english": "Hello World!", + "indonesian": "Halo Dunia!", + "turkish": "Merhaba Dünya!" + }, + "ok": true, + "numbers": [4, 8, 15, 16, 23, 42] + "null": null + } + } + . + . +} +``` + **contains** will match input that has the value declared expected fields. example stub JSON: ``` { diff --git a/stub/storage.go b/stub/storage.go index 4faf810d..2d21637c 100644 --- a/stub/storage.go +++ b/stub/storage.go @@ -7,6 +7,7 @@ import ( "log" "reflect" "regexp" + "sort" "sync" "github.com/lithammer/fuzzysearch/fuzzy" @@ -81,6 +82,13 @@ func findStub(stub *findStubPayload) (*Output, error) { } } + if expect := stubrange.Input.EqualsUnordered; expect != nil { + closestMatch = append(closestMatch, closeMatch{"equals_unordered", expect}) + if equalsUnordered(stub.Data, expect) { + return &stubrange.Output, nil + } + } + if expect := stubrange.Input.Contains; expect != nil { closestMatch = append(closestMatch, closeMatch{"contains", expect}) if contains(stubrange.Input.Contains, stub.Data) { @@ -186,18 +194,40 @@ func regexMatch(expect, actual interface{}) bool { } func equals(expect, actual map[string]interface{}) bool { - return find(expect, actual, true, true, deepEqual) + return find(expect, actual, true, true, deepEqual, false) +} + +func equalsUnordered(expect, actual map[string]interface{}) bool { + return find(expect, actual, true, true, deepEqual, true) } func contains(expect, actual map[string]interface{}) bool { - return find(expect, actual, true, false, deepEqual) + return find(expect, actual, true, false, deepEqual, false) } func matches(expect, actual map[string]interface{}) bool { - return find(expect, actual, true, false, regexMatch) + return find(expect, actual, true, false, regexMatch, false) +} + +func equalsIgnoreOrder(expect, actual interface{}) bool { + expectSlice, expectOk := expect.([]interface{}) + actualSlice, actualOk := actual.([]interface{}) + if !expectOk || !actualOk { + return false + } + if len(expectSlice) != len(actualSlice) { + return false + } + sort.Slice(expectSlice, func(i, j int) bool { + return fmt.Sprint(expectSlice[i]) < fmt.Sprint(expectSlice[j]) + }) + sort.Slice(actualSlice, func(i, j int) bool { + return fmt.Sprint(actualSlice[i]) < fmt.Sprint(actualSlice[j]) + }) + return reflect.DeepEqual(expectSlice, actualSlice) } -func find(expect, actual interface{}, acc, exactMatch bool, f matchFunc) bool { +func find(expect, actual interface{}, acc, exactMatch bool, f matchFunc, ignoreOrder bool) bool { // circuit brake if acc == false { @@ -225,9 +255,13 @@ func find(expect, actual interface{}, acc, exactMatch bool, f matchFunc) bool { } } + if expectArrayOk && actualArrayOk && ignoreOrder { + return equalsIgnoreOrder(expectArrayValue, actualArrayValue) + } + for expectItemIndex, expectItemValue := range expectArrayValue { actualItemValue := actualArrayValue[expectItemIndex] - acc = find(expectItemValue, actualItemValue, acc, exactMatch, f) + acc = find(expectItemValue, actualItemValue, acc, exactMatch, f, ignoreOrder) } return acc @@ -256,7 +290,7 @@ func find(expect, actual interface{}, acc, exactMatch bool, f matchFunc) bool { for expectItemKey, expectItemValue := range expectMapValue { actualItemValue := actualMapValue[expectItemKey] - acc = find(expectItemValue, actualItemValue, acc, exactMatch, f) + acc = find(expectItemValue, actualItemValue, acc, exactMatch, f, ignoreOrder) } return acc diff --git a/stub/stub.go b/stub/stub.go index 5509e7ea..541e35ea 100644 --- a/stub/stub.go +++ b/stub/stub.go @@ -55,9 +55,10 @@ type Stub struct { } type Input struct { - Equals map[string]interface{} `json:"equals"` - Contains map[string]interface{} `json:"contains"` - Matches map[string]interface{} `json:"matches"` + Equals map[string]interface{} `json:"equals"` + EqualsUnordered map[string]interface{} `json:"equals_unordered"` + Contains map[string]interface{} `json:"contains"` + Matches map[string]interface{} `json:"matches"` } type Output struct { @@ -118,6 +119,8 @@ func validateStub(stub *Stub) error { break case stub.Input.Equals != nil: break + case stub.Input.EqualsUnordered != nil: + break case stub.Input.Matches != nil: break default: diff --git a/stub/stub_test.go b/stub/stub_test.go index 4225abb3..7782d7ea 100644 --- a/stub/stub_test.go +++ b/stub/stub_test.go @@ -48,7 +48,7 @@ func TestStub(t *testing.T) { return httptest.NewRequest("GET", "/", nil) }, handler: listStub, - expect: "{\"Testing\":{\"TestMethod\":[{\"Input\":{\"equals\":{\"Hola\":\"Mundo\"},\"contains\":null,\"matches\":null},\"Output\":{\"data\":{\"Hello\":\"World\"},\"error\":\"\"}}]}}\n", + expect: "{\"Testing\":{\"TestMethod\":[{\"Input\":{\"equals\":{\"Hola\":\"Mundo\"},\"equals_unordered\":null,\"contains\":null,\"matches\":null},\"Output\":{\"data\":{\"Hello\":\"World\"},\"error\":\"\"}}]}}\n", }, { name: "find stub equals", @@ -99,6 +99,58 @@ func TestStub(t *testing.T) { handler: handleFindStub, expect: "{\"data\":{\"Hello\":\"World\"},\"error\":\"\"}\n", }, + { + name: "add stub equals_unordered", + mock: func() *http.Request { + payload := `{ + "service": "TestingUnordered", + "method":"TestMethod", + "input": { + "equals_unordered": { + "ids": [1,2] + } + }, + "output":{ + "data":{ + "hello":"world" + } + } + }` + return httptest.NewRequest("POST", "/add", bytes.NewReader([]byte(payload))) + }, + handler: addStub, + expect: `Success add stub`, + }, + { + name: "find stub equals_unordered", + mock: func() *http.Request { + payload := `{ + "service":"TestingUnordered", + "method":"TestMethod", + "data":{ + "ids":[1,2] + } + }` + return httptest.NewRequest("GET", "/find", bytes.NewReader([]byte(payload))) + }, + handler: handleFindStub, + expect: "{\"data\":{\"hello\":\"world\"},\"error\":\"\"}\n", + }, + { + name: "find stub equals_unordered reversed", + mock: func() *http.Request { + payload := `{ + "service":"TestingUnordered", + "method":"TestMethod", + "data":{ + "ids":[2,1] + } + }` + return httptest.NewRequest("GET", "/find", bytes.NewReader([]byte(payload))) + }, + handler: handleFindStub, + expect: "{\"data\":{\"hello\":\"world\"},\"error\":\"\"}\n", + }, { name: "add stub contains", mock: func() *http.Request {