diff --git a/dsl/interaction.go b/dsl/interaction.go index 1bf745db5..a94c933d2 100644 --- a/dsl/interaction.go +++ b/dsl/interaction.go @@ -21,48 +21,49 @@ type Interaction struct { } // Given specifies a provider state. Optional. -func (p *Interaction) Given(state string) *Interaction { - p.State = state - return p +func (i *Interaction) Given(state string) *Interaction { + i.State = state + + return i } // UponReceiving specifies the name of the test case. This becomes the name of // the consumer/provider pair in the Pact file. Mandatory. -func (p *Interaction) UponReceiving(description string) *Interaction { - p.Description = description - return p +func (i *Interaction) UponReceiving(description string) *Interaction { + i.Description = description + + return i } // WithRequest specifies the details of the HTTP request that will be used to // confirm that the Provider provides an API listening on the given interface. // Mandatory. -func (p *Interaction) WithRequest(request Request) *Interaction { - p.Request = request - - // Need to fix any weird JSON marshalling issues with the body Here - // If body is a string, not an object, we need to put it back into an object - // so that it's not double encoded - p.Request.Body = toObject(request.Body) +func (i *Interaction) WithRequest(request Request) *Interaction { + i.Request = request + + // Check if someone tried to add an object as a string representation + // as per original allowed implementation, e.g. + // { "foo": "bar", "baz": like("bat") } + if isJSONFormattedObject(request.Body) { + log.Println("[WARN] request body appears to be a JSON formatted object, " + + "no structural matching will occur. Support for structured strings has been" + + "deprecated as of 0.13.0") + } - return p + return i } // WillRespondWith specifies the details of the HTTP response that will be used to // confirm that the Provider must satisfy. Mandatory. -func (p *Interaction) WillRespondWith(response Response) *Interaction { - p.Response = response +func (i *Interaction) WillRespondWith(response Response) *Interaction { + i.Response = response - // Need to fix any weird JSON marshalling issues with the body Here - // If body is a string, not an object, we need to put it back into an object - // so that it's not double encoded - p.Response.Body = toObject(response.Body) - - return p + return i } -// Takes a string body and converts it to an interface{} representation. -func toObject(stringOrObject interface{}) interface{} { - +// Checks to see if someone has tried to submit a JSON string +// for an object, which is no longer supported +func isJSONFormattedObject(stringOrObject interface{}) bool { switch content := stringOrObject.(type) { case []byte: case string: @@ -70,14 +71,14 @@ func toObject(stringOrObject interface{}) interface{} { err := json.Unmarshal([]byte(content), &obj) if err != nil { - log.Printf("[DEBUG] interaction: error unmarshaling string '%v' into an object. Probably not an object: %v\n", stringOrObject, err.Error()) - return content + return false } - return obj - default: - // leave alone + // Check if a map type + if _, ok := obj.(map[string]interface{}); ok { + return true + } } - return stringOrObject + return false } diff --git a/dsl/interaction_test.go b/dsl/interaction_test.go index 03a5b9467..cff7ace20 100644 --- a/dsl/interaction_test.go +++ b/dsl/interaction_test.go @@ -45,10 +45,10 @@ func TestInteraction_WithRequest(t *testing.T) { Given("Some state"). UponReceiving("Some name for the test"). WithRequest(Request{ - Body: `{ - "foo": "bar", - "baz": "bat" - }`, + Body: map[string]string{ + "foo": "bar", + "baz": "bat", + }, }) obj := map[string]string{ @@ -60,12 +60,8 @@ func TestInteraction_WithRequest(t *testing.T) { body, _ := json.Marshal(obj) json.Unmarshal(body, &expect) - if _, ok := i.Request.Body.(map[string]interface{}); !ok { - t.Fatalf("Expected response to be of type 'map[string]string'") - } - - if !reflect.DeepEqual(i.Request.Body, expect) { - t.Fatalf("Expected response object body '%v' to match '%v'", i.Request.Body, expect) + if _, ok := i.Request.Body.(map[string]string); !ok { + t.Fatal("Expected response to be of type 'map[string]string', but got", reflect.TypeOf(i.Request.Body)) } } @@ -95,10 +91,10 @@ func TestInteraction_WillRespondWith(t *testing.T) { UponReceiving("Some name for the test"). WithRequest(Request{}). WillRespondWith(Response{ - Body: `{ + Body: map[string]string{ "foo": "bar", - "baz": "bat" - }`, + "baz": "bat", + }, }) obj := map[string]string{ @@ -110,37 +106,21 @@ func TestInteraction_WillRespondWith(t *testing.T) { body, _ := json.Marshal(obj) json.Unmarshal(body, &expect) - if _, ok := i.Response.Body.(map[string]interface{}); !ok { - t.Fatalf("Expected response to be of type 'map[string]string'") - } - - if !reflect.DeepEqual(i.Response.Body, expect) { - t.Fatalf("Expected response object body '%v' to match '%v'", i.Response.Body, expect) + if _, ok := i.Response.Body.(map[string]string); !ok { + t.Fatal("Expected response to be of type 'map[string]string', but got", reflect.TypeOf(i.Response.Body)) } } -func TestInteraction_toObject(t *testing.T) { - // unstructured string should not be changed - res := toObject("somestring") - content, ok := res.(string) - - if !ok { - t.Fatalf("must be a string") - } - - if content != "somestring" { - t.Fatalf("Expected 'somestring' but got '%s'", content) - } - - // errors should return a string repro of original interface{} - res = toObject("") - content, ok = res.(string) - - if !ok { - t.Fatalf("must be a string") +func TestInteraction_isStringLikeObject(t *testing.T) { + testCases := map[string]bool{ + "somestring": false, + "": false, + `{"foo":"bar"}`: true, } - if content != "" { - t.Fatalf("Expected '' but got '%s'", content) + for testCase, want := range testCases { + if isJsonFormattedObject(testCase) != want { + t.Fatal("want", want, "got", !want, "for test case", testCase) + } } } diff --git a/dsl/matcher.go b/dsl/matcher.go index 1d4967526..4bbfd0509 100644 --- a/dsl/matcher.go +++ b/dsl/matcher.go @@ -47,7 +47,7 @@ type term struct { func EachLike(content interface{}, minRequired int) Matcher { return Matcher{ "json_class": "Pact::ArrayLike", - "contents": toObject(content), + "contents": content, "min": minRequired, } } @@ -57,7 +57,7 @@ func EachLike(content interface{}, minRequired int) Matcher { func Like(content interface{}) Matcher { return Matcher{ "json_class": "Pact::SomethingLike", - "contents": toObject(content), + "contents": content, } } @@ -67,11 +67,11 @@ func Term(generate string, matcher string) Matcher { return Matcher{ "json_class": "Pact::Term", "data": map[string]interface{}{ - "generate": toObject(generate), + "generate": generate, "matcher": map[string]interface{}{ "json_class": "Regexp", "o": 0, - "s": toObject(matcher), + "s": matcher, }, }, } @@ -162,42 +162,10 @@ type Matcher map[string]interface{} func (m Matcher) isMatcher() {} -// MarshalJSON is a custom encoder for Header type -func (m Matcher) MarshalJSON() ([]byte, error) { - obj := map[string]interface{}{} - - for header, value := range m { - obj[header] = toObject(value) - } - - return json.Marshal(obj) -} - -// UnmarshalJSON is a custom decoder for Header type -func (m *Matcher) UnmarshalJSON(data []byte) error { - return json.Unmarshal(data, &m) -} - // MapMatcher allows a map[string]string-like object // to also contain complex matchers type MapMatcher map[string]StringMatcher -// MarshalJSON is a custom encoder for Header type -func (h MapMatcher) MarshalJSON() ([]byte, error) { - obj := map[string]interface{}{} - - for header, value := range h { - obj[header] = toObject(value) - } - - return json.Marshal(obj) -} - -// UnmarshalJSON is a custom decoder for Header type -func (h *MapMatcher) UnmarshalJSON(data []byte) error { - return json.Unmarshal(data, &h) -} - // Takes an object and converts it to a JSON representation func objectToString(obj interface{}) string { switch content := obj.(type) { diff --git a/dsl/matcher_test.go b/dsl/matcher_test.go index 435bafea1..f87809c86 100644 --- a/dsl/matcher_test.go +++ b/dsl/matcher_test.go @@ -58,19 +58,6 @@ func TestMatcher_LikeAsObject(t *testing.T) { } } -func TestMatcher_LikeAsObjectString(t *testing.T) { - expected := formatJSON(` - { - "contents": {"baz":"bat"}, - "json_class": "Pact::SomethingLike" - }`) - - match := formatJSON(Like(`{"baz":"bat"}`)) - if expected != match { - t.Fatalf("Expected Term to match. '%s' != '%s'", expected, match) - } -} - func TestMatcher_LikeNumber(t *testing.T) { expected := formatJSON(` { @@ -87,7 +74,7 @@ func TestMatcher_LikeNumber(t *testing.T) { func TestMatcher_LikeNumberAsString(t *testing.T) { expected := formatJSON(` { - "contents": 42, + "contents": "42", "json_class": "Pact::SomethingLike" }`) @@ -113,7 +100,7 @@ func TestMatcher_EachLikeNumber(t *testing.T) { func TestMatcher_EachLikeNumberAsString(t *testing.T) { expected := formatJSON(` { - "contents": 42, + "contents": "42", "json_class": "Pact::ArrayLike", "min": 1 }`) @@ -154,7 +141,7 @@ func TestMatcher_EachLikeObject(t *testing.T) { } } -func TestMatcher_EachLikeObjectAsString(t *testing.T) { +func TestMatcher_EachLikeObjectAsStringFail(t *testing.T) { expected := formatJSON(` { "contents": {"somekey":"someval"}, @@ -163,8 +150,8 @@ func TestMatcher_EachLikeObjectAsString(t *testing.T) { }`) match := formatJSON(EachLike(`{"somekey":"someval"}`, 3)) - if expected != match { - t.Fatalf("Expected Term to match. '%s' != '%s'", expected, match) + if expected == match { + t.Fatalf("Expected Term to NOT match. '%s' != '%s'", expected, match) } } @@ -182,20 +169,6 @@ func TestMatcher_EachLikeArray(t *testing.T) { } } -func TestMatcher_EachLikeArrayString(t *testing.T) { - expected := formatJSON(` - { - "contents": [1,2,3], - "json_class": "Pact::ArrayLike", - "min": 1 - }`) - - match := formatJSON(EachLike("[1,2,3]", 1)) - if expected != match { - t.Fatalf("Expected Term to match. '%s' != '%s'", expected, match) - } -} - func TestMatcher_NestLikeInEachLike(t *testing.T) { expected := formatJSON(` { @@ -510,18 +483,6 @@ func ExampleLike_object() { // "json_class": "Pact::SomethingLike" //} } -func ExampleLike_objectString() { - match := Like(`{"baz":"bat"}`) - fmt.Println(formatJSON(match)) - // Output: - //{ - // "contents": { - // "baz": "bat" - // }, - // "json_class": "Pact::SomethingLike" - //} -} - func ExampleLike_number() { match := Like(42) fmt.Println(formatJSON(match)) diff --git a/dsl/message.go b/dsl/message.go index 4bf0d9b40..c153d5fbb 100644 --- a/dsl/message.go +++ b/dsl/message.go @@ -44,7 +44,7 @@ func (p *Message) WithMetadata(metadata MapMatcher) *Message { // confirm that the Provider provides an API listening on the given interface. // Mandatory. func (p *Message) WithContent(content interface{}) *Message { - p.Content = toObject(content) + p.Content = content return p } diff --git a/dsl/mock_service_test.go b/dsl/mock_service_test.go index e7c89859e..63e8d47bd 100644 --- a/dsl/mock_service_test.go +++ b/dsl/mock_service_test.go @@ -15,15 +15,12 @@ import ( // Simple mock server for testing. This is getting confusing... func setupMockServer(success bool, t *testing.T) *httptest.Server { ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - request, err := ioutil.ReadAll(r.Body) + _, err := ioutil.ReadAll(r.Body) r.Body.Close() if err != nil { log.Fatal(err) } - t.Logf("%v\n", r) - t.Logf("Request Body: %s\n", request) - if success { fmt.Fprintln(w, "Hello, client") } else { diff --git a/examples/consumer/goconsumer/user_service_test.go b/examples/consumer/goconsumer/user_service_test.go index e4fa62b14..c1ad3486b 100644 --- a/examples/consumer/goconsumer/user_service_test.go +++ b/examples/consumer/goconsumer/user_service_test.go @@ -32,9 +32,7 @@ var term = dsl.Term type s = dsl.String type request = dsl.Request -// var str = dsl.S -var loginRequest = fmt.Sprintf(`{ "username":"%s", "password": "issilly" }`, name) - +var loginRequest = map[string]string{"username": name, "password": "issilly"} var commonHeaders = dsl.MapMatcher{ "Content-Type": term("application/json; charset=utf-8", `application\/json`), } @@ -142,12 +140,16 @@ func TestPactConsumerLoginHandler_UserExists(t *testing.T) { Query: dsl.MapMatcher{ "foo": term("bar", "[a-zA-Z]+"), }, - Body: loginRequest, + Body: loginRequest, + Headers: commonHeaders, }). WillRespondWith(dsl.Response{ - Status: 200, - Body: body, - Headers: commonHeaders, + Status: 200, + Body: body, + Headers: dsl.MapMatcher{ + "X-Api-Correlation-Id": dsl.Like("100"), + "Content-Type": term("application/json; charset=utf-8", `application\/json`), + }, }) err := pact.Verify(testBillyExists) diff --git a/examples/gin/provider/user_service.go b/examples/gin/provider/user_service.go index 2e5dc679b..fca41afc7 100644 --- a/examples/gin/provider/user_service.go +++ b/examples/gin/provider/user_service.go @@ -26,6 +26,8 @@ var userRepository = &types.UserRepository{ // UserLogin is the login route. func UserLogin(c *gin.Context) { + c.Header("X-Api-Correlation-Id", "1234") + var json Login if c.BindJSON(&json) == nil { user, err := userRepository.ByUsername(json.User) diff --git a/examples/go-kit/provider/transport.go b/examples/go-kit/provider/transport.go index 6ec23d573..20137e4fd 100644 --- a/examples/go-kit/provider/transport.go +++ b/examples/go-kit/provider/transport.go @@ -51,11 +51,13 @@ func encodeResponse(ctx context.Context, w http.ResponseWriter, response interfa return nil } w.Header().Set("Content-Type", "application/json; charset=utf-8") + w.Header().Set("X-Api-Correlation-Id", "1234") return json.NewEncoder(w).Encode(response) } func encodeError(_ context.Context, err error, w http.ResponseWriter) { w.Header().Set("Content-Type", "application/json; charset=utf-8") + w.Header().Set("X-Api-Correlation-Id", "1234") w.WriteHeader(codeFrom(err)) json.NewEncoder(w).Encode(map[string]interface{}{ "error": err.Error(), diff --git a/examples/mux/provider/user_service.go b/examples/mux/provider/user_service.go index f7e19a84e..6549b3f84 100644 --- a/examples/mux/provider/user_service.go +++ b/examples/mux/provider/user_service.go @@ -22,6 +22,7 @@ var userRepository = &types.UserRepository{ var UserLogin = func(w http.ResponseWriter, r *http.Request) { var login types.LoginRequest w.Header().Set("Content-Type", "application/json; charset=utf-8") + w.Header().Set("X-Api-Correlation-Id", "1234") body, err := ioutil.ReadAll(r.Body) defer r.Body.Close() diff --git a/scripts/pact.sh b/scripts/pact.sh index cd6604093..f2986af78 100755 --- a/scripts/pact.sh +++ b/scripts/pact.sh @@ -29,8 +29,6 @@ export PACT_BROKER_HOST="https://test.pact.dius.com.au" export PACT_BROKER_USERNAME="dXfltyFMgNOFZAxr8io9wJ37iUpY42M" export PACT_BROKER_PASSWORD="O5AIZWxelWbLvqMd8PkAVycBJh2Psyg1" export PATH="../build/pact/bin:${PATH}" -echo "which provier verifier? ->" -which pact-provider-verifier step "Running E2E regression and example projects" examples=("github.com/pact-foundation/pact-go/examples/consumer/goconsumer" "github.com/pact-foundation/pact-go/examples/go-kit/provider" "github.com/pact-foundation/pact-go/examples/mux/provider" "github.com/pact-foundation/pact-go/examples/gin/provider")