Skip to content

Commit c36a434

Browse files
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.
1 parent 2e72d35 commit c36a434

File tree

3 files changed

+93
-165
lines changed

3 files changed

+93
-165
lines changed

internal/app/pactproxy/interaction.go

Lines changed: 7 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -117,18 +117,8 @@ func LoadInteraction(data []byte, alias string) (*Interaction, error) {
117117

118118
switch mediaType {
119119
case mediaTypeJSON:
120-
if jsonRequestBody, ok := requestBody.(map[string]interface{}); ok {
121-
interaction.addJSONConstraintsFromPact("$.body", propertiesWithMatchingRule, jsonRequestBody)
122-
return interaction, nil
123-
}
124-
125-
if _, ok := requestBody.([]interface{}); ok {
126-
// An array request body should be accepted for application/json media type.
127-
// However, no constraint is added for it
128-
return interaction, nil
129-
}
130-
131-
return nil, fmt.Errorf("media type is %s but body is not json", mediaType)
120+
interaction.addJSONConstraintsFromPact("$.body", propertiesWithMatchingRule, requestBody)
121+
return interaction, nil
132122
case mediaTypeText, mediaTypeCsv, mediaTypeXml:
133123
if body, ok := requestBody.(string); ok {
134124
interaction.addTextConstraintsFromPact(propertiesWithMatchingRule, body)
@@ -271,29 +261,24 @@ func parseMediaType(request map[string]interface{}) (string, error) {
271261

272262
// This function adds constraints for all the fields in the JSON request body which do not
273263
// have a corresponding matching rule
274-
func (i *Interaction) addJSONConstraintsFromPact(path string, matchingRules map[string]bool, values map[string]interface{}) {
275-
for k, v := range values {
276-
i.addJSONConstraintsFromPactAny(path+"."+k, matchingRules, v)
277-
}
278-
}
279-
280-
func (i *Interaction) addJSONConstraintsFromPactAny(path string, matchingRules map[string]bool, value interface{}) {
264+
func (i *Interaction) addJSONConstraintsFromPact(path string, matchingRules map[string]bool, value interface{}) {
281265
if _, hasRule := matchingRules[path]; hasRule {
282266
return
283267
}
284-
285268
switch val := value.(type) {
286269
case map[string]interface{}:
287270
// json_class is used to test for a Pact DSL-style matching rule within the body. The matchingRules passed
288271
// to this method will not include these.
289272
if _, exists := val["json_class"]; exists {
290273
return
291274
}
292-
i.addJSONConstraintsFromPact(path, matchingRules, val)
275+
for k, v := range val {
276+
i.addJSONConstraintsFromPact(path+"."+k, matchingRules, v)
277+
}
293278
case []interface{}:
294279
// Create constraints for each element in the array. This allows matching rules to override them.
295280
for j := range val {
296-
i.addJSONConstraintsFromPactAny(fmt.Sprintf("%s[%d]", path, j), matchingRules, val[j])
281+
i.addJSONConstraintsFromPact(fmt.Sprintf("%s[%d]", path, j), matchingRules, val[j])
297282
}
298283
// Length constraint so that requests with additional elements at the end of the array will not match
299284
i.AddConstraint(interactionConstraint{

internal/app/pactproxy/interaction_test.go

Lines changed: 84 additions & 140 deletions
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ func TestLoadInteractionPlainTextConstraints(t *testing.T) {
121121
}
122122

123123
func TestLoadInteractionJSONConstraints(t *testing.T) {
124-
arrMatchersNotPresent := `{
124+
nestedArrMatchersNotPresent := `{
125125
"description": "A request to create an address",
126126
"request": {
127127
"method": "POST",
@@ -143,7 +143,7 @@ func TestLoadInteractionJSONConstraints(t *testing.T) {
143143
}
144144
}
145145
}`
146-
arrMatcherPresent :=
146+
nestedArrMatcherPresent :=
147147
`{
148148
"description": "A request to create an address",
149149
"request": {
@@ -171,14 +171,50 @@ func TestLoadInteractionJSONConstraints(t *testing.T) {
171171
}
172172
}
173173
}`
174+
arrayOfStrings := `{
175+
"description": "A request to create an address",
176+
"request": {
177+
"method": "POST",
178+
"path": "/addresses",
179+
"headers": {
180+
"Content-Type": "application/json"
181+
},
182+
"body": ["line 1", "line 2"]
183+
},
184+
"response": {
185+
"status": 200,
186+
"headers": {
187+
"Content-Type": "application/json"
188+
},
189+
"body": ["line 1", "line 2"]
190+
}
191+
}`
192+
arrayOfObjects := `{
193+
"description": "A request to create an address",
194+
"request": {
195+
"method": "POST",
196+
"path": "/addresses",
197+
"headers": {
198+
"Content-Type": "application/json"
199+
},
200+
"body": [ {"key": "val"}, {"key": "val"} ]
201+
},
202+
"response": {
203+
"status": 200,
204+
"headers": {
205+
"Content-Type": "application/json"
206+
},
207+
"body": [ {"key": "val"}, {"key": "val"} ]
208+
}
209+
}`
174210
tests := []struct {
175211
name string
176212
interaction []byte
177213
wantConstraints []interactionConstraint
178214
}{
179215
{
180-
name: "array and matcher not present - interactions are created per element",
181-
interaction: []byte(arrMatchersNotPresent),
216+
name: "nested array and matcher not present - interactions are created per element",
217+
interaction: []byte(nestedArrMatchersNotPresent),
182218
wantConstraints: []interactionConstraint{
183219
{
184220
Path: "$.body.addressLines[0]",
@@ -198,8 +234,8 @@ func TestLoadInteractionJSONConstraints(t *testing.T) {
198234
},
199235
},
200236
{
201-
name: "array and matcher present - interaction is not created for matched element",
202-
interaction: []byte(arrMatcherPresent),
237+
name: "nested array and matcher present - interaction is not created for matched element",
238+
interaction: []byte(nestedArrMatcherPresent),
203239
wantConstraints: []interactionConstraint{
204240
{
205241
Path: "$.body.addressLines[1]",
@@ -213,6 +249,48 @@ func TestLoadInteractionJSONConstraints(t *testing.T) {
213249
},
214250
},
215251
},
252+
{
253+
name: "body array and matcher not present - interactions are created per element",
254+
interaction: []byte(arrayOfStrings),
255+
wantConstraints: []interactionConstraint{
256+
{
257+
Path: "$.body[0]",
258+
Format: "%v",
259+
Values: []interface{}{"line 1"},
260+
},
261+
{
262+
Path: "$.body[1]",
263+
Format: "%v",
264+
Values: []interface{}{"line 2"},
265+
},
266+
{
267+
Path: "$.body",
268+
Format: fmtLen,
269+
Values: []interface{}{2},
270+
},
271+
},
272+
},
273+
{
274+
name: "body array of objects - interactions are created per element",
275+
interaction: []byte(arrayOfObjects),
276+
wantConstraints: []interactionConstraint{
277+
{
278+
Path: "$.body[0].key",
279+
Format: "%v",
280+
Values: []interface{}{"val"},
281+
},
282+
{
283+
Path: "$.body[1].key",
284+
Format: "%v",
285+
Values: []interface{}{"val"},
286+
},
287+
{
288+
Path: "$.body",
289+
Format: fmtLen,
290+
Values: []interface{}{2},
291+
},
292+
},
293+
},
216294
}
217295
for _, tt := range tests {
218296
t.Run(tt.name, func(t *testing.T) {
@@ -362,140 +440,6 @@ func TestV3MatchingRulesLeadToCorrectConstraints(t *testing.T) {
362440
}
363441
}
364442

365-
func TestLoadArrayRequestBodyInteractions(t *testing.T) {
366-
arrayOfStrings := `{
367-
"description": "A request to create an address",
368-
"request": {
369-
"method": "POST",
370-
"path": "/addresses",
371-
"headers": {
372-
"Content-Type": "application/json"
373-
},
374-
"body": ["a", "b", "c"]
375-
},
376-
"response": {
377-
"status": 200,
378-
"headers": {
379-
"Content-Type": "application/json"
380-
},
381-
"body": ["a", "b", "c"]
382-
}
383-
}`
384-
arrayOfInts := `{
385-
"description": "A request to create an address",
386-
"request": {
387-
"method": "POST",
388-
"path": "/addresses",
389-
"headers": {
390-
"Content-Type": "application/json"
391-
},
392-
"body": [1, 2, 3]
393-
},
394-
"response": {
395-
"status": 200,
396-
"headers": {
397-
"Content-Type": "application/json"
398-
},
399-
"body": [1, 2, 3]
400-
}
401-
}`
402-
arrayOfBools := `{
403-
"description": "A request to create an address",
404-
"request": {
405-
"method": "POST",
406-
"path": "/addresses",
407-
"headers": {
408-
"Content-Type": "application/json"
409-
},
410-
"body": [true, false, true]
411-
},
412-
"response": {
413-
"status": 200,
414-
"headers": {
415-
"Content-Type": "application/json"
416-
},
417-
"body": [true, false, true]
418-
}
419-
}`
420-
arrayOfObjects := `{
421-
"description": "A request to create an address",
422-
"request": {
423-
"method": "POST",
424-
"path": "/addresses",
425-
"headers": {
426-
"Content-Type": "application/json"
427-
},
428-
"body": [ {"key": "val"}, {"key": "val"} ]
429-
},
430-
"response": {
431-
"status": 200,
432-
"headers": {
433-
"Content-Type": "application/json"
434-
},
435-
"body": [ {"key": "val"}, {"key": "val"} ]
436-
}
437-
}`
438-
arrayOfStringsWithMatcher :=
439-
`{
440-
"description": "A request to create an address",
441-
"request": {
442-
"method": "POST",
443-
"path": "/addresses",
444-
"headers": {
445-
"Content-Type": "application/json"
446-
},
447-
"body": ["a", "b", "c"],
448-
"matchingRules": {
449-
"$.body": {
450-
"match": "type"
451-
}
452-
}
453-
},
454-
"response": {
455-
"status": 200,
456-
"headers": {
457-
"Content-Type": "application/json"
458-
},
459-
"body": ["a", "b", "c"]
460-
}
461-
}`
462-
463-
tests := []struct {
464-
name string
465-
interaction []byte
466-
}{
467-
{
468-
name: "array of strings",
469-
interaction: []byte(arrayOfStrings),
470-
},
471-
{
472-
name: "array of ints",
473-
interaction: []byte(arrayOfInts),
474-
},
475-
{
476-
name: "array of bools",
477-
interaction: []byte(arrayOfBools),
478-
},
479-
{
480-
name: "array of objects",
481-
interaction: []byte(arrayOfObjects),
482-
},
483-
{
484-
name: "array of strings with matcher",
485-
interaction: []byte(arrayOfStringsWithMatcher),
486-
},
487-
}
488-
489-
for _, tt := range tests {
490-
t.Run(tt.name, func(t *testing.T) {
491-
interaction, err := LoadInteraction(tt.interaction, "alias")
492-
require.NoError(t, err, "unexpected error %v", err)
493-
494-
require.Empty(t, interaction.constraints, "No constraint should be added for the interaction")
495-
})
496-
}
497-
}
498-
499443
func Test_parseMediaType(t *testing.T) {
500444
tests := []struct {
501445
name string

internal/app/proxy_test.go

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -559,7 +559,6 @@ func TestArrayBodyRequest(t *testing.T) {
559559
the_response_body_is(tc.respBody)
560560
})
561561
}
562-
563562
}
564563

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

596595
then.
597-
// Pact Mock Server returns 500 if request body does not match
596+
// Pact Proxy returns 400 if request body does not match
598597
pact_verification_is_not_successful().and().
599-
the_response_is_(http.StatusInternalServerError)
598+
the_response_is_(http.StatusBadRequest)
600599
})
601600
}
602601
}

0 commit comments

Comments
 (0)