Hello! I've got a question regarding request/respo...
# pact-go
v
Hello! I've got a question regarding request/response bodies matching. In my consumer test I describe interaction this way:
Copy code
body := waveplanner.CreateWavePlanBody{
		Waves: []waveplanner.Wave{
			{
				CutoffTime: "04:00",
				FromTime:   "10:00",
				ToTime:     "15:59",
			},
			{
				CutoffTime: "12:00",
				FromTime:   "16:00",
				ToTime:     "20:59",
			},
			{
				CutoffTime: "16:00",
				FromTime:   "21:00",
				ToTime:     "09:59",
			},
		},
	}
	created := waveplanner.CreateWavePlanResponse{
		ID:    uuid.NewString(),
		MfcID: mfc,
		CreatedBy: "user-id",
		Waves: []waveplanner.WaveItem{
			{
				ID:         uuid.NewString(),
				CutoffTime: "04:00",
				FromTime:   "10:00",
				ToTime:     "15:59",
				Schedules:  []waveplanner.ScheduleItem{},
			},
			{
				ID:         uuid.NewString(),
				CutoffTime: "12:00",
				FromTime:   "16:00",
				ToTime:     "20:59",
				Schedules:  []waveplanner.ScheduleItem{},
			},
			{
				ID:         uuid.NewString(),
				CutoffTime: "16:00",
				FromTime:   "21:00",
				ToTime:     "09:59",
				Schedules:  []waveplanner.ScheduleItem{},
			},
		},
	}

mockProvider.
   AddInteraction().
   Given(fmt.Sprintf("retailer %s, mfc %s exists", retailer, mfc)).
   UponReceiving(fmt.Sprintf("a request to post wave plan for for retailer %s mfc %s", retailer, mfc)).
   WithRequest(http.MethodPost, fmt.Sprintf("/v1/retailers/%s/mfcs/%s/wavePlan", retailer, mfc), func(b *consumer.V4RequestBuilder) {
      b.
         Headers(matchers.HeadersMatcher{
            "X-Env-Type":   {matchers.Like("env")},
            "X-Token":      {matchers.Like("token")},
            "Content-Type": {matchers.S("application/json")},
         }).
         JSONBody(matchers.Like(body))
   }).
   WillRespondWith(http.StatusCreated, func(repsb *consumer.V4ResponseBuilder) {
      repsb.
         Headers(matchers.HeadersMatcher{
            "Content-Type": {matchers.S("application/json")},
         }).
         JSONBody(matchers.Like(created))
   })
