Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Optional paths #169

Open
wants to merge 16 commits into
base: master
Choose a base branch
from
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,9 @@ Mock definition:
"cookies": {
"name": "value"
},
"optionalPaths": {
".path.to.optional.field": true
},
"body": "Expected Body"
},
"response": {
Expand Down Expand Up @@ -204,6 +207,7 @@ A core feature of Mmock is the ability to return canned HTTP responses for reque
* *queryStringParameters*: Array of query strings. It allows more than one value for the same key.
* *headers*: Array of headers. It allows more than one value for the same key. **Case sensitive!**
* *cookies*: Array of cookies.
* *optionalPaths*: A map of paths within the body that should be considered optional. The value should always be true.
* *body*: Body string. It allows * pattern. It also supports regular expressions for field values within JSON request bodies.

In case of queryStringParameters, headers and cookies, the request can be matched only if all defined keys in mock will be present with the exact or glob value.
Expand Down Expand Up @@ -631,6 +635,7 @@ You can always disable this behavior adding the following flag `-server-statisti
- Improved logging with levels thanks to [@jcdietrich](https://github.com/jcdietrich) [@jdietrich-tc](https://github.com/jdietrich-tc)
- Support for Regular Expressions for QueryStringParameters [@jcdietrich](https://github.com/jcdietrich) [@jdietrich-tc](https://github.com/jdietrich-tc)
- Support for URI and Description tags [@jcdietrich](https://github.com/jcdietrich) [@jdietrich-tc](https://github.com/jdietrich-tc)
- Support for Optional Paths within the body [@jcdietrich](https://github.com/jcdietrich) [@jdietrich-tc](https://github.com/jdietrich-tc)

### Contributing

Expand Down
6 changes: 3 additions & 3 deletions pkg/match/payload/comparator.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import (
)

type Comparer interface {
Compare(s1, s2 string) bool
Compare(s1, s2 string, optionalPaths map[string]bool, currentPath string) bool
}

type Comparator struct {
Expand Down Expand Up @@ -33,12 +33,12 @@ func (c Comparator) AddComparer(contentType string, comparer Comparer) {
c.comparers[contentType] = comparer
}

func (c Comparator) Compare(contentType, s1, s2 string) (comparable bool, equals bool) {
func (c Comparator) Compare(contentType, s1, s2 string, optionalPaths map[string]bool) (comparable bool, equals bool) {
parts := strings.Split(contentType, ";")
comparer, ok := c.comparers[parts[0]]
if !ok {
return false, false
}

return true, comparer.Compare(s1, s2)
return true, comparer.Compare(s1, s2, optionalPaths, "")
}
20 changes: 13 additions & 7 deletions pkg/match/payload/comparator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,10 @@ func TestComparator_Compare(t *testing.T) {
comparers map[string]Comparer
}
type args struct {
contentType string
s1 string
s2 string
contentType string
s1 string
s2 string
optionalPaths map[string]bool
}

tests := []struct {
Expand All @@ -19,17 +20,22 @@ func TestComparator_Compare(t *testing.T) {
wantComparable bool
wantEquals bool
}{
{"Compare json ok", fields{map[string]Comparer{"application/json": &JSONComparator{}}}, args{"application/json", "{\"name\":\"bob\",\"age\":30}", "{\"name\":\"bob\",\"age\":30}"}, true, true},
{"Compare json ko", fields{map[string]Comparer{"application/json": &JSONComparator{}}}, args{"application/json", "{\"name\":\"bob\",\"age\":30}", "{\"name\":\"bob\",\"age\":40}"}, true, false},
{"Not comparable", fields{map[string]Comparer{"application/xml": &XMLComparator{}}}, args{"application/json", "{\"name\":\"bob\",\"age\":30}", "{\"name\":\"bob\",\"age\":40}"}, false, false},
{"Compare json ok", fields{map[string]Comparer{"application/json": &JSONComparator{}}}, args{"application/json", "{\"name\":\"bob\",\"age\":30}", "{\"name\":\"bob\",\"age\":30}", map[string]bool{}}, true, true},
{"Compare json ko", fields{map[string]Comparer{"application/json": &JSONComparator{}}}, args{"application/json", "{\"name\":\"bob\",\"age\":30}", "{\"name\":\"bob\",\"age\":40}", map[string]bool{}}, true, false},
{"Not comparable", fields{map[string]Comparer{"application/xml": &XMLComparator{}}}, args{"application/json", "{\"name\":\"bob\",\"age\":30}", "{\"name\":\"bob\",\"age\":40}", map[string]bool{}}, false, false},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := Comparator{
comparers: tt.fields.comparers,
}
gotComparable, gotEquals := c.Compare(tt.args.contentType, tt.args.s1, tt.args.s2)
gotComparable, gotEquals := c.Compare(
tt.args.contentType,
tt.args.s1,
tt.args.s2,
tt.args.optionalPaths,
)
if gotComparable != tt.wantComparable {
t.Errorf("Comparator.Compare() gotComparable = %v, want %v", gotComparable, tt.wantComparable)
}
Expand Down
67 changes: 47 additions & 20 deletions pkg/match/payload/json_comparator.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,22 @@ func isArray(s string) bool {
return len(st) > 0 && st[0] == '['
}

func (jc *JSONComparator) doCompareJSONRegexUnmarshaled(patterns, values map[string]interface{}) bool {
func (jc *JSONComparator) doCompareJSONRegexUnmarshaled(
patterns, values map[string]interface{},
optionalPaths map[string]bool,
currentPath string) bool {
var matches bool
matches = jc.match(patterns, values)
matches = jc.match(patterns, values, optionalPaths, currentPath)
if !matches {
log.Debugf("values: %v don't match: %v", values, patterns)
}
return matches
}

func (jc *JSONComparator) doCompareJSONRegex(jsonWithPatterns, jsonWithValues string) bool {
func (jc *JSONComparator) doCompareJSONRegex(
jsonWithPatterns, jsonWithValues string,
optionalPaths map[string]bool,
currentPath string) bool {
var patterns map[string]interface{}
var values map[string]interface{}
if err := json.Unmarshal([]byte(jsonWithPatterns), &patterns); err != nil {
Expand All @@ -41,10 +47,13 @@ func (jc *JSONComparator) doCompareJSONRegex(jsonWithPatterns, jsonWithValues st
log.Errorf("error in json values: %v", err)
return false
}
return jc.doCompareJSONRegexUnmarshaled(patterns, values)
return jc.doCompareJSONRegexUnmarshaled(patterns, values, optionalPaths, currentPath)
}

func (jc *JSONComparator) doCompareArrayRegex(jsonWithPatterns, jsonWithValues string) bool {
func (jc *JSONComparator) doCompareArrayRegex(
jsonWithPatterns, jsonWithValues string,
optionalPaths map[string]bool,
currentPath string) bool {
var patterns []map[string]interface{}
var values []map[string]interface{}

Expand All @@ -57,13 +66,16 @@ func (jc *JSONComparator) doCompareArrayRegex(jsonWithPatterns, jsonWithValues s
log.Errorf("error in json patterns: %v", err)
return false
}
return jc.doCompareArrayRegexUnmarshaled(patterns, values)
return jc.doCompareArrayRegexUnmarshaled(patterns, values, optionalPaths, currentPath)
}

func (jc *JSONComparator) doCompareArrayRegexUnmarshaled(patterns, values []map[string]interface{}) bool {
func (jc *JSONComparator) doCompareArrayRegexUnmarshaled(
patterns, values []map[string]interface{},
optionalPaths map[string]bool,
currentPath string) bool {

for i := 0; i < len(patterns); i++ {
if !jc.match(patterns[i], values[i]) {
if !jc.match(patterns[i], values[i], optionalPaths, currentPath) {
log.Debugf("value %v doesn't match %v",
values[i], patterns[i])
return false
Expand All @@ -72,18 +84,26 @@ func (jc *JSONComparator) doCompareArrayRegexUnmarshaled(patterns, values []map[
return true
}

func (jc *JSONComparator) match(p, v map[string]interface{}) bool {
func (jc *JSONComparator) match(
p, v map[string]interface{},
optionalPaths map[string]bool,
currentPath string) bool {
for field, pattern := range p {

var currentFieldPath = fmt.Sprintf("%s.%s", currentPath, field)
value, exists := v[field]
log.Debugf("comparing field %v with pattern %v against value %v",
field, pattern, value)
currentFieldPath, pattern, value)

if !exists {
log.Debugf("field doesn't exist: %v", field)
if !exists && !optionalPaths[currentFieldPath] {
log.Debugf("field doesn't exist, and isn't optional: %v", currentFieldPath)

return false
} else if !exists && optionalPaths[currentFieldPath] {

log.Debugf("field doesn't exist, but is optional: %v", currentFieldPath)
continue
}

str, ok := pattern.(string)
if !ok {
var valueType reflect.Kind
Expand All @@ -94,19 +114,23 @@ func (jc *JSONComparator) match(p, v map[string]interface{}) bool {
patternType = reflect.ValueOf(pattern).Kind()

if valueType == reflect.Map && patternType == reflect.Map {
log.Debugf("recursing into map %v", field)
log.Debugf("recursing into map %v", currentFieldPath)

result = jc.doCompareJSONRegexUnmarshaled(
pattern.(map[string]interface{}),
value.(map[string]interface{}))
value.(map[string]interface{}),
optionalPaths,
currentFieldPath,
)

if !result {
return false
}
} else if (valueType == reflect.Array || valueType == reflect.Slice) &&
(patternType == reflect.Array || patternType == reflect.Slice) {

log.Debugf("recursing into array %v", field)
log.Debugf("recursing into array %v", currentFieldPath)

valueJsonBytes, err1 := json.Marshal(value)
patternJsonBytes, err2 := json.Marshal(pattern)

Expand All @@ -117,7 +141,7 @@ func (jc *JSONComparator) match(p, v map[string]interface{}) bool {
}

result = jc.doCompareArrayRegex(
string(patternJsonBytes), string(valueJsonBytes))
string(patternJsonBytes), string(valueJsonBytes), optionalPaths, currentFieldPath)

if !result {
return false
Expand All @@ -136,16 +160,19 @@ func (jc *JSONComparator) match(p, v map[string]interface{}) bool {
return true
}

func (jc *JSONComparator) Compare(s1, s2 string) bool {
func (jc *JSONComparator) Compare(
s1, s2 string,
optionalPaths map[string]bool,
currentPath string) bool {

if isArray(s1) != isArray(s2) {
log.Debugf("only one of these is an array %v %v", s1, s2)
return false
}

if isArray(s1) || isArray(s2) {
return jc.doCompareArrayRegex(s1, s2)
return jc.doCompareArrayRegex(s1, s2, optionalPaths, currentPath)
}

return jc.doCompareJSONRegex(s1, s2)
return jc.doCompareJSONRegex(s1, s2, optionalPaths, currentPath)
}
Loading
Loading