diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index dcb85ccb2..7217f6247 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -19,3 +19,45 @@ npm i -g cz-conventional-changelog ``` `git cz` to commit and commitizen will guide you. + +## Developing + +For full integration testing locally, Ruby 2.1.5 must be installed. Under the +hood, Pact Go bundles the +[Pact Mock Service](https://github.com/bethesque/pact-mock_service) and +[Pact Provider Verifier](https://github.com/pact-foundation/pact-provider-verifier) +projects to implement up to v2.0 of the Pact Specification. This is only +temporary, until [Pact Reference](https://github.com/pact-foundation/pact-reference/) +work is completed. + +* Git clone https://github.com/pact-foundation/pact-go.git +* Run `make dev` to build the package and setup the Ruby 'binaries' locally + +### Vendoring + +We use [Govend](https://github.com/govend/govend) to vendor packages. Please ensure +any new packages are added to `vendor.yml` prior to patching. + +## Integration Tests + +Before releasing a new version, in addition to the standard (isolated) tests +we smoke test the key features against a running Daemon and Broker. + +1. Start daemon: + +``` +go build . +./pact-go daemon +``` + +2. Start a broker + +See [Pact Broker](https://github.com/bethesque/pact_broker#usage) for details. +Make sure you have basic auth setup so we can test authentication. + +3. Run the integrated tests + +``` +cd dsl +PACT_INTEGRATED_TESTS=1 PACT_BROKER_USERNAME="pactuser" PACT_BROKER_PASSWORD="pactpassword" PACT_BROKER_HOST="http://pactbroker" go test -run TestPact_Integration +``` diff --git a/README.md b/README.md index 63be89744..db833f469 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,27 @@ how to get going. [![Go Report Card](https://goreportcard.com/badge/github.com/pact-foundation/pact-go)](https://goreportcard.com/report/github.com/pact-foundation/pact-go) [![GoDoc](https://godoc.org/github.com/pact-foundation/pact-go?status.svg)](https://godoc.org/github.com/pact-foundation/pact-go) +## Table of Contents + + +1. [Table of Contents](#table-of-contents) +2. [Installation](#installation) +3. [Running](#running) + 1. [Consumer](#consumer) + 1. [Matching (Consumer Tests)](#matching-consumer-tests) + 2. [Provider](#provider) + 3. [Publishing Pacts to a Broker and Tagging Pacts](#publishing-pacts-to-a-broker-and-tagging-pacts) + 1. [Publishing from Go code](#publishing-from-go-code) + 2. [Publishing from the CLI](#publishing-from-the-cli) + 4. [Using the Pact Broker with Basic authentication](#using-the-pact-broker-with-basic-authentication) + 5. [Output Logging](#output-logging) +4. [Contact](#contact) +5. [Documentation](#documentation) +6. [Roadmap](#roadmap) +7. [Contributing](#contributing) + + + ## Installation * Download a [release](https://github.com/pact-foundation/pact-go/releases) for your OS. @@ -45,7 +66,7 @@ DSLs communicate over a local (RPC) connection, and is transparent to clients. *NOTE: The daemon is completely thread safe and it is normal to leave the daemon running for long periods (e.g. on a CI server).* -### Example - Consumer +### Consumer 1. Start the daemon with `./pact-go daemon`. 1. `cd /examples`. 1. `go run consumer.go`. @@ -94,7 +115,79 @@ func TestSomeApi(t *testing.T) { } ``` -### Example - Provider + +#### Matching (Consumer Tests) + +In addition to verbatim value matching, you have 3 useful matching functions +in the `dsl` package that can increase expressiveness and reduce brittle test +cases. + +* `dsl.Term(example, matcher)` - tells Pact that the value should match using +a given regular expression, using `example` in mock responses. `example` must be +a string. +* `dsl.Like(content)` - tells Pact that the value itself is not important, as long +as the element _type_ (valid JSON number, string, object etc.) itself matches. +* `dsl.EachLike(content, min)` - tells Pact that the value should be an array type, +consisting of elements like those passed in. `min` must be >= 1. `content` may +be a valid JSON value: e.g. strings, numbers and objects. + +*Example:* + +Here is a complex example that shows how all 3 terms can be used together: + +```go +jumper := Like(`"jumper"`) +shirt := Like(`"shirt"`) +tag := EachLike(fmt.Sprintf(`[%s, %s]`, jumper, shirt), 2) +size := Like(10) +colour := Term("red", "red|green|blue") + +match := formatJSON( + EachLike( + EachLike( + fmt.Sprintf( + `{ + "size": %s, + "colour": %s, + "tag": %s + }`, size, colour, tag), + 1), + 1)) +``` + +This example will result in a response body from the mock server that looks like: +```json +[ + [ + { + "size": 10, + "colour": "red", + "tag": [ + [ + "jumper", + "shirt" + ], + [ + "jumper", + "shirt" + ] + ] + } + ] +] +``` + +See the [matcher tests](https://github.com/pact-foundation/pact-go/blob/master/dsl/matcher_test.go) +for more matching examples. + +*NOTE*: One caveat to note, is that you will need to use valid Ruby +[regular expressions](http://ruby-doc.org/core-2.1.5/Regexp.html) and double +escape backslashes. + +Read more about [flexible matching](https://github.com/realestate-com-au/pact/wiki/Regular-expressions-and-type-matching-with-Pact). + + +### Provider 1. Start your Provider API: @@ -155,24 +248,17 @@ Note that `PactURLs` is a list of local pact files or remote based urls (e.g. from a [Pact Broker](http://docs.pact.io/documentation/sharings_pacts.html)). -#### Using the Pact Broker with Basic authentication - -The following flags are required to use basic authentication when -retrieving Pact files from a Pact Broker: - -* `BrokerUsername` - the username for Pact Broker basic authentication. -* `BrokerPassword` - the password for Pact Broker basic authentication. See the `Skip()'ed` [integration tests](https://github.com/pact-foundation/pact-go/blob/master/dsl/pact_test.go) for a more complete E2E example. -### Publishing Pacts to a Broker and Tag Pacts +### Publishing Pacts to a Broker and Tagging Pacts See the [Pact Broker](http://docs.pact.io/documentation/sharings_pacts.html) documentation for more details on the Broker and this [article](http://rea.tech/enter-the-pact-matrix-or-how-to-decouple-the-release-cycles-of-your-microservices/) on how to make it work for you. -#### Publish from Go code +#### Publishing from Go code ```go pact.PublishPacts(&types.PublishRequest{ @@ -185,7 +271,7 @@ pact.PublishPacts(&types.PublishRequest{ }) ``` -#### Publish from CLI +#### Publishing from the CLI Use a cURL request like the following to PUT the pact to the right location, specifying your consumer name, provider name and consumer version. @@ -196,75 +282,13 @@ curl -v -XPUT \-H "Content-Type: application/json" \ http://your-pact-broker/pacts/provider/A%20Provider/consumer/A%20Consumer/version/1.0.0 ``` -### Matching (Consumer Tests) - -In addition to verbatim value matching, you have 3 useful matching functions -in the `dsl` package that can increase expressiveness and reduce brittle test -cases. - -* `dsl.Term(example, matcher)` - tells Pact that the value should match using -a given regular expression, using `example` in mock responses. `example` must be -a string. -* `dsl.Like(content)` - tells Pact that the value itself is not important, as long -as the element _type_ (valid JSON number, string, object etc.) itself matches. -* `dsl.EachLike(content, min)` - tells Pact that the value should be an array type, -consisting of elements like those passed in. `min` must be >= 1. `content` may -be a valid JSON value: e.g. strings, numbers and objects. - -*Example:* - -Here is a complex example that shows how all 3 terms can be used together: - -```go -jumper := Like(`"jumper"`) -shirt := Like(`"shirt"`) -tag := EachLike(fmt.Sprintf(`[%s, %s]`, jumper, shirt), 2) -size := Like(10) -colour := Term("red", "red|green|blue") - -match := formatJSON( - EachLike( - EachLike( - fmt.Sprintf( - `{ - "size": %s, - "colour": %s, - "tag": %s - }`, size, colour, tag), - 1), - 1)) -``` +### Using the Pact Broker with Basic authentication -This example will result in a response body from the mock server that looks like: -```json -[ - [ - { - "size": 10, - "colour": "red", - "tag": [ - [ - "jumper", - "shirt" - ], - [ - "jumper", - "shirt" - ] - ] - } - ] -] -``` - -See the [matcher tests](https://github.com/pact-foundation/pact-go/blob/master/dsl/matcher_test.go) -for more matching examples. - -*NOTE*: One caveat to note, is that you will need to use valid Ruby -[regular expressions](http://ruby-doc.org/core-2.1.5/Regexp.html) and double -escape backslashes. +The following flags are required to use basic authentication when +publishing or retrieving Pact files to/from a Pact Broker: -Read more about [flexible matching](https://github.com/realestate-com-au/pact/wiki/Regular-expressions-and-type-matching-with-Pact). +* `BrokerUsername` - the username for Pact Broker basic authentication. +* `BrokerPassword` - the password for Pact Broker basic authentication. ### Output Logging @@ -283,29 +307,12 @@ pact := &Pact{ * Twitter: [@pact_up](https://twitter.com/pact_up) * Google users group: https://groups.google.com/forum/#!forum/pact-support +* Gitter: https://gitter.im/realestate-com-au/pact ## Documentation Additional documentation can be found at the main [Pact website](http://pact.io) and in the [Pact Wiki](https://github.com/realestate-com-au/pact/wiki). -## Developing - -For full integration testing locally, Ruby 2.1.5 must be installed. Under the -hood, Pact Go bundles the -[Pact Mock Service](https://github.com/bethesque/pact-mock_service) and -[Pact Provider Verifier](https://github.com/pact-foundation/pact-provider-verifier) -projects to implement up to v2.0 of the Pact Specification. This is only -temporary, until [Pact Reference](https://github.com/pact-foundation/pact-reference/) -work is completed. - -* Git clone https://github.com/pact-foundation/pact-go.git -* Run `make dev` to build the package and setup the Ruby 'binaries' locally - -### Vendoring - -We use [Govend](https://github.com/govend/govend) to vendor packages. Please ensure -any new packages are added to `vendor.yml` prior to patching. - ## Roadmap The [roadmap](docs.pact.io/roadmap/) for Pact and Pact Go is outlined on our main website. diff --git a/dsl/pact_test.go b/dsl/pact_test.go index dc9589ba6..c9ab2b721 100644 --- a/dsl/pact_test.go +++ b/dsl/pact_test.go @@ -213,97 +213,134 @@ func TestPact_AddInteraction(t *testing.T) { } func TestPact_Integration(t *testing.T) { - t.Skip() // Enable when running E2E/integration tests before a release - - // Setup Provider API for verification (later...) - providerPort := setupProviderAPI() - pactDaemonPort := 6666 - - // Create Pact connecting to local Daemon - pact := &Pact{ - Port: pactDaemonPort, - Consumer: "My Consumer", - Provider: "My Provider", - LogLevel: "DEBUG", - } - defer pact.Teardown() - - // Pass in test case - var test = func() error { - _, err := http.Get(fmt.Sprintf("http://localhost:%d/foobar", pact.Server.Port)) - if err != nil { - t.Fatalf("Error sending request: %v", err) + if os.Getenv("PACT_INTEGRATED_TESTS") != "" { + // Enable when running E2E/integration tests before a release + + // Setup Provider API for verification (later...) + providerPort := setupProviderAPI() + pactDaemonPort := 6666 + + // Create Pact connecting to local Daemon + pact := &Pact{ + Port: pactDaemonPort, + Consumer: "billy", + Provider: "bobby", + LogLevel: "DEBUG", } - _, err = http.Get(fmt.Sprintf("http://localhost:%d/bazbat", pact.Server.Port)) - if err != nil { - t.Fatalf("Error sending request: %v", err) + defer pact.Teardown() + + // Pass in test case + var test = func() error { + _, err := http.Get(fmt.Sprintf("http://localhost:%d/foobar", pact.Server.Port)) + if err != nil { + t.Fatalf("Error sending request: %v", err) + } + _, err = http.Get(fmt.Sprintf("http://localhost:%d/bazbat", pact.Server.Port)) + if err != nil { + t.Fatalf("Error sending request: %v", err) + } + + return err } - return err - } - - // Setup a complex interaction - jumper := Like(`"jumper"`) - shirt := Like(`"shirt"`) - tag := EachLike(fmt.Sprintf(`[%s, %s]`, jumper, shirt), 2) - size := Like(10) - colour := Term("red", "red|green|blue") + // Setup a complex interaction + jumper := Like(`"jumper"`) + shirt := Like(`"shirt"`) + tag := EachLike(fmt.Sprintf(`[%s, %s]`, jumper, shirt), 2) + size := Like(10) + colour := Term("red", "red|green|blue") - body := - formatJSON( - EachLike( + body := + formatJSON( EachLike( - fmt.Sprintf( - `{ + EachLike( + fmt.Sprintf( + `{ "size": %s, "colour": %s, "tag": %s }`, size, colour, tag), - 1), - 1)) + 1), + 1)) + + // Set up our interactions. Note we have multiple in this test case! + pact. + AddInteraction(). + Given("Some state"). + UponReceiving("Some name for the test"). + WithRequest(&Request{ + Method: "GET", + Path: "/foobar", + }). + WillRespondWith(&Response{ + Status: 200, + Headers: map[string]string{ + "Content-Type": "application/json", + }, + }) + pact. + AddInteraction(). + Given("Some state2"). + UponReceiving("Some name for the test"). + WithRequest(&Request{ + Method: "GET", + Path: "/bazbat", + }). + WillRespondWith(&Response{ + Status: 200, + Body: body, + }) + + // Verify Collaboration Test interactionns (Consumer sid) + err := pact.Verify(test) + if err != nil { + t.Fatalf("Error on Verify: %v", err) + } - // Set up our interactions. Note we have multiple in this test case! - pact. - AddInteraction(). - Given("Some state"). - UponReceiving("Some name for the test"). - WithRequest(&Request{ - Method: "GET", - Path: "/foobar", - }). - WillRespondWith(&Response{ - Status: 200, - Headers: map[string]string{ - "Content-Type": "application/json", - }, + // Publish the Pacts... + p := &Publisher{} + brokerHost := os.Getenv("PACT_BROKER_HOST") + err = p.Publish(&types.PublishRequest{ + PactURLs: []string{"../pacts/billy-bobby.json"}, + PactBroker: brokerHost, + ConsumerVersion: "1.0.0", + Tags: []string{"latest", "foobar", "sit4"}, + BrokerUsername: os.Getenv("PACT_BROKER_USERNAME"), + PactBrokerPassword: os.Getenv("PACT_BROKER_PASSWORD"), }) - pact. - AddInteraction(). - Given("Some state2"). - UponReceiving("Some name for the test"). - WithRequest(&Request{ - Method: "GET", - Path: "/bazbat", - }). - WillRespondWith(&Response{ - Status: 200, - Body: body, + + if err != nil { + t.Fatalf("Error: %v", err) + } + + // Verify the Provider - local Pact Files + response := pact.VerifyProvider(&types.VerifyRequest{ + ProviderBaseURL: fmt.Sprintf("http://localhost:%d", providerPort), + PactURLs: []string{"./pacts/billy-bobby.json"}, + ProviderStatesURL: fmt.Sprintf("http://localhost:%d/states", providerPort), + ProviderStatesSetupURL: fmt.Sprintf("http://localhost:%d/setup", providerPort), }) + fmt.Println(response.Message) - // Verify - err := pact.Verify(test) - if err != nil { - t.Fatalf("Error on Verify: %v", err) - } + if response.ExitCode != 0 { + t.Fatalf("Expected exit code of 0, got %d", response.ExitCode) + } - response := pact.VerifyProvider(&types.VerifyRequest{ - ProviderBaseURL: fmt.Sprintf("http://localhost:%d", providerPort), - PactURLs: []string{"./pacts/my_consumer-my_provider.json"}, - ProviderStatesURL: fmt.Sprintf("http://localhost:%d/states", providerPort), - ProviderStatesSetupURL: fmt.Sprintf("http://localhost:%d/setup", providerPort), - }) + // Verify the Provider - Published Pacts + response = pact.VerifyProvider(&types.VerifyRequest{ + ProviderBaseURL: fmt.Sprintf("http://localhost:%d", providerPort), + PactURLs: []string{fmt.Sprintf("%s/pacts/provider/bobby/consumer/billy/latest/sit4", brokerHost)}, + ProviderStatesURL: fmt.Sprintf("http://localhost:%d/states", providerPort), + ProviderStatesSetupURL: fmt.Sprintf("http://localhost:%d/setup", providerPort), + BrokerUsername: os.Getenv("PACT_BROKER_USERNAME"), + BrokerPassword: os.Getenv("PACT_BROKER_PASSWORD"), + }) + fmt.Println(response.Message) - fmt.Println(response.Message) + if response.ExitCode != 0 { + t.Fatalf("Expected exit code of 0, got %d", response.ExitCode) + } + } } // Used as the Provider in the verification E2E steps @@ -316,7 +353,7 @@ func setupProviderAPI() int { }) mux.HandleFunc("/states", func(w http.ResponseWriter, req *http.Request) { log.Println("[DEBUG] provider API: states") - fmt.Fprintf(w, `{"My Consumer": ["Some state", "Some state2"]}`) + fmt.Fprintf(w, `{"billy": ["Some state", "Some state2"]}`) w.Header().Add("Content-Type", "application/json") }) mux.HandleFunc("/foobar", func(w http.ResponseWriter, req *http.Request) { diff --git a/dsl/publish.go b/dsl/publish.go index 83139e0bd..142570449 100644 --- a/dsl/publish.go +++ b/dsl/publish.go @@ -62,8 +62,8 @@ func (p *Publisher) validate() error { return errors.New("ConsumerVersion is mandatory") } - if (p.request.PactBrokerUsername != "" && p.request.PactBrokerPassword == "") || (p.request.PactBrokerUsername == "" && p.request.PactBrokerPassword != "") { - return errors.New("Must provide both or none of PactBrokerUsername and PactBrokerPassword") + if (p.request.BrokerUsername != "" && p.request.PactBrokerPassword == "") || (p.request.BrokerUsername == "" && p.request.PactBrokerPassword != "") { + return errors.New("Must provide both or none of BrokerUsername and PactBrokerPassword") } return nil @@ -81,8 +81,8 @@ func (p *Publisher) call(method string, url string, content []byte) error { req.Header.Set("Content-Type", "application/json") - if p.request != nil && p.request.PactBrokerUsername != "" && p.request.PactBrokerPassword != "" { - req.SetBasicAuth(p.request.PactBrokerUsername, p.request.PactBrokerPassword) + if p.request != nil && p.request.BrokerUsername != "" && p.request.PactBrokerPassword != "" { + req.SetBasicAuth(p.request.BrokerUsername, p.request.PactBrokerPassword) } res, err := client.Do(req) diff --git a/dsl/publish_test.go b/dsl/publish_test.go index 5ba957399..414cb93ca 100644 --- a/dsl/publish_test.go +++ b/dsl/publish_test.go @@ -141,13 +141,13 @@ func TestPublish_validate(t *testing.T) { PactURLs: []string{ testFile, }, - ConsumerVersion: "1.0.0", - PactBrokerUsername: "userwithoutpass", + ConsumerVersion: "1.0.0", + BrokerUsername: "userwithoutpass", }, } err = p.validate() - if err.Error() != "Must provide both or none of PactBrokerUsername and PactBrokerPassword" { + if err.Error() != "Must provide both or none of BrokerUsername and PactBrokerPassword" { t.Fatalf("Expected a different error but got '%s'", err.Error()) } @@ -163,7 +163,7 @@ func TestPublish_validate(t *testing.T) { } err = p.validate() - if err.Error() != "Must provide both or none of PactBrokerUsername and PactBrokerPassword" { + if err.Error() != "Must provide both or none of BrokerUsername and PactBrokerPassword" { t.Fatalf("Expected a different error but got '%s'", err.Error()) } @@ -427,7 +427,7 @@ func TestPublish_PublishWithAuth(t *testing.T) { PactURLs: []string{f.Name()}, PactBroker: broker.URL, ConsumerVersion: "1.0.0", - PactBrokerUsername: "foo", + BrokerUsername: "foo", PactBrokerPassword: "bar", }) @@ -447,7 +447,7 @@ func TestPublish_PublishWithAuthFail(t *testing.T) { PactURLs: []string{f.Name()}, PactBroker: broker.URL, ConsumerVersion: "1.0.0", - PactBrokerUsername: "foo", + BrokerUsername: "foo", PactBrokerPassword: "fail", }) @@ -493,18 +493,3 @@ func TestPublish_tagRequestFail(t *testing.T) { t.Fatalf("Expected error but got none") } } - -func TestPublish_EndToEnd(t *testing.T) { - t.Skip() - p := &Publisher{} - err := p.Publish(&types.PublishRequest{ - PactURLs: []string{"../pacts/billy-bobby.json"}, - PactBroker: "http://localhost:8080", - ConsumerVersion: "1.0.0", - Tags: []string{"latest", "foobar", "sit4"}, - }) - - if err != nil { - t.Fatalf("Error: %v", err) - } -} diff --git a/types/publish_request.go b/types/publish_request.go index 511329511..06b98f622 100644 --- a/types/publish_request.go +++ b/types/publish_request.go @@ -9,7 +9,7 @@ type PublishRequest struct { PactBroker string // Username for Pact Broker basic authentication. Optional - PactBrokerUsername string + BrokerUsername string // Password for Pact Broker basic authentication. Optional PactBrokerPassword string