diff --git a/Makefile b/Makefile index 7b3211448..f85f4fcaf 100755 --- a/Makefile +++ b/Makefile @@ -43,7 +43,7 @@ pact: install docker go test -v -tags=consumer -count=1 github.com/pact-foundation/pact-go/examples/v2/... -run TestExample go test -v -tags=provider -count=1 github.com/pact-foundation/pact-go/examples/v2/... -run TestExample -pactv3: installv3 +pactv3: clean #installv3 @echo "--- 🔨 Running Pact examples" mkdir -p ./examples/v3/pacts go test -v -tags=consumer -count=1 github.com/pact-foundation/pact-go/examples/v3/... diff --git a/examples/v2/consumer/goconsumer/user_service_test.go b/examples/v2/consumer/goconsumer/user_service_test.go index 8ae310d45..798fd7c60 100644 --- a/examples/v2/consumer/goconsumer/user_service_test.go +++ b/examples/v2/consumer/goconsumer/user_service_test.go @@ -145,20 +145,12 @@ func TestExampleConsumerLoginHandler_UserExists(t *testing.T) { AddInteraction(). Given("User jmarie exists"). UponReceiving("A request to login with user 'jmarie'"). - WithRequest(request{ - Method: "POST", - Path: term("/login/10", "/login/[0-9]+"), - Query: dsl.MapMatcher{ - "foo": term("bar", "[a-zA-Z]+"), - }, - Body: loginRequest, - Headers: commonHeaders, - }). + WillRespondWith(dsl.Response{ Status: 200, - Body: dsl.Match(ex.LoginResponse{ - User: &ex.User{}, - }), + Body: map[string]interface{}{ + "Foo": eachLike("foo", 0), + }, Headers: dsl.MapMatcher{ "X-Api-Correlation-Id": dsl.Like("100"), "Content-Type": term("application/json; charset=utf-8", `application\/json`), diff --git a/examples/v2/pacts/consumer-httpbin.json b/examples/v2/pacts/consumer-httpbin.json deleted file mode 100644 index d034ce33d..000000000 --- a/examples/v2/pacts/consumer-httpbin.json +++ /dev/null @@ -1,57 +0,0 @@ -{ - "consumer": { - "name": "consumer" - }, - "provider": { - "name": "httpbin" - }, - "interactions": [ - { - "description": "A request to the GET echo service", - "request": { - "method": "GET", - "path": "/get" - }, - "response": { - "status": 200 - } - }, - { - "description": "A request to the GET 404 status service", - "request": { - "method": "GET", - "path": "/status/404" - }, - "response": { - "status": 404 - } - }, - { - "description": "A GET request to the bearer service", - "request": { - "method": "GET", - "path": "/bearer", - "headers": { - "Authorization": "Bearer 1234" - } - }, - "response": { - "status": 200, - "body": { - "authenticated": true, - "token": "1234" - }, - "matchingRules": { - "$.body.token": { - "match": "type" - } - } - } - } - ], - "metadata": { - "pactSpecification": { - "version": "2.0.0" - } - } -} diff --git a/examples/v2/pacts/consumer-selfsignedtls.json b/examples/v2/pacts/consumer-selfsignedtls.json deleted file mode 100644 index 36cef42e4..000000000 --- a/examples/v2/pacts/consumer-selfsignedtls.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "consumer": { - "name": "consumer" - }, - "provider": { - "name": "httpbin" - }, - "interactions": [ - { - "description": "A request to the hello self-signed API", - "request": { - "method": "GET", - "path": "/hello" - }, - "response": { - "status": 200 - } - } - ], - "metadata": { - "pactSpecification": { - "version": "2.0.0" - } - } -} diff --git a/examples/v2/pacts/jmarie-loginprovider.json b/examples/v2/pacts/jmarie-loginprovider.json deleted file mode 100644 index 027ee288d..000000000 --- a/examples/v2/pacts/jmarie-loginprovider.json +++ /dev/null @@ -1,232 +0,0 @@ -{ - "consumer": { - "name": "jmarie" - }, - "provider": { - "name": "loginprovider" - }, - "interactions": [ - { - "description": "A request to login with user 'jmarie'", - "providerState": "User jmarie exists", - "request": { - "method": "POST", - "path": "/login/10", - "query": "foo=bar", - "headers": { - "Content-Type": "application/json; charset=utf-8" - }, - "body": { - "username": "jmarie", - "password": "issilly" - }, - "matchingRules": { - "$.path": { - "match": "regex", - "regex": "\\/login\\/[0-9]+" - }, - "$.query.foo[0]": { - "match": "regex", - "regex": "[a-zA-Z]+" - }, - "$.headers.Content-Type": { - "match": "regex", - "regex": "application\\/json" - } - } - }, - "response": { - "status": 200, - "headers": { - "Content-Type": "application/json; charset=utf-8", - "X-Api-Correlation-Id": "100", - "X-Auth-Token": "1234" - }, - "body": { - "user": { - "id": 10, - "name": "Jean-Marie de La Beaujardière😀😍", - "password": "password123", - "type": "admin", - "username": "jmarie" - } - }, - "matchingRules": { - "$.headers.Content-Type": { - "match": "regex", - "regex": "application\\/json" - }, - "$.headers.X-Api-Correlation-Id": { - "match": "type" - }, - "$.headers.X-Auth-Token": { - "match": "type" - }, - "$.body.user.id": { - "match": "type" - }, - "$.body.user.name": { - "match": "type" - }, - "$.body.user.password": { - "match": "type" - }, - "$.body.user.type": { - "match": "regex", - "regex": "^(admin|user|guest)$" - }, - "$.body.user.username": { - "match": "type" - } - } - } - }, - { - "description": "A request to login with user 'jmarie'", - "providerState": "User jmarie does not exist", - "request": { - "method": "POST", - "path": "/login/10", - "query": "foo=anything", - "headers": { - "Content-Type": "application/json; charset=utf-8" - }, - "body": { - "username": "jmarie", - "password": "issilly" - }, - "matchingRules": { - "$.headers.Content-Type": { - "match": "regex", - "regex": "application\\/json" - } - } - }, - "response": { - "status": 404, - "headers": { - "Content-Type": "application/json; charset=utf-8" - }, - "matchingRules": { - "$.headers.Content-Type": { - "match": "regex", - "regex": "application\\/json" - } - } - } - }, - { - "description": "A request to login with user 'jmarie'", - "providerState": "User jmarie is unauthorized", - "request": { - "method": "POST", - "path": "/login/10", - "headers": { - "Content-Type": "application/json; charset=utf-8" - }, - "body": { - "username": "jmarie", - "password": "issilly" - }, - "matchingRules": { - "$.headers.Content-Type": { - "match": "regex", - "regex": "application\\/json" - } - } - }, - "response": { - "status": 401, - "headers": { - "Content-Type": "application/json; charset=utf-8" - }, - "matchingRules": { - "$.headers.Content-Type": { - "match": "regex", - "regex": "application\\/json" - } - } - } - }, - { - "description": "A request to get user 'jmarie'", - "providerState": "User jmarie is authenticated", - "request": { - "method": "GET", - "path": "/users/10", - "headers": { - "Authorization": "Bearer 1234" - } - }, - "response": { - "status": 200, - "headers": { - "Content-Type": "application/json; charset=utf-8" - }, - "body": { - "id": 10, - "name": "Jean-Marie de La Beaujardière😀😍", - "password": "password123", - "type": "admin", - "username": "jmarie" - }, - "matchingRules": { - "$.headers.Content-Type": { - "match": "regex", - "regex": "application\\/json" - }, - "$.body.id": { - "match": "type" - }, - "$.body.name": { - "match": "type" - }, - "$.body.password": { - "match": "type" - }, - "$.body.type": { - "match": "regex", - "regex": "^(admin|user|guest)$" - }, - "$.body.username": { - "match": "type" - } - } - } - }, - { - "description": "A request to get with user 'jmarie'", - "providerState": "User jmarie is unauthenticated", - "request": { - "method": "GET", - "path": "/users/10", - "headers": { - "Content-Type": "application/json; charset=utf-8" - }, - "matchingRules": { - "$.headers.Content-Type": { - "match": "regex", - "regex": "application\\/json" - } - } - }, - "response": { - "status": 401, - "headers": { - "Content-Type": "application/json; charset=utf-8" - }, - "matchingRules": { - "$.headers.Content-Type": { - "match": "regex", - "regex": "application\\/json" - } - } - } - } - ], - "metadata": { - "pactSpecification": { - "version": "2.0.0" - } - } -} \ No newline at end of file diff --git a/examples/v2/pacts/myconsumer-myprovider.json b/examples/v2/pacts/myconsumer-myprovider.json deleted file mode 100644 index 7e2da4258..000000000 --- a/examples/v2/pacts/myconsumer-myprovider.json +++ /dev/null @@ -1,48 +0,0 @@ -{ - "consumer": { - "name": "MyConsumer" - }, - "provider": { - "name": "MyProvider" - }, - "interactions": [ - { - "description": "A request to get foo", - "providerState": "User foo exists", - "request": { - "method": "GET", - "path": "/foobar", - "headers": { - "Authorization": "Bearer 1234", - "Content-Type": "application/json" - }, - "body": { - "name": "billy" - } - }, - "response": { - "status": 200, - "headers": { - "Content-Type": "application/json" - }, - "body": { - "lastName": "sampson", - "name": "billy" - }, - "matchingRules": { - "$.body.lastName": { - "match": "type" - }, - "$.body.name": { - "match": "type" - } - } - } - } - ], - "metadata": { - "pactSpecification": { - "version": "2.0.0" - } - } -} \ No newline at end of file diff --git a/examples/v2/pacts/pactgomessageconsumer-pactgomessageprovider.json b/examples/v2/pacts/pactgomessageconsumer-pactgomessageprovider.json deleted file mode 100644 index d011a1a7c..000000000 --- a/examples/v2/pacts/pactgomessageconsumer-pactgomessageprovider.json +++ /dev/null @@ -1,110 +0,0 @@ -{ - "consumer": { - "name": "PactGoMessageConsumer" - }, - "provider": { - "name": "PactGoMessageProvider" - }, - "messages": [ - { - "description": "a user", - "providerStates": [ - { - "name": "user with id 127 exists", - "params": null - } - ], - "contents": { - "access": [ - { - "role": "admin" - }, - { - "role": "admin" - }, - { - "role": "admin" - } - ], - "id": 127, - "name": "Baz" - }, - "matchingRules": { - "body": { - "$.access": { - "matchers": [ - { - "min": 3 - } - ] - }, - "$.access[*].*": { - "matchers": [ - { - "match": "type" - } - ] - }, - "$.access[*].role": { - "matchers": [ - { - "match": "regex", - "regex": "admin|controller|user" - } - ] - }, - "$.id": { - "matchers": [ - { - "match": "type" - } - ] - } - } - }, - "metaData": { - "Content-Type": "application/json; charset=utf-8" - } - }, - { - "description": "an order", - "providerStates": [ - { - "name": "an order exists", - "params": null - } - ], - "contents": { - "id": 42, - "item": "apple" - }, - "matchingRules": { - "body": { - "$.id": { - "matchers": [ - { - "match": "type" - } - ] - }, - "$.item": { - "matchers": [ - { - "match": "regex", - "regex": "(apple|orange)" - } - ] - } - } - }, - "metaData": { - "Content-Type": "application/json; charset=utf-8" - } - } - ], - "metadata": { - "pactSpecification": { - "version": "3.0.0" - } - } -} \ No newline at end of file diff --git a/examples/v3/consumer_test.go b/examples/v3/consumer_test.go index 2f30cd3f2..448dc8a60 100644 --- a/examples/v3/consumer_test.go +++ b/examples/v3/consumer_test.go @@ -7,7 +7,6 @@ import ( "errors" "fmt" "io/ioutil" - "log" "net/http" "net/url" "strings" @@ -30,17 +29,7 @@ func TestConsumerV2(t *testing.T) { TLS: true, }) - // Override default matching behaviour - // mockProvider.SetMatchingConfig(v3.PactSerialisationOptionsV2{ - // QueryStringStyle: v3.AlwaysArray, - // QueryStringStyle: v3.Array, - // QueryStringStyle: v3.Default, - // }) - - // TODO: probably better than deferring to the execute test phase, but not sure - if err != nil { - t.Fatal(err) - } + assert.NoError(t, err) // Set up our expected interactions. mockProvider. @@ -56,7 +45,6 @@ func TestConsumerV2(t *testing.T) { v3.Regex("baz", "[a-z]+"), }, }). - // Body: v3.MatchV2(&User{}), JSON(v3.MapMatcher{ "id": v3.Like(27), "name": v3.Like("billy"), @@ -65,24 +53,56 @@ func TestConsumerV2(t *testing.T) { }). WillRespondWith(200). Headers(v3.MapMatcher{"Content-Type": v3.Regex("application/json", "application\\/json")}). - // // Body: v3.Match(&User{}), JSON(v3.MapMatcher{ "dateTime": v3.Regex("2020-01-01", "[0-9\\-]+"), - "name": s("FirstName"), - "lastName": s("LastName"), + "name": s("Billy"), + "lastName": s("Sampson"), "itemsMin": v3.ArrayMinLike("thereshouldbe3ofthese", 3), - // Add any of these this to demonstrate adding a v3 matcher failing the build (not at the type system level unfortunately) - // "id": v3.Integer(1), - // "superstring": v3.Includes("foo"), - // "accountBalance": v3.Decimal(123.76), - // "itemsMinMax": v3.ArrayMinMaxLike(27, 3, 5), - // "equality": v3.Equality("a thing"), }) // Execute pact test - if err := mockProvider.ExecuteTest(test); err != nil { - log.Fatalf("Error on Verify: %v", err) - } + err = mockProvider.ExecuteTest(test) + assert.NoError(t, err) +} + +func TestConsumerV2_Match(t *testing.T) { + v3.SetLogLevel("TRACE") + + mockProvider, err := v3.NewHTTPMockProviderV2(v3.MockHTTPProviderConfigV2{ + Consumer: "V2ConsumerMatch", + Provider: "V2ProviderMatch", + Host: "127.0.0.1", + Port: 8080, + TLS: true, + }) + + assert.NoError(t, err) + + // Set up our expected interactions. + mockProvider. + AddInteraction(). + Given("User foo exists"). + UponReceiving("A request to do a foo"). + WithRequest("POST", v3.Regex("/foobar", `\/foo.*`)). + HeadersArray(v3.HeadersMatcher{ + "Content-Type": []v3.Matcher{s("application/json")}, + "Authorization": []v3.Matcher{v3.Like("Bearer 1234")}, + }). + Query(v3.QueryMatcher{ + "baz": []v3.Matcher{ + v3.Regex("bar", "[a-z]+"), + v3.Regex("bat", "[a-z]+"), + v3.Regex("baz", "[a-z]+"), + }, + }). + BodyMatch(&User{}). + WillRespondWith(200). + Headers(v3.MapMatcher{"Content-Type": v3.Regex("application/json", "application\\/json")}). + BodyMatch(&User{}) + + // Execute pact test + err = mockProvider.ExecuteTest(test) + assert.NoError(t, err) } func TestConsumerV3(t *testing.T) { @@ -95,10 +115,7 @@ func TestConsumerV3(t *testing.T) { Port: 8080, TLS: true, }) - - if err != nil { - t.Fatal(err) - } + assert.NoError(t, err) // Set up our expected interactions. mockProvider. @@ -127,11 +144,10 @@ func TestConsumerV3(t *testing.T) { }). WillRespondWith(200). Headers(v3.MapMatcher{"Content-Type": s("application/json")}). - // Body: v3.MatchV3(&User{}), JSON(v3.MapMatcher{ "dateTime": v3.Regex("2020-01-01", "[0-9\\-]+"), - "name": s("FirstName"), - "lastName": s("LastName"), + "name": s("Billy"), + "lastName": s("Sampson"), "superstring": v3.Includes("foo"), "id": v3.Integer(12), "accountBalance": v3.Decimal(123.76), @@ -148,9 +164,8 @@ func TestConsumerV3(t *testing.T) { }) // Execute pact test - if err := mockProvider.ExecuteTest(test); err != nil { - log.Fatalf("Error on Verify: %v", err) - } + err = mockProvider.ExecuteTest(test) + assert.NoError(t, err) } func TestMessagePact(t *testing.T) { @@ -159,7 +174,6 @@ func TestMessagePact(t *testing.T) { provider, err := v3.NewMessagePactV3(v3.MessageConfig{ Consumer: "V3MessageConsumer", Provider: "V3MessageProvider", // must be different to the HTTP one, can't mix both interaction styles - // SpecificationVersion: v3.V3, }) assert.NoError(t, err) @@ -176,8 +190,8 @@ func TestMessagePact(t *testing.T) { }). JSON(v3.MapMatcher{ "datetime": v3.Regex("2020-01-01", "[0-9\\-]+"), - "name": s("FirstName"), - "lastName": s("LastName"), + "name": s("Billy"), + "lastName": s("Sampson"), "id": v3.Integer(12), }). AsType(&User{}). @@ -189,11 +203,9 @@ func TestMessagePact(t *testing.T) { type User struct { ID int `json:"id" pact:"example=27"` - Name string `json:"name" pact:"example=billy"` - LastName string `json:"lastName" pact:"example=sampson"` + Name string `json:"name" pact:"example=Billy"` + LastName string `json:"lastName" pact:"example=Sampson"` Date string `json:"datetime" pact:"example=2020-01-01'T'08:00:45,format=yyyy-MM-dd'T'HH:mm:ss,generator=datetime"` - // Date string `json:"datetime" pact:"example=2020-01-01'T'08:00:45,regex=[0-9-]+,format=yyyy-MM-dd'T'HH:mm:ss,generator=datetime"` - // Date string `json:"datetime" pact:"example=20200101,regex=[0-9a-z-A-Z]+"` } // Pass in test case @@ -207,12 +219,10 @@ var test = func(config v3.MockServerConfig) error { req := &http.Request{ Method: "POST", URL: &url.URL{ - Host: fmt.Sprintf("%s:%d", "localhost", config.Port), - Scheme: "https", - Path: "/foobar", - // RawQuery: "baz=foo&baz=foo&baz=foo", // TODO: Currently doesn't support matching rules being sent over the wire, so must have exact values + Host: fmt.Sprintf("%s:%d", "localhost", config.Port), + Scheme: "https", + Path: "/foobar", RawQuery: "baz=bat&baz=foo&baz=something", // Default behaviour, test matching - // RawQuery: "baz[]=bat&baz[]=foo&baz[]=something", // TODO: Rust v3 does not support this syntax }, Body: ioutil.NopCloser(strings.NewReader(`{"id": 27, "name":"billy", "lastName":"sampson", "datetime":"2021-01-01T08:00:45"}`)), Header: make(http.Header), diff --git a/examples/v3/pacts/V2Consumer-V2Provider.json b/examples/v3/pacts/V2Consumer-V2Provider.json index 7166d776a..55bb2f66a 100644 --- a/examples/v3/pacts/V2Consumer-V2Provider.json +++ b/examples/v3/pacts/V2Consumer-V2Provider.json @@ -8,9 +8,9 @@ "providerState": "User foo exists", "request": { "body": { - "datetime": "2020-01-01'T'08:00:45,format=yyyy-MM-dd'T'HH:mm:ss,generator=datetime", + "datetime": "2020-01-01'T'08:00:45", "id": 27, - "lastName": "sampson", + "lastName": "billy", "name": "billy" }, "headers": { @@ -34,9 +34,10 @@ "match": "type" }, "$.header.Content-Type": { - "match": "type" + "match": "regex", + "regex": "application\\/json" }, - "$.path.": { + "$.path": { "match": "regex", "regex": "\\/foo.*" }, @@ -62,11 +63,11 @@ "dateTime": "2020-01-01", "itemsMin": [ "thereshouldbe3ofthese", - null, - null + "thereshouldbe3ofthese", + "thereshouldbe3ofthese" ], - "lastName": "LastName", - "name": "FirstName" + "lastName": "Sampson", + "name": "Billy" }, "headers": { "Content-Type": "application/json" @@ -77,16 +78,8 @@ "regex": "[0-9\\-]+" }, "$.body.itemsMin": { - "match": "type" - }, - "$.body.lastName": { - "match": "type" - }, - "$.body.name": { - "match": "type" - }, - "$.header.Content-Type": { - "match": "type" + "match": "type", + "min": 3 } }, "status": 200 @@ -94,11 +87,8 @@ } ], "metadata": { - "pactGo": { - "version": "v1.4.3" - }, "pactRust": { - "version": "0.8.6" + "version": "0.9.2" }, "pactSpecification": { "version": "2.0.0" diff --git a/examples/v3/pacts/V2ConsumerMatch-V2ProviderMatch.json b/examples/v3/pacts/V2ConsumerMatch-V2ProviderMatch.json new file mode 100644 index 000000000..8b635bda7 --- /dev/null +++ b/examples/v3/pacts/V2ConsumerMatch-V2ProviderMatch.json @@ -0,0 +1,77 @@ +{ + "consumer": { + "name": "V2ConsumerMatch" + }, + "interactions": [ + { + "description": "A request to do a foo", + "providerState": "User foo exists", + "request": { + "body": { + "datetime": "2020-01-01'T'08:00:45,format=yyyy-MM-dd'T'HH:mm:ss,generator=datetime", + "id": 27, + "lastName": "Sampson", + "name": "Billy" + }, + "headers": { + "Authorization": "Bearer 1234", + "Content-Type": "application/json" + }, + "matchingRules": { + "$.body.datetime": { + "match": "type" + }, + "$.body.id": { + "match": "type" + }, + "$.body.lastName": { + "match": "type" + }, + "$.body.name": { + "match": "type" + }, + "$.header.Authorization": { + "match": "type" + }, + "$.header.Content-Type": { + "match": "regex", + "regex": "application\\/json" + }, + "$.path": { + "match": "regex", + "regex": "\\/foo.*" + }, + "$.query.baz[0]": { + "match": "regex", + "regex": "[a-z]+" + }, + "$.query.baz[1]": { + "match": "regex", + "regex": "[a-z]+" + }, + "$.query.baz[2]": { + "match": "regex", + "regex": "[a-z]+" + } + }, + "method": "POST", + "path": "/foobar", + "query": "baz=bar&baz=bat&baz=baz" + }, + "response": { + "status": 200 + } + } + ], + "metadata": { + "pactRust": { + "version": "0.9.2" + }, + "pactSpecification": { + "version": "2.0.0" + } + }, + "provider": { + "name": "V2ProviderMatch" + } +} \ No newline at end of file diff --git a/examples/v3/pacts/V3Consumer-V3Provider.json b/examples/v3/pacts/V3Consumer-V3Provider.json index 98a2302d6..a10021477 100644 --- a/examples/v3/pacts/V3Consumer-V3Provider.json +++ b/examples/v3/pacts/V3Consumer-V3Provider.json @@ -9,15 +9,15 @@ { "name": "User foo exists", "params": { - "some": "param" + "id": "foo" } } ], "request": { "body": { - "datetime": "2020-01-01'T'08:00:45", + "datetime": "2020-01-01T08:00:45", "id": 27, - "lastName": "sampson", + "lastName": "billy", "name": "billy" }, "generators": { @@ -25,6 +25,10 @@ "$.datetime": { "format": "yyyy-MM-dd'T'HH:mm:ss", "type": "DateTime" + }, + "$.name": { + "expression": "${name}", + "type": "ProviderState" } } }, @@ -34,15 +38,16 @@ }, "matchingRules": { "body": { - "$.id": { + "$.datetime": { "combine": "AND", "matchers": [ { - "match": "integer" + "match": "timestamp", + "timestamp": "yyyy-MM-dd'T'HH:mm:ss" } ] }, - "$.lastName": { + "$.id": { "combine": "AND", "matchers": [ { @@ -50,25 +55,25 @@ } ] }, - "$.name": { + "$.lastName": { "combine": "AND", "matchers": [ { "match": "type" } ] - } - }, - "headers": { - "$.Authorization": { + }, + "$.name": { "combine": "AND", "matchers": [ { "match": "type" } ] - }, - "$.Content-Type": { + } + }, + "header": { + "Authorization": { "combine": "AND", "matchers": [ { @@ -87,7 +92,7 @@ ] }, "query": { - "$.baz[0]": { + "baz[0]": { "combine": "AND", "matchers": [ { @@ -96,7 +101,7 @@ } ] }, - "$.baz[1]": { + "baz[1]": { "combine": "AND", "matchers": [ { @@ -105,7 +110,7 @@ } ] }, - "$.baz[2]": { + "baz[2]": { "combine": "AND", "matchers": [ { @@ -129,6 +134,13 @@ "response": { "body": { "accountBalance": 123.76, + "arrayContaining": [ + "string", + 1, + { + "foo": "bar" + } + ], "dateTime": "2020-01-01", "equality": "a thing", "id": 12, @@ -144,8 +156,8 @@ 27, 27 ], - "lastName": "LastName", - "name": "FirstName", + "lastName": "Sampson", + "name": "Billy", "superstring": "foo" }, "headers": { @@ -161,6 +173,55 @@ } ] }, + "$.arrayContaining": { + "combine": "AND", + "matchers": [ + { + "match": "arrayContains", + "variants": [ + { + "index": 0, + "rules": { + "$": { + "combine": "AND", + "matchers": [ + { + "match": "type" + } + ] + } + } + }, + { + "index": 1, + "rules": { + "$": { + "combine": "AND", + "matchers": [ + { + "match": "integer" + } + ] + } + } + }, + { + "index": 2, + "rules": { + "$.foo": { + "combine": "AND", + "matchers": [ + { + "match": "type" + } + ] + } + } + } + ] + } + ] + }, "$.dateTime": { "combine": "AND", "matchers": [ @@ -190,7 +251,8 @@ "combine": "AND", "matchers": [ { - "match": "type" + "match": "type", + "min": 3 } ] }, @@ -204,22 +266,6 @@ } ] }, - "$.lastName": { - "combine": "AND", - "matchers": [ - { - "match": "type" - } - ] - }, - "$.name": { - "combine": "AND", - "matchers": [ - { - "match": "type" - } - ] - }, "$.superstring": { "combine": "AND", "matchers": [ @@ -229,16 +275,6 @@ } ] } - }, - "headers": { - "$.Content-Type": { - "combine": "AND", - "matchers": [ - { - "match": "type" - } - ] - } } }, "status": 200 @@ -246,11 +282,8 @@ } ], "metadata": { - "pactGo": { - "version": "v1.4.3" - }, "pactRust": { - "version": "0.8.6" + "version": "0.9.2" }, "pactSpecification": { "version": "3.0.0" diff --git a/examples/v3/pacts/V3MessageConsumer-V3MessageProvider.json b/examples/v3/pacts/V3MessageConsumer-V3MessageProvider.json index 24ddf3013..cc6c4dca2 100644 --- a/examples/v3/pacts/V3MessageConsumer-V3MessageProvider.json +++ b/examples/v3/pacts/V3MessageConsumer-V3MessageProvider.json @@ -1 +1,60 @@ -{"consumer":{"name":"V3MessageConsumer"},"provider":{"name":"V3MessageProvider"},"messages":[{"contents":{"datetime":"2020-01-01","id":12,"lastName":"LastName","name":"FirstName"},"providerStates":[{"name":"User with id 127 exists","params":{"id":127}}],"metadata":{"Content-Type":"application/json; charset=utf-8"},"description":"a user event","matchingRules":{"body":{"$.datetime":{"combine":"AND","matchers":[{"match":"regex","regex":"[0-9\\-]+"}]},"$.id":{"combine":"AND","matchers":[{"match":"integer"}]},"$.lastName":{"combine":"AND","matchers":[{"match":"type"}]},"$.name":{"combine":"AND","matchers":[{"match":"type"}]}},"path":{}}}],"metadata":{"pactGo":{"version":"v1.4.3"},"pactSpecification":{"version":"3.0.0"}}} \ No newline at end of file +{ + "consumer": { + "name": "V3MessageConsumer" + }, + "messages": [ + { + "contents": { + "datetime": "2020-01-01", + "id": 12, + "lastName": "Sampson", + "name": "Billy" + }, + "description": "a user event", + "matchingRules": { + "body": { + "$.datetime": { + "combine": "AND", + "matchers": [ + { + "match": "regex", + "regex": "[0-9\\-]+" + } + ] + }, + "$.id": { + "combine": "AND", + "matchers": [ + { + "match": "integer" + } + ] + } + } + }, + "metadata": { + "Content-Type": "application/json", + "contentType": "application/json" + }, + "providerStates": [ + { + "name": "User with id 127 exists", + "params": { + "id": 127 + } + } + ] + } + ], + "metadata": { + "pactRust": { + "version": "0.9.2" + }, + "pactSpecification": { + "version": "3.0.0" + } + }, + "provider": { + "name": "V3MessageProvider" + } +} \ No newline at end of file diff --git a/examples/v3/provider_test.go b/examples/v3/provider_test.go index a1943d483..c279bb805 100644 --- a/examples/v3/provider_test.go +++ b/examples/v3/provider_test.go @@ -45,8 +45,11 @@ func TestV3HTTPProvider(t *testing.T) { // Verify the Provider with local Pact Files err := verifier.VerifyProvider(t, v3.VerifyRequest{ ProviderBaseURL: "http://localhost:8111", - PactFiles: []string{filepath.ToSlash(fmt.Sprintf("%s/V3Consumer-V3Provider.json", pactDir))}, - RequestFilter: f, + PactFiles: []string{ + filepath.ToSlash(fmt.Sprintf("%s/V3Consumer-V3Provider.json", pactDir)), + filepath.ToSlash(fmt.Sprintf("%s/V2ConsumerMatch-V2ProviderMatch.json", pactDir)), + }, + RequestFilter: f, BeforeEach: func() error { log.Println("[DEBUG] HOOK before each") return nil @@ -99,9 +102,9 @@ func TestV3MessageProvider(t *testing.T) { if setup { user = &User{ ID: 127, - Name: "Baz", + Name: "Billy", Date: "2020-01-01", - LastName: "sampson", + LastName: "Sampson", } } @@ -139,11 +142,19 @@ func startServer() { 27, 27, 27, + 27, 27 ], - "lastName": "LastName", - "name": "FirstName", - "superstring": "foo" + "lastName": "Sampson", + "name": "Billy", + "superstring": "foo", + "arrayContaining": [ + "string", + 1, + { + "foo": "bar" + } + ] }`, ) }) @@ -154,7 +165,7 @@ func startServer() { type User struct { ID int `json:"id" pact:"example=27"` Name string `json:"name" pact:"example=billy"` - LastName string `json:"lastName" pact:"example=sampson"` + LastName string `json:"lastName" pact:"example=Sampson"` Date string `json:"datetime" pact:"example=2020-01-01'T'08:00:45,format=yyyy-MM-dd'T'HH:mm:ss,generator=datetime"` // Date string `json:"datetime" pact:"example=20200101,regex=[0-9a-z-A-Z]+"` } diff --git a/v3/interaction.go b/v3/interaction.go index 80fb8c695..72d360250 100644 --- a/v3/interaction.go +++ b/v3/interaction.go @@ -41,11 +41,6 @@ func (i *Interaction) WithRequest(method Method, path Matcher) *InteractionReque } } -func (i *InteractionRequest) Method(method Method) *InteractionRequest { - - return i -} - func (i *InteractionRequest) Query(query QueryMatcher) *InteractionRequest { q := make(map[string][]interface{}) for k, values := range query { @@ -58,12 +53,14 @@ func (i *InteractionRequest) Query(query QueryMatcher) *InteractionRequest { return i } -func (i *InteractionRequest) Headers(headers HeadersMatcher) *InteractionRequest { - h := make(map[string]interface{}) - for k, v := range headers { - h[k] = v - } - i.interaction.WithRequestHeaders(h) +func (i *InteractionRequest) HeadersArray(headers HeadersMatcher) *InteractionRequest { + i.interaction.WithRequestHeaders(headersMatcherToNativeHeaders(headers)) + + return i +} + +func (i *InteractionRequest) Headers(headers MapMatcher) *InteractionRequest { + i.interaction.WithRequestHeaders(mapMatcherToNativeHeaders(headers)) return i } @@ -106,6 +103,12 @@ func (i *InteractionRequest) Body(contentType string, body []byte) *InteractionR return i } +func (i *InteractionRequest) BodyMatch(body interface{}) *InteractionRequest { + i.interaction.WithJSONRequestBody(MatchV2(body)) + + return i +} + // WillRespondWith specifies the details of the HTTP response that will be used to // confirm that the Provider must satisfy. Mandatory. // Defaults to application/json. @@ -118,21 +121,40 @@ func (i *InteractionRequest) WillRespondWith(status int) *InteractionResponse { } } +func (i *InteractionResponse) HeadersArray(headers HeadersMatcher) *InteractionResponse { + i.interaction.WithResponseHeaders(headersMatcherToNativeHeaders(headers)) + + return i +} + func (i *InteractionResponse) Headers(headers MapMatcher) *InteractionResponse { - h := make(map[string]interface{}) + i.interaction.WithRequestHeaders(mapMatcherToNativeHeaders(headers)) + + return i +} + +func headersMatcherToNativeHeaders(headers HeadersMatcher) map[string][]interface{} { + h := make(map[string][]interface{}) + for k, v := range headers { - h[k] = v.(stringLike).string() + h[k] = make([]interface{}, len(v)) + for i, vv := range v { + h[k][i] = vv + } } - i.interaction.WithResponseHeaders(h) - return i + return h } -// func (i *InteractionResponse) Status(status int) *InteractionResponse { -// i.interaction.WithStatus(status) +func mapMatcherToNativeHeaders(headers MapMatcher) map[string][]interface{} { + h := make(map[string][]interface{}) -// return i -// } + for k, v := range headers { + h[k] = []interface{}{v} + } + + return h +} func (i *InteractionResponse) JSON(body interface{}) *InteractionResponse { i.interaction.WithJSONResponseBody(body) @@ -152,6 +174,12 @@ func (i *InteractionResponse) Body(contentType string, body []byte) *Interaction return i } +func (i *InteractionResponse) BodyMatch(body interface{}) *InteractionResponse { + i.interaction.WithJSONRequestBody(MatchV2(body)) + + return i +} + // TODO: allow these old interfaces? // // func (i *InteractionResponse) WillRespondWithContent(contentType string, response Response) *InteractionResponse { diff --git a/v3/internal/native/mockserver/mock_server.go b/v3/internal/native/mockserver/mock_server.go index e960ea584..f83f11387 100644 --- a/v3/internal/native/mockserver/mock_server.go +++ b/v3/internal/native/mockserver/mock_server.go @@ -501,37 +501,35 @@ func (i *Interaction) WithRequest(method string, pathOrMatcher interface{}) *Int return i } -func (i *Interaction) WithRequestHeaders(valueOrMatcher map[string]interface{}) *Interaction { - // func (i *Interaction) WithRequestHeaders(valueOrMatcher map[string]string) *Interaction { +func (i *Interaction) WithRequestHeaders(valueOrMatcher map[string][]interface{}) *Interaction { return i.withHeaders(INTERACTION_PART_REQUEST, valueOrMatcher) } -func (i *Interaction) WithResponseHeaders(valueOrMatcher map[string]interface{}) *Interaction { - // func (i *Interaction) WithResponseHeaders(valueOrMatcher map[string]string) *Interaction { +func (i *Interaction) WithResponseHeaders(valueOrMatcher map[string][]interface{}) *Interaction { return i.withHeaders(INTERACTION_PART_RESPONSE, valueOrMatcher) } -func (i *Interaction) withHeaders(part interactionType, valueOrMatcher map[string]interface{}) *Interaction { - // func (i *Interaction) withHeaders(part interactionType, valueOrMatcher map[string]string) *Interaction { +func (i *Interaction) withHeaders(part interactionType, valueOrMatcher map[string][]interface{}) *Interaction { for k, v := range valueOrMatcher { cName := C.CString(k) defer free(cName) - value := stringFromInterface(v) - fmt.Printf("withheaders, sending: %+v \n\n", value) - cValue := C.CString(value) - defer free(cValue) + for _, header := range v { + value := stringFromInterface(header) + cValue := C.CString(value) + defer free(cValue) + + // TODO: the index here only applies to headers with multiple values + C.with_header(i.handle, C.int(part), cName, C.int(0), cValue) + } - // TODO: the index here only applies to headers with multiple values - C.with_header(i.handle, C.int(part), cName, C.int(0), cValue) } return i } func (i *Interaction) WithQuery(valueOrMatcher map[string][]interface{}) *Interaction { - // func (i *Interaction) WithQuery(valueOrMatcher map[string][]string) *Interaction { for k, values := range valueOrMatcher { cName := C.CString(k) diff --git a/v3/matcher.go b/v3/matcher.go index 1990119b4..e78f56027 100644 --- a/v3/matcher.go +++ b/v3/matcher.go @@ -279,11 +279,7 @@ func (m *MapMatcher) UnmarshalJSON(bytes []byte) (err error) { return } -// MapMatcher allows a map[string]string-like object -// to also contain complex matchers -// TODO: bring back this type? -// type MapMatcher map[string]Matcher -type HeadersMatcher = MapMatcher +type HeadersMatcher = map[string][]Matcher type MetadataMatcher = MapMatcher // QueryMatcher matches a query string interface diff --git a/v3/request.go b/v3/request.go index eb5a363fe..63d69739b 100644 --- a/v3/request.go +++ b/v3/request.go @@ -2,9 +2,11 @@ package v3 // Request is the default implementation of the Request interface. type Request struct { - Method string `json:"method"` + Method Method `json:"method"` Path Matcher `json:"path"` Query QueryMatcher `json:"query,omitempty"` Headers HeadersMatcher `json:"headers,omitempty"` Body interface{} `json:"body,omitempty"` } + +type Method string