Skip to content

Commit

Permalink
Allow constraints when JSON body is an array
Browse files Browse the repository at this point in the history
Previously, a JSON request body being an array was a special-case. Now that arrays are handled nested within the JSON body, treat them as any other JSON. The constraints will now get correctly added as with any other value.
  • Loading branch information
joshkeegan-form3 committed Apr 4, 2024
1 parent afc4723 commit b59a60b
Show file tree
Hide file tree
Showing 3 changed files with 93 additions and 165 deletions.
29 changes: 7 additions & 22 deletions internal/app/pactproxy/interaction.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,18 +117,8 @@ func LoadInteraction(data []byte, alias string) (*Interaction, error) {

switch mediaType {
case mediaTypeJSON:
if jsonRequestBody, ok := requestBody.(map[string]interface{}); ok {
interaction.addJSONConstraintsFromPact("$.body", propertiesWithMatchingRule, jsonRequestBody)
return interaction, nil
}

if _, ok := requestBody.([]interface{}); ok {
// An array request body should be accepted for application/json media type.
// However, no constraint is added for it
return interaction, nil
}

return nil, fmt.Errorf("media type is %s but body is not json", mediaType)
interaction.addJSONConstraintsFromPact("$.body", propertiesWithMatchingRule, requestBody)
return interaction, nil
case mediaTypeText, mediaTypeCsv, mediaTypeXml:
if body, ok := requestBody.(string); ok {
interaction.addTextConstraintsFromPact(propertiesWithMatchingRule, body)
Expand Down Expand Up @@ -271,29 +261,24 @@ func parseMediaType(request map[string]interface{}) (string, error) {

// This function adds constraints for all the fields in the JSON request body which do not
// have a corresponding matching rule
func (i *Interaction) addJSONConstraintsFromPact(path string, matchingRules map[string]bool, values map[string]interface{}) {
for k, v := range values {
i.addJSONConstraintsFromPactAny(path+"."+k, matchingRules, v)
}
}

func (i *Interaction) addJSONConstraintsFromPactAny(path string, matchingRules map[string]bool, value interface{}) {
func (i *Interaction) addJSONConstraintsFromPact(path string, matchingRules map[string]bool, value interface{}) {
if _, hasRule := matchingRules[path]; hasRule {
return
}

switch val := value.(type) {
case map[string]interface{}:
// json_class is used to test for a Pact DSL-style matching rule within the body. The matchingRules passed
// to this method will not include these.
if _, exists := val["json_class"]; exists {
return
}
i.addJSONConstraintsFromPact(path, matchingRules, val)
for k, v := range val {
i.addJSONConstraintsFromPact(path+"."+k, matchingRules, v)
}
case []interface{}:
// Create constraints for each element in the array. This allows matching rules to override them.
for j := range val {
i.addJSONConstraintsFromPactAny(fmt.Sprintf("%v[%v]", path, j), matchingRules, val[j])
i.addJSONConstraintsFromPact(fmt.Sprintf("%v[%v]", path, j), matchingRules, val[j])
}
// Length constraint so that requests with additional elements at the end of the array will not match
i.AddConstraint(interactionConstraint{
Expand Down
224 changes: 84 additions & 140 deletions internal/app/pactproxy/interaction_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ func TestLoadInteractionPlainTextConstraints(t *testing.T) {
}

func TestLoadInteractionJSONConstraints(t *testing.T) {
arrMatchersNotPresent := `{
nestedArrMatchersNotPresent := `{
"description": "A request to create an address",
"request": {
"method": "POST",
Expand All @@ -143,7 +143,7 @@ func TestLoadInteractionJSONConstraints(t *testing.T) {
}
}
}`
arrMatcherPresent :=
nestedArrMatcherPresent :=
`{
"description": "A request to create an address",
"request": {
Expand Down Expand Up @@ -171,14 +171,50 @@ func TestLoadInteractionJSONConstraints(t *testing.T) {
}
}
}`
arrayOfStrings := `{
"description": "A request to create an address",
"request": {
"method": "POST",
"path": "/addresses",
"headers": {
"Content-Type": "application/json"
},
"body": ["line 1", "line 2"]
},
"response": {
"status": 200,
"headers": {
"Content-Type": "application/json"
},
"body": ["line 1", "line 2"]
}
}`
arrayOfObjects := `{
"description": "A request to create an address",
"request": {
"method": "POST",
"path": "/addresses",
"headers": {
"Content-Type": "application/json"
},
"body": [ {"key": "val"}, {"key": "val"} ]
},
"response": {
"status": 200,
"headers": {
"Content-Type": "application/json"
},
"body": [ {"key": "val"}, {"key": "val"} ]
}
}`
tests := []struct {
name string
interaction []byte
wantConstraints []interactionConstraint
}{
{
name: "array and matcher not present - interactions are created per element",
interaction: []byte(arrMatchersNotPresent),
name: "nested array and matcher not present - interactions are created per element",
interaction: []byte(nestedArrMatchersNotPresent),
wantConstraints: []interactionConstraint{
{
Path: "$.body.addressLines[0]",
Expand All @@ -198,8 +234,8 @@ func TestLoadInteractionJSONConstraints(t *testing.T) {
},
},
{
name: "array and matcher present - interaction is not created for matched element",
interaction: []byte(arrMatcherPresent),
name: "nested array and matcher present - interaction is not created for matched element",
interaction: []byte(nestedArrMatcherPresent),
wantConstraints: []interactionConstraint{
{
Path: "$.body.addressLines[1]",
Expand All @@ -213,6 +249,48 @@ func TestLoadInteractionJSONConstraints(t *testing.T) {
},
},
},
{
name: "body array and matcher not present - interactions are created per element",
interaction: []byte(arrayOfStrings),
wantConstraints: []interactionConstraint{
{
Path: "$.body[0]",
Format: "%v",
Values: []interface{}{"line 1"},
},
{
Path: "$.body[1]",
Format: "%v",
Values: []interface{}{"line 2"},
},
{
Path: "$.body",
Format: fmtLen,
Values: []interface{}{2},
},
},
},
{
name: "body array of objects - interactions are created per element",
interaction: []byte(arrayOfObjects),
wantConstraints: []interactionConstraint{
{
Path: "$.body[0].key",
Format: "%v",
Values: []interface{}{"val"},
},
{
Path: "$.body[1].key",
Format: "%v",
Values: []interface{}{"val"},
},
{
Path: "$.body",
Format: fmtLen,
Values: []interface{}{2},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
Expand Down Expand Up @@ -362,140 +440,6 @@ func TestV3MatchingRulesLeadToCorrectConstraints(t *testing.T) {
}
}

func TestLoadArrayRequestBodyInteractions(t *testing.T) {
arrayOfStrings := `{
"description": "A request to create an address",
"request": {
"method": "POST",
"path": "/addresses",
"headers": {
"Content-Type": "application/json"
},
"body": ["a", "b", "c"]
},
"response": {
"status": 200,
"headers": {
"Content-Type": "application/json"
},
"body": ["a", "b", "c"]
}
}`
arrayOfInts := `{
"description": "A request to create an address",
"request": {
"method": "POST",
"path": "/addresses",
"headers": {
"Content-Type": "application/json"
},
"body": [1, 2, 3]
},
"response": {
"status": 200,
"headers": {
"Content-Type": "application/json"
},
"body": [1, 2, 3]
}
}`
arrayOfBools := `{
"description": "A request to create an address",
"request": {
"method": "POST",
"path": "/addresses",
"headers": {
"Content-Type": "application/json"
},
"body": [true, false, true]
},
"response": {
"status": 200,
"headers": {
"Content-Type": "application/json"
},
"body": [true, false, true]
}
}`
arrayOfObjects := `{
"description": "A request to create an address",
"request": {
"method": "POST",
"path": "/addresses",
"headers": {
"Content-Type": "application/json"
},
"body": [ {"key": "val"}, {"key": "val"} ]
},
"response": {
"status": 200,
"headers": {
"Content-Type": "application/json"
},
"body": [ {"key": "val"}, {"key": "val"} ]
}
}`
arrayOfStringsWithMatcher :=
`{
"description": "A request to create an address",
"request": {
"method": "POST",
"path": "/addresses",
"headers": {
"Content-Type": "application/json"
},
"body": ["a", "b", "c"],
"matchingRules": {
"$.body": {
"match": "type"
}
}
},
"response": {
"status": 200,
"headers": {
"Content-Type": "application/json"
},
"body": ["a", "b", "c"]
}
}`

tests := []struct {
name string
interaction []byte
}{
{
name: "array of strings",
interaction: []byte(arrayOfStrings),
},
{
name: "array of ints",
interaction: []byte(arrayOfInts),
},
{
name: "array of bools",
interaction: []byte(arrayOfBools),
},
{
name: "array of objects",
interaction: []byte(arrayOfObjects),
},
{
name: "array of strings with matcher",
interaction: []byte(arrayOfStringsWithMatcher),
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
interaction, err := LoadInteraction(tt.interaction, "alias")
require.NoError(t, err, "unexpected error %v", err)

require.Empty(t, interaction.constraints, "No constraint should be added for the interaction")
})
}
}

func Test_parseMediaType(t *testing.T) {
tests := []struct {
name string
Expand Down
5 changes: 2 additions & 3 deletions internal/app/proxy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -559,7 +559,6 @@ func TestArrayBodyRequest(t *testing.T) {
the_response_body_is(tc.respBody)
})
}

}

func TestArrayBodyRequestWithModifiedStatusCode(t *testing.T) {
Expand Down Expand Up @@ -594,9 +593,9 @@ func TestArrayBodyRequestUnmatchedRequestBody(t *testing.T) {
a_request_is_sent_with("application/json", tc.unmatchedReqBody)

then.
// Pact Mock Server returns 500 if request body does not match
// Pact Proxy returns 400 if request body does not match
pact_verification_is_not_successful().and().
the_response_is_(http.StatusInternalServerError)
the_response_is_(http.StatusBadRequest)
})
}
}
Expand Down

0 comments on commit b59a60b

Please sign in to comment.