running the test I get pact created, with these matching rules for _request/response bodies_:
Copy code
"matchingRules": {
  "body": {
    "$": {
      "combine": "AND",
      "matchers": [
        {
          "match": "type"
        }
      ]
    }
  },
and of course the pact contains request /response body content. My question is: does this matchingRule means that on Provider verification stage the request/response body example will be used as a source of expected request format? Before this I was using the older pact version and used Body: dsl.Like(map[string]interface{}{}) way of describing expected response format, which produced exact matching rules per field.
đź‘‹ 1
y
Yes I believe so, it will look at every node in your response given by the provider and check that is matches the type specified in the interaction response Do you have a copy of the matchers generated from your older test? I assume they are v2.0.0 not v3? v4 is shown in your example
v
Right..so this piece of code:
Copy code
Body: dsl.Like(map[string]interface{}{
   "code":                 dsl.Term(code, uuidMatcher),
   "mfc-id":               dsl.Like("D02"),
   "complete-at":          dsl.Timestamp(),
   "started-at":           dsl.Timestamp(),
   "picked-at":            dsl.Timestamp(),
   "closed-at":            dsl.Timestamp(),
   "cutoffs":              dsl.EachLike(dsl.Timestamp(), 1),
   "status":               dsl.Term("SPLIT", "INCOMPLETE|NEW|SPLIT|PROGRESS|COMPLETE"),
   "type":                 dsl.Term("PRELIM", "PRELIM|DELTA"),
   "zone":                 dsl.Term("STORE", "STORE|OSR|MANUAL"),
   "total-lines":          dsl.Like(10),
   "total-lines-complete": dsl.Like(9),
   "total-units":          dsl.Like(30),
   "total-units-picked":   dsl.Like(25),
   "out-of-stock-units":   dsl.Like(1),
}),
generates these matching rules for response body:
Copy code
"matchingRules": {
  "$.body": {
    "match": "type"
  },
  "$.body.closed-at": {
    "match": "regex",
    "regex": "^([\\+-]?\\d{4}(?!\\d{2}\\b))((-?)((0[1-9]|1[0-2])(\\3([12]\\d|0[1-9]|3[01]))?|W([0-4]\\d|5[0-2])(-?[1-7])?|(00[1-9]|0[1-9]\\d|[12]\\d{2}|3([0-5]\\d|6[1-6])))([T\\s]((([01]\\d|2[0-3])((:?)[0-5]\\d)?|24\\:?00)([\\.,]\\d+(?!:))?)?(\\17[0-5]\\d([\\.,]\\d+)?)?([zZ]|([\\+-])([01]\\d|2[0-3]):?([0-5]\\d)?)?)?)?$"
  },
  "$.body.code": {
    "match": "regex",
    "regex": "[0-9a-fA-F]{8}\\b-[0-9a-fA-F]{4}\\b-[0-9a-fA-F]{4}\\b-[0-9a-fA-F]{4}\\b-[0-9a-fA-F]{12}"
  },
  "$.body.complete-at": {
    "match": "regex",
    "regex": "^([\\+-]?\\d{4}(?!\\d{2}\\b))((-?)((0[1-9]|1[0-2])(\\3([12]\\d|0[1-9]|3[01]))?|W([0-4]\\d|5[0-2])(-?[1-7])?|(00[1-9]|0[1-9]\\d|[12]\\d{2}|3([0-5]\\d|6[1-6])))([T\\s]((([01]\\d|2[0-3])((:?)[0-5]\\d)?|24\\:?00)([\\.,]\\d+(?!:))?)?(\\17[0-5]\\d([\\.,]\\d+)?)?([zZ]|([\\+-])([01]\\d|2[0-3]):?([0-5]\\d)?)?)?)?$"
  },
  "$.body.cutoffs": {
    "min": 1
  },
  "$.body.cutoffs[*].*": {
    "match": "type"
  },
  "$.body.cutoffs[*]": {
    "match": "regex",
    "regex": "^([\\+-]?\\d{4}(?!\\d{2}\\b))((-?)((0[1-9]|1[0-2])(\\3([12]\\d|0[1-9]|3[01]))?|W([0-4]\\d|5[0-2])(-?[1-7])?|(00[1-9]|0[1-9]\\d|[12]\\d{2}|3([0-5]\\d|6[1-6])))([T\\s]((([01]\\d|2[0-3])((:?)[0-5]\\d)?|24\\:?00)([\\.,]\\d+(?!:))?)?(\\17[0-5]\\d([\\.,]\\d+)?)?([zZ]|([\\+-])([01]\\d|2[0-3]):?([0-5]\\d)?)?)?)?$"
  },
  "$.body.mfc-id": {
    "match": "type"
  },
  "$.body.out-of-stock-units": {
    "match": "type"
  },
  "$.body.picked-at": {
    "match": "regex",
    "regex": "^([\\+-]?\\d{4}(?!\\d{2}\\b))((-?)((0[1-9]|1[0-2])(\\3([12]\\d|0[1-9]|3[01]))?|W([0-4]\\d|5[0-2])(-?[1-7])?|(00[1-9]|0[1-9]\\d|[12]\\d{2}|3([0-5]\\d|6[1-6])))([T\\s]((([01]\\d|2[0-3])((:?)[0-5]\\d)?|24\\:?00)([\\.,]\\d+(?!:))?)?(\\17[0-5]\\d([\\.,]\\d+)?)?([zZ]|([\\+-])([01]\\d|2[0-3]):?([0-5]\\d)?)?)?)?$"
  },
  "$.body.started-at": {
    "match": "regex",
    "regex": "^([\\+-]?\\d{4}(?!\\d{2}\\b))((-?)((0[1-9]|1[0-2])(\\3([12]\\d|0[1-9]|3[01]))?|W([0-4]\\d|5[0-2])(-?[1-7])?|(00[1-9]|0[1-9]\\d|[12]\\d{2}|3([0-5]\\d|6[1-6])))([T\\s]((([01]\\d|2[0-3])((:?)[0-5]\\d)?|24\\:?00)([\\.,]\\d+(?!:))?)?(\\17[0-5]\\d([\\.,]\\d+)?)?([zZ]|([\\+-])([01]\\d|2[0-3]):?([0-5]\\d)?)?)?)?$"
  },
  "$.body.status": {
    "match": "regex",
    "regex": "INCOMPLETE|NEW|SPLIT|PROGRESS|COMPLETE"
  },
  "$.body.total-lines": {
    "match": "type"
  },
  "$.body.total-lines-complete": {
    "match": "type"
  },
  "$.body.total-units": {
    "match": "type"
  },
  "$.body.total-units-picked": {
    "match": "type"
  },
  "$.body.type": {
    "match": "regex",
    "regex": "PRELIM|DELTA"
  },
  "$.body.zone": {
    "match": "regex",
    "regex": "STORE|OSR|MANUAL"
  }
}
m
Yep, that looks about right to me.
Copy code
"$.body": {
    "match": "type"
  },
Says “recursively check the JSON to ensure it matches the same field types”. Then every specific matcher overrides that matching cascade, where there is a more specific matcher
Make sense?
BTW, how are you finding the 2.x.x interface compared to 1.x.x? I’m looking to release 2.x.x as stable soon, there is just one area I’d like to improve in the plugins/v4 interface
v
it looks good so far, but I've just started and this was my first test for post request. Before I wrote tests only for GET and it was easier:) F.i., I've found that content-type header is added to the request automatically and your service has to have it also, right? Also I still have doubts regarding list matching and optional fields. My response has an array field that may contain some values or may be empty depending on the data. I'm using matchers.ArrayMinLike, but it requires man array length 1. So I ended up having 2 different interactions: one with empty list, the other one with list with some values. Maybe you could suggest another solution here.
👍 1
m
F.i., I’ve found that content-type header is added to the request automatically and your service has to have it also, right?
Pact must know the content-type, yes, I think that’s true
Also I still have doubts regarding list matching and optional fields. My response has an array field that may contain some values or may be empty depending on the data. I’m using matchers.ArrayMinLike, but it requires man array length 1. So I ended up having 2 different interactions: one with empty list, the other one with list with some values. Maybe you could suggest another solution here.
that’s the solution
see howtooptional for more 👇
v
I continue to compare pact v1 and v2 and here is another diff: before this part of interaction
Copy code
WillRespondWith(
			dsl.Response{
				Status: http.StatusOK,
				Headers: dsl.MapMatcher{
					"Content-Type": dsl.String("application/json"),
				},
				Body: dsl.Like(map[string]interface{}{
					"code":      dsl.Term(code, uuidMatcher),
					"closed-at": dsl.Timestamp(),
				}),
			},
		)
generated this part of contract:
with v2 the same piece of interaction looks this way:
Copy code
WillRespondWith(http.StatusOK, func(b *consumer.V4ResponseBuilder) {
   b.
      Header("Content-Type", matchers.S("application/json")).
      JSONBody(matchers.MapMatcher{
         "code":      matchers.Term(code, uuidMatcher),
         "closed-at": matchers.Timestamp(),
      })
},
)
and the contract:
we can see that there are 2 new fields added to the body:
Copy code
"contentType": "application/json",
"encoded": false
why does pact add them if we have header content-type?..
m
something is not right for sure. I’ll need a repro project if you wouldn’t mind so I can get to the bottom of it
v
well....it is not a public repo
y
It should be feasible to create a minimal reproducible examples in a public repo. You could fork the pact-go repo and create examples in the test suite. Appreciate you cannot share your private code
v
But it is not the first version of the test which does not work. The test worked with v1...and does not work with v2. Service code didn't change. The only part which seems different is how I describe the response body
Copy code
Body: dsl.Like(map[string]interface{}{
					"code":      dsl.Term(code, uuidMatcher),
					"closed-at": dsl.Timestamp(),
				}),
and
Copy code
JSONBody(matchers.MapMatcher{
         "code":      matchers.Term(code, uuidMatcher),
         "closed-at": matchers.Timestamp(),
      })
What is the difference in these 2 descriptions?