From 2e72d350fe2819fd6f8782f71da22cbb60a573b1 Mon Sep 17 00:00:00 2001 From: Josh Keegan Date: Wed, 3 Apr 2024 16:10:08 +0100 Subject: [PATCH 1/2] support matching on arrays nested in the body If there is an array nested within a JSON body on the pact, when a request was received and the interaction had its constraints evaluated, the constraint for the value of the array got skipped. This meant that if pact-proxy had interactions that differed only in the content of arrays, when requests were received it would match all of those interactions rather than the specific one that would have been matched had the contents of the array been considered. This was causing flaky tests in a project that used pact-proxy to wait for N of a specific interaction. The interaction was the same as another apart from the content of an array in the body. Since pact-proxy could not tell these apart, it would continue after N of either of these interactions, not N of the one the test needed to wait for. That randomly caused the next test to fail because interactions from the previous test were still ongoing. Now when constraints are added from the Pact, we generate constraints for each element in the array, as well as a length check. When evaluating constraints, we can then run these like we do for any other type. This allows for two interactions to be created that differ only in the contents of an array and pact-proxy is able to tell them apart. Generating constraints on a per-element basis is done to also allow for matching rules that apply to an individual array element. When matching rules are specified, pact-proxy does not enforce these so the constraint must not be generated for that element. As with non-array matching rules, if a request is received and the remaining constraints are met, the request is considered a match and proxied to Pact server, which will check matching rules. --- internal/app/pactproxy/constraint.go | 34 ++++ internal/app/pactproxy/interaction.go | 73 ++++--- internal/app/pactproxy/interaction_test.go | 108 +++++++++++ internal/app/proxy_stage_test.go | 3 +- internal/app/proxy_test.go | 209 ++++++++++++++++++++- 5 files changed, 392 insertions(+), 35 deletions(-) diff --git a/internal/app/pactproxy/constraint.go b/internal/app/pactproxy/constraint.go index 74fc55d..284f198 100644 --- a/internal/app/pactproxy/constraint.go +++ b/internal/app/pactproxy/constraint.go @@ -1,9 +1,12 @@ package pactproxy import ( + "fmt" "strings" ) +const fmtLen = "_length_" + type interactionConstraint struct { Interaction string `json:"interaction"` Path string `json:"path"` @@ -15,3 +18,34 @@ type interactionConstraint struct { func (i interactionConstraint) Key() string { return strings.Join([]string{i.Interaction, i.Path}, "_") } + +func (i interactionConstraint) check(expectedValues []interface{}, actualValue interface{}) error { + if i.Format == fmtLen { + if len(expectedValues) != 1 { + return fmt.Errorf( + "expected single positive integer value for path %q length constraint, but there are %v expected values", + i.Path, len(expectedValues)) + } + expected, ok := expectedValues[0].(int) + if !ok || expected < 0 { + return fmt.Errorf("expected value for %q length constraint must be a positive integer", i.Path) + } + + actualSlice, ok := actualValue.([]interface{}) + if !ok { + return fmt.Errorf("value at path %q must be an array due to length constraint", i.Path) + } + if expected != len(actualSlice) { + return fmt.Errorf("value of length %v at path %q does not match length constraint %v", + expected, i.Path, len(actualSlice)) + } + return nil + } + + expected := fmt.Sprintf(i.Format, expectedValues...) + actual := fmt.Sprintf("%v", actualValue) + if expected != actual { + return fmt.Errorf("value %q at path %q does not match constraint %q", actual, i.Path, expected) + } + return nil +} diff --git a/internal/app/pactproxy/interaction.go b/internal/app/pactproxy/interaction.go index 8c5bd29..e9b97a1 100644 --- a/internal/app/pactproxy/interaction.go +++ b/internal/app/pactproxy/interaction.go @@ -4,7 +4,6 @@ import ( "encoding/json" "fmt" "mime" - "reflect" "regexp" "strings" "sync" @@ -202,6 +201,8 @@ func getPathRegex(matchingRules map[string]interface{}) (string, error) { return regexString, nil } +// Gets the pact JSON file style matching rules from the "matchingRules" property of the request. +// Note that Pact DSL style matching rules within the body are identified later when adding JSON constraints. func getMatchingRules(request map[string]interface{}) map[string]interface{} { rules, hasRules := request["matchingRules"] if !hasRules { @@ -272,22 +273,40 @@ func parseMediaType(request map[string]interface{}) (string, error) { // have a corresponding matching rule func (i *Interaction) addJSONConstraintsFromPact(path string, matchingRules map[string]bool, values map[string]interface{}) { for k, v := range values { - switch val := v.(type) { - case map[string]interface{}: - if _, exists := val["json_class"]; exists { - continue - } - i.addJSONConstraintsFromPact(path+"."+k, matchingRules, val) - default: - p := path + "." + k - if _, hasRule := matchingRules[p]; !hasRule { - i.AddConstraint(interactionConstraint{ - Path: p, - Format: "%v", - Values: []interface{}{val}, - }) - } + i.addJSONConstraintsFromPactAny(path+"."+k, matchingRules, v) + } +} + +func (i *Interaction) addJSONConstraintsFromPactAny(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) + 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]) } + // Length constraint so that requests with additional elements at the end of the array will not match + i.AddConstraint(interactionConstraint{ + Path: path, + Format: fmtLen, + Values: []interface{}{len(val)}, + }) + default: + i.AddConstraint(interactionConstraint{ + Path: path, + Format: "%v", + Values: []interface{}{val}, + }) } } @@ -341,10 +360,10 @@ func (i *Interaction) EvaluateConstraints(request requestDocument, interactions i.mu.RLock() defer i.mu.RUnlock() for _, constraint := range i.constraints { - values := constraint.Values + expected := constraint.Values if constraint.Source != "" { var err error - values, err = i.loadValuesFromSource(constraint, interactions) + expected, err = i.loadValuesFromSource(constraint, interactions) if err != nil { violations = append(violations, err.Error()) result = false @@ -352,22 +371,16 @@ func (i *Interaction) EvaluateConstraints(request requestDocument, interactions } } - actual := "" - val, err := jsonpath.Get(request.encodeValues(constraint.Path), map[string]interface{}(request)) + actual, err := jsonpath.Get(request.encodeValues(constraint.Path), map[string]interface{}(request)) if err != nil { - log.Warn(err) - } - if reflect.TypeOf(val) == reflect.TypeOf([]interface{}{}) { - log.Infof("skipping matching on []interface{} type for path '%s'", constraint.Path) + violations = append(violations, + fmt.Sprintf("constraint path %q cannot be resolved within request: %q", constraint.Path, err)) + result = false continue } - if err == nil { - actual = fmt.Sprintf("%v", val) - } - expected := fmt.Sprintf(constraint.Format, values...) - if actual != expected { - violations = append(violations, fmt.Sprintf("value '%s' at path '%s' does not match constraint '%s'", actual, constraint.Path, expected)) + if err := constraint.check(expected, actual); err != nil { + violations = append(violations, err.Error()) result = false } } diff --git a/internal/app/pactproxy/interaction_test.go b/internal/app/pactproxy/interaction_test.go index 9110e83..ff2409e 100644 --- a/internal/app/pactproxy/interaction_test.go +++ b/internal/app/pactproxy/interaction_test.go @@ -120,6 +120,114 @@ func TestLoadInteractionPlainTextConstraints(t *testing.T) { } } +func TestLoadInteractionJSONConstraints(t *testing.T) { + arrMatchersNotPresent := `{ + "description": "A request to create an address", + "request": { + "method": "POST", + "path": "/addresses", + "headers": { + "Content-Type": "application/json" + }, + "body": { + "addressLines": ["line 1", "line 2"] + } + }, + "response": { + "status": 200, + "headers": { + "Content-Type": "application/json" + }, + "body": { + "addressLines": ["line 1", "line 2"] + } + } + }` + arrMatcherPresent := + `{ + "description": "A request to create an address", + "request": { + "method": "POST", + "path": "/addresses", + "headers": { + "Content-Type": "application/json" + }, + "body": { + "addressLines": ["line 1", "line 2"] + }, + "matchingRules": { + "$.body.addressLines[0]": { + "regex": ".*" + } + } + }, + "response": { + "status": 200, + "headers": { + "Content-Type": "application/json" + }, + "body": { + "addressLines": ["line 1", "line 2"] + } + } + }` + tests := []struct { + name string + interaction []byte + wantConstraints []interactionConstraint + }{ + { + name: "array and matcher not present - interactions are created per element", + interaction: []byte(arrMatchersNotPresent), + wantConstraints: []interactionConstraint{ + { + Path: "$.body.addressLines[0]", + Format: "%v", + Values: []interface{}{"line 1"}, + }, + { + Path: "$.body.addressLines[1]", + Format: "%v", + Values: []interface{}{"line 2"}, + }, + { + Path: "$.body.addressLines", + Format: fmtLen, + Values: []interface{}{2}, + }, + }, + }, + { + name: "array and matcher present - interaction is not created for matched element", + interaction: []byte(arrMatcherPresent), + wantConstraints: []interactionConstraint{ + { + Path: "$.body.addressLines[1]", + Format: "%v", + Values: []interface{}{"line 2"}, + }, + { + Path: "$.body.addressLines", + Format: fmtLen, + Values: []interface{}{2}, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + interaction, err := LoadInteraction(tt.interaction, "alias") + require.NoError(t, err) + + actual := make([]interactionConstraint, 0, len(interaction.constraints)) + for _, constraint := range interaction.constraints { + actual = append(actual, constraint) + } + assert.ElementsMatch(t, tt.wantConstraints, actual) + }) + } +} + // This test asserts that given a pact v3-style nested matching rule, a constraint // is not created for the corresponding property func TestV3MatchingRulesLeadToCorrectConstraints(t *testing.T) { diff --git a/internal/app/proxy_stage_test.go b/internal/app/proxy_stage_test.go index c68660a..c3d1494 100644 --- a/internal/app/proxy_stage_test.go +++ b/internal/app/proxy_stage_test.go @@ -451,8 +451,7 @@ func (s *ProxyStage) the_nth_response_body_is(n int, data []byte) *ProxyStage { s.assert.GreaterOrEqual(len(s.responseBodies), n, "number of request bodies is les than expected") body := s.responseBodies[n-1] - c := bytes.Compare(body, data) - s.assert.Equal(0, c, "Expected body did not match") + s.assert.Equal(data, body, "Expected body did not match") return s } diff --git a/internal/app/proxy_test.go b/internal/app/proxy_test.go index e0c8539..0730de2 100644 --- a/internal/app/proxy_test.go +++ b/internal/app/proxy_test.go @@ -3,6 +3,8 @@ package app import ( "net/http" "testing" + + "github.com/pact-foundation/pact-go/dsl" ) func TestLargePactResponse(t *testing.T) { @@ -592,9 +594,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, - // so the response status code is not checked - pact_verification_is_not_successful() + // Pact Mock Server returns 500 if request body does not match + pact_verification_is_not_successful().and(). + the_response_is_(http.StatusInternalServerError) }) } } @@ -637,3 +639,204 @@ func TestArrayBodyRequestConstraintDoesNotMatch(t *testing.T) { }) } } + +func TestArrayNestedWithinBody(t *testing.T) { + pactReqBody := map[string]interface{}{ + "entries": []interface{}{ + map[string]string{"key": "val"}, + map[string]string{"key": "val"}, + }, + } + const respContentType = "application/json" + const respBody = `[{"status":"ok"}]` + + const matchedReqBody = `{"entries": [ {"key": "val"}, {"key": "val"} ]}` + const unmatchedReqBody = `{"entries": [ {"key": "val"}, {"key": "unexpected value"} ]}` + const unmatchedReqBodyAdditionalArrElem = `{"entries": [ {"key": "val"}, {"key": "val"}, {"key": "val"} ]}` + + const matchedConstraintPath = "$.body.entries[0].key" + const matchedConstraintValue = "val" + + const unmatchedConstraintPath = "$.body.entries[1].key" + const unmatchedConstraintValue = "wrong value" + + const outOfBoundsConstraintPath = "$.body.entries[2].key" + + t.Run("Matches", func(t *testing.T) { + given, when, then := NewProxyStage(t) + + given. + a_pact_that_expects("application/json", pactReqBody, respContentType, respBody) + + when. + a_request_is_sent_with("application/json", matchedReqBody) + + then. + pact_verification_is_successful().and(). + the_response_is_(http.StatusOK).and(). + the_response_body_is(respBody) + }) + + t.Run("Does not match", func(t *testing.T) { + given, when, then := NewProxyStage(t) + + given. + a_pact_that_expects("application/json", pactReqBody, respContentType, respBody) + + when. + a_request_is_sent_with("application/json", unmatchedReqBody) + + then. + pact_verification_is_not_successful().and(). + the_response_is_(http.StatusBadRequest) + }) + + t.Run("Does not match - additional array element", func(t *testing.T) { + given, when, then := NewProxyStage(t) + + given. + a_pact_that_expects("application/json", pactReqBody, respContentType, respBody) + + when. + a_request_is_sent_with("application/json", unmatchedReqBodyAdditionalArrElem) + + then. + pact_verification_is_not_successful().and(). + the_response_is_(http.StatusBadRequest) + }) + + t.Run("Matches with additional constraint", func(t *testing.T) { + given, when, then := NewProxyStage(t) + + given. + a_pact_that_expects("application/json", pactReqBody, respContentType, respBody).and(). + an_additional_constraint_is_added(matchedConstraintPath, matchedConstraintValue) + + when. + a_request_is_sent_with("application/json", matchedReqBody) + + then. + pact_verification_is_successful().and(). + the_response_is_(http.StatusOK).and(). + the_response_body_is(respBody) + }) + + t.Run("Does not match due to additional constraint with different value", func(t *testing.T) { + given, when, then := NewProxyStage(t) + + given. + a_pact_that_expects("application/json", pactReqBody, respContentType, respBody).and(). + an_additional_constraint_is_added(unmatchedConstraintPath, unmatchedConstraintValue) + + when. + a_request_is_sent_with("application/json", matchedReqBody) + + then. + pact_verification_is_not_successful().and(). + the_response_is_(http.StatusBadRequest) + }) + + t.Run("Does not match due to additional constraint out of array bounds", func(t *testing.T) { + given, when, then := NewProxyStage(t) + + given. + a_pact_that_expects("application/json", pactReqBody, respContentType, respBody).and(). + an_additional_constraint_is_added(outOfBoundsConstraintPath, matchedConstraintValue) + + when. + a_request_is_sent_with("application/json", matchedReqBody) + + then. + pact_verification_is_not_successful().and(). + the_response_is_(http.StatusBadRequest) + }) +} + +// Matching rules are not enforced by Pact proxy, so this test is checking that constraints aren't being +// applied when matching rules are specified, leading to the request being handled by Pact server. +// Note: these tests are using pact DSL style matching rules. Pact proxy identifies these, but differently to +// ones specified in the "matchingRules" JSON property. Loading of the "matchingRules" JSON property style +// gets unit tested in TestLoadInteractionJSONConstraints. +func TestArrayNestedWithinBodyContainingMatchers_NoConstraint(t *testing.T) { + pactReqBody := map[string]interface{}{ + "entries": []interface{}{ + map[string]any{"key": "val"}, + map[string]any{"key": dsl.Term("a", "(a|b)")}, + }, + } + const respContentType = "application/json" + const respBody = `[{"status":"ok"}]` + + const matchedReqBody = `{"entries": [ {"key": "val"}, {"key": "a"} ]}` + const unmatchedReqBody = `{"entries": [ {"key": "val"}, {"key": "c"} ]}` + + t.Run("Matches", func(t *testing.T) { + given, when, then := NewProxyStage(t) + + given. + a_pact_that_expects("application/json", pactReqBody, respContentType, respBody) + + when. + a_request_is_sent_with("application/json", matchedReqBody) + + then. + pact_verification_is_successful().and(). + the_response_is_(http.StatusOK).and(). + the_response_body_is(respBody) + }) + + t.Run("Does not match", func(t *testing.T) { + given, when, then := NewProxyStage(t) + + given. + a_pact_that_expects("application/json", pactReqBody, respContentType, respBody) + + when. + a_request_is_sent_with("application/json", unmatchedReqBody) + + then. + pact_verification_is_not_successful().and(). + // Since the request is passed to Pact Server, the response for not matching is 500 + the_response_is_(http.StatusInternalServerError) + }) +} + +func TestEmptyArrayNestedWithinBody(t *testing.T) { + pactReqBody := map[string]interface{}{ + "entries": []interface{}{}, + } + const respContentType = "application/json" + const respBody = `[{"status":"ok"}]` + + const matchedReqBody = `{"entries": []}` + const unmatchedReqBody = `{"entries": [ {"key": "val"} ]}` + + t.Run("Matches", func(t *testing.T) { + given, when, then := NewProxyStage(t) + + given. + a_pact_that_expects("application/json", pactReqBody, respContentType, respBody) + + when. + a_request_is_sent_with("application/json", matchedReqBody) + + then. + pact_verification_is_successful().and(). + the_response_is_(http.StatusOK).and(). + the_response_body_is(respBody) + }) + + t.Run("Does not match", func(t *testing.T) { + given, when, then := NewProxyStage(t) + + given. + a_pact_that_expects("application/json", pactReqBody, respContentType, respBody) + + when. + a_request_is_sent_with("application/json", unmatchedReqBody) + + then. + pact_verification_is_not_successful().and(). + the_response_is_(http.StatusBadRequest) + }) +} From c36a434b8b629415a3a053e09c1d63a9c47cf27a Mon Sep 17 00:00:00 2001 From: Josh Keegan Date: Thu, 4 Apr 2024 15:59:50 +0100 Subject: [PATCH 2/2] Allow constraints when JSON body is an array 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. --- internal/app/pactproxy/interaction.go | 29 +-- internal/app/pactproxy/interaction_test.go | 224 ++++++++------------- internal/app/proxy_test.go | 5 +- 3 files changed, 93 insertions(+), 165 deletions(-) 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) }) } }