diff --git a/internal/app/pactproxy/interaction.go b/internal/app/pactproxy/interaction.go index e9b97a1..d84dde9 100644 --- a/internal/app/pactproxy/interaction.go +++ b/internal/app/pactproxy/interaction.go @@ -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) @@ -271,17 +261,10 @@ 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 @@ -289,11 +272,13 @@ func (i *Interaction) addJSONConstraintsFromPactAny(path string, matchingRules m 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("%s[%d]", path, j), matchingRules, val[j]) + i.addJSONConstraintsFromPact(fmt.Sprintf("%s[%d]", 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{ diff --git a/internal/app/pactproxy/interaction_test.go b/internal/app/pactproxy/interaction_test.go index ff2409e..d615151 100644 --- a/internal/app/pactproxy/interaction_test.go +++ b/internal/app/pactproxy/interaction_test.go @@ -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", @@ -143,7 +143,7 @@ func TestLoadInteractionJSONConstraints(t *testing.T) { } } }` - arrMatcherPresent := + nestedArrMatcherPresent := `{ "description": "A request to create an address", "request": { @@ -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]", @@ -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]", @@ -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) { @@ -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 diff --git a/internal/app/proxy_test.go b/internal/app/proxy_test.go index 0730de2..3f3f9fa 100644 --- a/internal/app/proxy_test.go +++ b/internal/app/proxy_test.go @@ -559,7 +559,6 @@ func TestArrayBodyRequest(t *testing.T) { the_response_body_is(tc.respBody) }) } - } func TestArrayBodyRequestWithModifiedStatusCode(t *testing.T) { @@ -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) }) } }