diff --git a/.github/workflows/release_gem.yml b/.github/workflows/release_gem.yml new file mode 100644 index 00000000..fac97a38 --- /dev/null +++ b/.github/workflows/release_gem.yml @@ -0,0 +1,59 @@ +name: Release gem + +on: + repository_dispatch: + types: + - release-triggered + workflow_dispatch: + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-ruby@v1 + with: + ruby-version: '2.6' + - run: | + gem install bundler -v 2.1 + bundle install + - name: Test + run: bundle exec rake + + release: + needs: test + runs-on: ubuntu-latest + outputs: + gem_name: ${{ steps.release-gem.outputs.gem_name }} + version: ${{ steps.release-gem.outputs.version }} + increment: ${{ steps.release-gem.outputs.increment }} + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: 0 + - id: release-gem + uses: pact-foundation/release-gem@v0.0.11 + env: + GEM_HOST_API_KEY: '${{ secrets.RUBYGEMS_API_KEY }}' + GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}' + INCREMENT: '${{ github.event.client_payload.increment }}' + + notify-gem-released: + needs: release + strategy: + matrix: + repository: [pact-foundation/pact-ruby-cli, pact-foundation/pact-ruby-standalone, pact-foundation/pact_broker-client] + runs-on: ubuntu-latest + steps: + - name: Notify ${{ matrix.repository }} of gem release + uses: peter-evans/repository-dispatch@v1 + with: + token: ${{ secrets.GHTOKENFORPACTCLIRELEASE }} + repository: ${{ matrix.repository }} + event-type: gem-released + client-payload: | + { + "name": "${{ needs.release.outputs.gem_name }}", + "version": "${{ needs.release.outputs.version }}", + "increment": "${{ needs.release.outputs.increment }}" + } diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 00000000..0cb529a4 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,20 @@ +name: Test + +on: push + +jobs: + test: + runs-on: "ubuntu-latest" + continue-on-error: ${{ matrix.experimental }} + strategy: + fail-fast: false + matrix: + ruby_version: ["2.2", "2.7", "3.0"] + experimental: [false] + steps: + - uses: actions/checkout@v2 + - uses: ruby/setup-ruby@v1 + with: + ruby-version: ${{ matrix.ruby_version }} + - run: "bundle install" + - run: "bundle exec rake" diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index edc2f435..00000000 --- a/.travis.yml +++ /dev/null @@ -1,19 +0,0 @@ -language: ruby -rvm: -- 2.2.4 -- 2.3.1 -- 2.4 -- jruby-9.0.5.0 -- jruby-9.1.13.0 -gemfile: -- gemfiles/default.gemfile -- gemfiles/rspec_2.gemfile -- gemfiles/rspec_3.0.gemfile -deploy: - provider: rubygems - api_key: - secure: AzTHDbKRr1ZO4E2mRyvU054Tx8c2cZbKkoDBZjSAQ2CY3E7oH137NTAIGd4BthH/E9mbEXtGpZIDfWPbaOcUJQ5Bz24CWTKmGyic6FrPhJnOW5CKVSLGCDPzpmqHULv/GTN16YN0Dh1HLeGYZzlHlxT0+4AVvbvBAleHrAFeJs8= - gem: pact - on: - tags: true - repo: pact-foundation/pact-ruby diff --git a/Appraisals b/Appraisals deleted file mode 100644 index e9289b87..00000000 --- a/Appraisals +++ /dev/null @@ -1,10 +0,0 @@ -appraise "default" do -end - -appraise "rspec-2" do - gem "rspec", "2.14.1" -end - -appraise "rspec-3.0" do - gem "rspec", "3.0.0" -end diff --git a/CHANGELOG.md b/CHANGELOG.md index 6d075072..133c14e9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,401 @@ + +### v1.61.0 (2021-12-16) + +#### Features + +* support description of matching_branch and matching_tag consumer version selectors ([8e8bb22](/../../commit/8e8bb22)) + +#### Bug Fixes + +* pass through includePendingStatus to the 'pacts for verification' API when it is false ([f0e37a4](/../../commit/f0e37a4)) + + +### v1.60.0 (2021-10-01) + +#### Features + +* allow SSL verification to be disabled in the HAL client by setting the environment variable PACT_DISABLE_SSL_VERIFICATION=true ([ce07d32](/../../commit/ce07d32)) + + +### v1.59.0 (2021-09-07) + +#### Features + +* update descriptions for new consumer version selectors ([0471397](/../../commit/0471397)) + + +### v1.58.0 (2021-09-01) + +#### Features + +* support publishing verification results with a version branch ([da2facf](/../../commit/da2facf)) + +#### Bug Fixes + +* gracefully handle display of username that causes InvalidComponentError to be raised when composing a URI ([cecb98f](/../../commit/cecb98f)) + + +### v1.57.0 (2021-01-27) + +#### Features + +* allow verbose flag to be set when publishing verifications ([9238e4c](/../../commit/9238e4c)) + + +### v1.56.0 (2021-01-22) + +#### Features + +* catch and log error during help text generation ([182a7cd](/../../commit/182a7cd)) + + +### v1.55.7 (2020-11-25) + +#### Bug Fixes + +* add consumer name to the selection description (#229) ([5127036](/../../commit/5127036)) + + +### v1.55.6 (2020-11-06) + +#### Bug Fixes + +* require rspec now that pact-support does not depend on it ([5b5c27c](/../../commit/5b5c27c)) + + +### v1.55.5 (2020-10-12) + +#### Bug Fixes + +* **security** + * hide personal access token given in uri (#225) ([f6db12d](/../../commit/f6db12d)) + + +### v1.55.4 (2020-10-09) + +#### Bug Fixes + +* add back missing output describing the interactions filter ([1a2d7c1](/../../commit/1a2d7c1)) + + +### v1.55.3 (2020-09-28) + +#### Bug Fixes + +* correct logic for determining if all interactions for a pact have been verified ([c4f968e](/../../commit/c4f968e)) +* de-duplicate re-run commands ([0813498](/../../commit/0813498)) + + +### v1.55.2 (2020-09-26) + +#### Bug Fixes + +* correctly calculate exit code when a mix of pending and non pending pacts are verified ([533faa1](/../../commit/533faa1)) + + +### v1.55.1 (2020-09-26) + +#### Bug Fixes + +* remove accidentally committed debug logging ([081423e](/../../commit/081423e)) + + +### v1.55.0 (2020-09-26) + +#### Features + +* add consumer_version_selectors to pact verification DSL, and convert consumer_version_tags to selectors ([39e6c4a](/../../commit/39e6c4a)) +* allow verification task to set just a pact_helper without a URI ([303077d](/../../commit/303077d)) +* split pending and failed rerun commands into separate sections ([f839391](/../../commit/f839391)) +* update output during verification so the pact info shows before the describe blocks of the pact that is being verified ([15ec231](/../../commit/15ec231)) + + +### v1.54.0 (2020-09-12) + +#### Features + +* use pb relation in preference to beta relation when fetching pacts for verification ([7563fcf](/../../commit/7563fcf)) +* allow include_wip_pacts_since to use a Date, DateTime or Time ([dd35366](/../../commit/dd35366)) +* add support for include_wip_pacts_since ([f2247b8](/../../commit/f2247b8)) + + +### v1.53.0 (2020-09-11) + +#### Features + +* add support for the enable_pending flag ([16866f4](/../../commit/16866f4)) + + +### v1.52.0 (2020-09-10) + +#### Features + +* support webdav http methods ([fa1d712](/../../commit/fa1d712)) + + +### v1.51.1 (2020-08-12) + +#### Bug Fixes + +* update thor dependency (#218) ([bf3ce69](/../../commit/bf3ce69)) +* bump rake dependency per CVE-2020-8130 (#219) ([09feaa6](/../../commit/09feaa6)) + + +### v1.51.0 (2020-06-24) + + +#### Features + +* allow individual interactions to be re-run by setting PACT_BROKER_INTERACTION_ID ([a586d80](/../../commit/a586d80)) + + + +### v1.50.1 (2020-06-15) + + +#### Bug Fixes + +* fix integration with pact-message-ruby (#216) ([d2da13e](/../../commit/d2da13e)) + + + +### v1.50.0 (2020-04-25) + + +#### Features + +* Set expected interactions on mock service but without writing them to pact file (#210) ([14f5327](/../../commit/14f5327)) + + + +### v1.49.3 (2020-04-22) + + +#### Bug Fixes + +* pact selection verification options logging ([9ff59f4](/../../commit/9ff59f4)) + + + +### v1.49.2 (2020-04-08) + + +#### Bug Fixes + +* json parser error for top level JSON values ([dafbc35](/../../commit/dafbc35)) + + + +### v1.49.1 (2020-03-21) + + +#### Bug Fixes + +* ensure diff is included in the json output ([0bd9753](/../../commit/0bd9753)) +* ensure the presence of basic auth credentials does not cause an error when displaying the path of a pact on the local filesystem ([f6a0b4d](/../../commit/f6a0b4d)) + + + +### v1.49.0 (2020-02-18) + + +#### Features + +* use environment variables PACT_BROKER_USERNAME and PACT_BROKER_PASSWORD when verifying a pact by URL, if the environment variables are present ([308f25d](/../../commit/308f25d)) + + + +### v1.48.0 (2020-02-13) + + +#### Features + +* use certificates from SSL_CERT_FILE and SSL_CERT_DIR environment variables in HTTP connections ([164912b](/../../commit/164912b)) + + + +### v1.47.0 (2020-02-08) + + +#### Features + +* update json formatter output ([376e47a](/../../commit/376e47a)) +* add pact metadata to json formatter ([6c6ddb8](/../../commit/6c6ddb8)) + + + +### v1.46.1 (2020-01-22) + + +#### Bug Fixes + +* send output messages to the correct stream when using the XML formatter ([e768a33](/../../commit/e768a33)) + + + +### v1.46.0 (2020-01-22) + + +#### Features + +* expose full notice object in JSON output ([bdc2711](/../../commit/bdc2711)) + + +#### Bug Fixes + +* remove accidentally committed verbose: true ([498518c](/../../commit/498518c)) + + + +### v1.45.0 (2020-01-21) + + +#### Features + +* use custom json formatter when --format json is specified and send it straight to stdout or the configured file ([6c703a1](/../../commit/6c703a1)) +* support pending pacts in json formatter ([2c0d20d](/../../commit/2c0d20d)) + + +#### Bug Fixes + +* show pending test output in yellow instead of red ([e8d4a55](/../../commit/e8d4a55)) + + + +### v1.44.1 (2020-01-20) + + +#### Bug Fixes + +* print notices from 'pacts for verification' response to indicate why pacts are included an/or pending ([b107348](/../../commit/b107348)) + + + +### v1.44.0 (2020-01-16) + + +#### Features + +* **message pact** + * add DSL for configuring Message Pact verifications ([a5181b6](/../../commit/a5181b6)) + + + +### v1.43.1 (2020-01-11) + + +#### Bug Fixes + +* use configured credentials when fetching the diff with previous version ([b9deb09](/../../commit/b9deb09)) +* use URI.open instead of Kernel.open ([7b3ea81](/../../commit/7b3ea81)) + + + +### v1.43.0 (2020-01-11) + + +#### Features + +* **verify** + * allow includePendingStatus to be specified when fetching pacts ([1f5fc9c](/../../commit/1f5fc9c)) + + + +### v1.42.3 (2019-11-15) + + +#### Bug Fixes + +* **verify** + * exit with status 0 if all pacts are in pending state ([2f7110b](/../../commit/2f7110b)) + + + +### v1.42.2 (2019-11-09) + + +#### Bug Fixes + +* remove missed &. ([be700d8](/../../commit/be700d8)) + + + +### v1.42.1 (2019-11-09) + + +#### Bug Fixes + +* can't use safe navigation operator because of Ruby 2.2 in Travelling Ruby for the pact-ruby-standalone ([3068ceb](/../../commit/3068ceb)) + + + +### v1.42.0 (2019-09-26) + + +#### Features + +* use new 'pacts for verification' endpoint to retrieve pacts (#199) ([55bb935](/../../commit/55bb935)) + + + +### v1.41.2 (2019-09-10) + + +#### Bug Fixes + +* **pact_helper_locator** + * add 'test' dir to file patterns (#196) ([746883d](/../../commit/746883d)) + +* file upload spec ([0fe072c](/../../commit/0fe072c)) + + + +### v1.41.1 (2019-09-04) + + +#### Bug Fixes + +* use to_json instead of JSON.dump because it generates different JSON when used in conjuction with other libraries (eg. Oj) ([14566fb](/../../commit/14566fb)) + + + +### v1.41.0 (2019-05-22) + + +#### Features + +* redact Authorization header from HTTP client debug output ([c48c991](/../../commit/c48c991)) + + + +### v1.40.0 (2019-02-22) + + +#### Features + +* remove ruby 2.2 tests ([4a30791](/../../commit/4a30791)) +* add support for bearer token ([297268d](/../../commit/297268d)) + + + +### v1.39.0 (2019-02-21) + + +#### Features + +* allow host of mock service to be specified ([de267bd](/../../commit/de267bd)) + + + +### v1.38.0 (2019-02-11) + + +#### Features + +* unlock rack-test dependency to allow version 1.1.0 ([b0c40f6](/../../commit/b0c40f6)) +* update http client code ([bba3a08](/../../commit/bba3a08)) + + ### v1.37.0 (2018-11-15) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d4d7e71e..7e805dcd 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -11,7 +11,7 @@ Please provide the following information with your issue to enable us to respond 1. Fork it 2. Create your feature branch (`git checkout -b feat/my-new-feature`) -3. Commit your changes. **Please use the contentional changelog format for [sematic commit messages](http://karma-runner.github.io/1.0/dev/git-commit-msg.html)** (`git commit -am 'feat(some new feat): add a thing'`) +3. Commit your changes. **Please use the conventional changelog format for [semantic commit messages](http://karma-runner.github.io/1.0/dev/git-commit-msg.html)** (`git commit -am 'feat(some new feat): add a thing'`) 4. Push to the branch (`git push origin feat/my-new-feature`) 5. Create new Pull Request diff --git a/Gemfile b/Gemfile index b324a79a..f2371c89 100644 --- a/Gemfile +++ b/Gemfile @@ -7,4 +7,8 @@ if ENV['X_PACT_DEVELOPMENT'] gem "pact-support", path: '../pact-support' gem "pact-mock_service", path: '../pact-mock_service' gem "pry-byebug" -end \ No newline at end of file +end + +group :local_development do + gem "pry-byebug" +end diff --git a/README.md b/README.md index 81169a55..1ec32987 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # Pact -[![travis-ci.org Build Status](https://travis-ci.org/pact-foundation/pact-ruby.svg?branch=master)](https://travis-ci.org/pact-foundation/pact-ruby) +![Test](https://github.com/pact-foundation/pact-ruby/workflows/Test/badge.svg) [![Backers on Open Collective](https://opencollective.com/pact-foundation/backers/badge.svg)](#backers) [![Sponsors on Open Collective](https://opencollective.com/pact-foundation/sponsors/badge.svg)](#sponsors) @@ -85,7 +85,7 @@ We're going to write an integration, with Pact tests, between a consumer, the Zo #### 1. Start with your model -Imagine a model class that looks something like this. The attributes for a Alligator live on a remote server, and will need to be retrieved by an HTTP call to the Animal Service. +Imagine a model class that looks something like this. The attributes for an Alligator live on a remote server, and will need to be retrieved by an HTTP call to the Animal Service. ```ruby class Alligator @@ -131,6 +131,7 @@ Pact.service_consumer "Zoo App" do has_pact_with "Animal Service" do mock_service :animal_service do port 1234 + host "..." # optional, defaults to "localhost" end end end @@ -165,7 +166,7 @@ describe AnimalServiceClient, :pact => true do body: {name: 'Betty'} ) end - it "returns a alligator" do + it "returns an alligator" do expect(subject.get_alligator).to eq(Alligator.new('Betty')) end @@ -305,7 +306,7 @@ Currently, Ruby Pact supports writing Pacts in v2, and verifying Pacts in v3 for ## Links -[Simplifying microservices testing with pacts](http://dius.com.au/2014/05/20/simplifying-micro-service-testing-with-pacts/) - Ron Holshausen (one of the original pact authors) +[Simplifying microservices testing with pacts](http://dius.com.au/2014/05/19/simplifying-micro-service-testing-with-pacts/) - Ron Holshausen (one of the original pact authors) [Pact specification](https://github.com/pact-foundation/pact-specification) diff --git a/ROADMAP.md b/ROADMAP.md index 501154b7..65943c69 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -6,4 +6,4 @@ ## Long term * Add XML support -* Create a test matrix to ensure compatiblity with implementations in other languages +* Create a test matrix to ensure compatibility with implementations in other languages diff --git a/Rakefile b/Rakefile index 78539201..b6d716fd 100644 --- a/Rakefile +++ b/Rakefile @@ -6,5 +6,5 @@ require 'rspec/core/rake_task' Dir.glob('./lib/tasks/**/*.rake').each { |task| load task } Dir.glob('./tasks/**/*.rake').each { |task| load task } -task :default => [:spec, 'spec:provider', 'pact:tests:all'] +task :default => [:spec, :spec_with_active_support, 'spec:provider', 'pact:tests:all'] diff --git a/example/animal-service/Gemfile b/example/animal-service/Gemfile index fd0d85cf..00445e10 100644 --- a/example/animal-service/Gemfile +++ b/example/animal-service/Gemfile @@ -7,8 +7,7 @@ group :development, :test do end gem 'rake' -gem 'rack', '~> 2.0' -gem 'json', '~>1.8' +gem 'rack', '~> 2.1' gem 'sqlite3' gem 'sequel' gem 'sinatra' diff --git a/example/animal-service/Gemfile.lock b/example/animal-service/Gemfile.lock index e0ffc240..37164e54 100644 --- a/example/animal-service/Gemfile.lock +++ b/example/animal-service/Gemfile.lock @@ -1,91 +1,89 @@ PATH remote: ../.. specs: - pact (1.22.2) - json (> 1.8.5) - pact-mock_service (~> 2.0) - pact-support (~> 1.4) - rack-test (~> 0.6, >= 0.6.3) - randexp (~> 0.1.7) - rspec (>= 2.14) + pact (1.53.0) + pact-mock_service (~> 3.0, >= 3.3.1) + pact-support (~> 1.9) + rack-test (>= 0.6.3, < 2.0.0) + rspec (~> 3.0) term-ansicolor (~> 1.0) - thor - webrick + thor (>= 0.20, < 2.0) + webrick (~> 1.3) GEM remote: https://rubygems.org/ specs: awesome_print (1.8.0) - coderay (1.1.2) - diff-lcs (1.3) + coderay (1.1.3) + diff-lcs (1.4.4) filelock (1.1.1) find_a_port (1.0.1) - json (1.8.6) - method_source (0.9.0) - mustermann (1.0.2) - pact-mock_service (2.6.4) + json (2.3.1) + method_source (1.0.0) + mustermann (1.1.1) + ruby2_keywords (~> 0.0.1) + pact-mock_service (3.6.2) filelock (~> 1.1) find_a_port (~> 1.0.1) json - pact-support (~> 1.2, >= 1.2.1) + pact-support (~> 1.12, >= 1.12.0) rack (~> 2.0) rspec (>= 2.14) term-ansicolor (~> 1.0) - thor (~> 0.19) + thor (>= 0.19, < 2.0) webrick (~> 1.3) - pact-support (1.6.0) + pact-support (1.15.1) awesome_print (~> 1.1) - find_a_port (~> 1.0.1) - json randexp (~> 0.1.7) rspec (>= 2.14) term-ansicolor (~> 1.0) - thor - pry (0.11.3) - coderay (~> 1.1.0) - method_source (~> 0.9.0) - rack (2.0.4) - rack-protection (2.0.1) + pry (0.13.1) + coderay (~> 1.1) + method_source (~> 1.0) + rack (2.2.3) + rack-protection (2.1.0) rack - rack-test (0.8.3) + rack-test (1.1.0) rack (>= 1.0, < 3) - rake (12.3.1) + rake (13.0.1) randexp (0.1.7) - rspec (3.7.0) - rspec-core (~> 3.7.0) - rspec-expectations (~> 3.7.0) - rspec-mocks (~> 3.7.0) - rspec-core (3.7.1) - rspec-support (~> 3.7.0) - rspec-expectations (3.7.0) + rspec (3.9.0) + rspec-core (~> 3.9.0) + rspec-expectations (~> 3.9.0) + rspec-mocks (~> 3.9.0) + rspec-core (3.9.2) + rspec-support (~> 3.9.3) + rspec-expectations (3.9.2) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.7.0) - rspec-mocks (3.7.0) + rspec-support (~> 3.9.0) + rspec-mocks (3.9.1) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.7.0) - rspec-support (3.7.1) - sequel (5.7.1) - sinatra (2.0.1) + rspec-support (~> 3.9.0) + rspec-support (3.9.3) + ruby2_keywords (0.0.2) + sequel (5.36.0) + sinatra (2.1.0) mustermann (~> 1.0) - rack (~> 2.0) - rack-protection (= 2.0.1) + rack (~> 2.2) + rack-protection (= 2.1.0) tilt (~> 2.0) - sqlite3 (1.3.13) - term-ansicolor (1.6.0) + sqlite3 (1.4.2) + sync (0.5.0) + term-ansicolor (1.7.1) tins (~> 1.0) - thor (0.20.0) - tilt (2.0.8) - tins (1.16.3) - webrick (1.4.2) + thor (1.0.1) + tilt (2.0.10) + tins (1.25.0) + sync + webrick (1.6.0) PLATFORMS ruby DEPENDENCIES - json (~> 1.8) pact! pry - rack (~> 2.0) + rack (~> 2.1) rake rspec sequel @@ -93,4 +91,4 @@ DEPENDENCIES sqlite3 BUNDLED WITH - 1.15.4 + 2.0.2 diff --git a/example/zoo-app/Gemfile b/example/zoo-app/Gemfile index dfa52213..d35c1500 100644 --- a/example/zoo-app/Gemfile +++ b/example/zoo-app/Gemfile @@ -8,5 +8,5 @@ group :development, :test do end gem 'rake' -gem 'rack', '~>2.0' +gem 'rack', '~>2.1' gem 'httparty' diff --git a/example/zoo-app/Gemfile.lock b/example/zoo-app/Gemfile.lock index 9298ac9d..37143d82 100644 --- a/example/zoo-app/Gemfile.lock +++ b/example/zoo-app/Gemfile.lock @@ -1,82 +1,82 @@ PATH remote: ../.. specs: - pact (1.36.2) - json (> 1.8.5) - pact-mock_service (~> 2.10) - pact-support (~> 1.8) - rack-test (~> 0.6, >= 0.6.3) - randexp (~> 0.1.7) - rspec (>= 2.14) + pact (1.53.0) + pact-mock_service (~> 3.0, >= 3.3.1) + pact-support (~> 1.9) + rack-test (>= 0.6.3, < 2.0.0) + rspec (~> 3.0) term-ansicolor (~> 1.0) - thor - webrick + thor (>= 0.20, < 2.0) + webrick (~> 1.3) GEM remote: https://rubygems.org/ specs: awesome_print (1.8.0) - coderay (1.1.2) - diff-lcs (1.3) + coderay (1.1.3) + diff-lcs (1.4.4) filelock (1.1.1) find_a_port (1.0.1) - httparty (0.16.2) + httparty (0.18.1) + mime-types (~> 3.0) multi_xml (>= 0.5.2) - json (2.1.0) - method_source (0.9.0) + json (2.3.1) + method_source (1.0.0) + mime-types (3.3.1) + mime-types-data (~> 3.2015) + mime-types-data (3.2020.0512) multi_xml (0.6.0) - pact-mock_service (2.12.0) + pact-mock_service (3.6.2) filelock (~> 1.1) find_a_port (~> 1.0.1) json - pact-support (~> 1.2, >= 1.2.1) + pact-support (~> 1.12, >= 1.12.0) rack (~> 2.0) rspec (>= 2.14) term-ansicolor (~> 1.0) - thor (~> 0.19) + thor (>= 0.19, < 2.0) webrick (~> 1.3) - pact-support (1.8.0) + pact-support (1.15.1) awesome_print (~> 1.1) - find_a_port (~> 1.0.1) - json randexp (~> 0.1.7) rspec (>= 2.14) term-ansicolor (~> 1.0) - thor - pact_broker-client (1.16.2) - httparty - json - rake + pact_broker-client (1.29.1) + httparty (~> 0.18) + rake (~> 13.0) table_print (~> 1.5) - term-ansicolor + term-ansicolor (~> 1.7) thor (~> 0.20) - pry (0.11.3) - coderay (~> 1.1.0) - method_source (~> 0.9.0) - rack (2.0.5) - rack-test (0.8.3) + pry (0.13.1) + coderay (~> 1.1) + method_source (~> 1.0) + rack (2.2.3) + rack-test (1.1.0) rack (>= 1.0, < 3) - rake (12.3.1) + rake (13.0.1) randexp (0.1.7) - rspec (3.8.0) - rspec-core (~> 3.8.0) - rspec-expectations (~> 3.8.0) - rspec-mocks (~> 3.8.0) - rspec-core (3.8.0) - rspec-support (~> 3.8.0) - rspec-expectations (3.8.2) + rspec (3.9.0) + rspec-core (~> 3.9.0) + rspec-expectations (~> 3.9.0) + rspec-mocks (~> 3.9.0) + rspec-core (3.9.2) + rspec-support (~> 3.9.3) + rspec-expectations (3.9.2) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.8.0) - rspec-mocks (3.8.0) + rspec-support (~> 3.9.0) + rspec-mocks (3.9.1) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.8.0) - rspec-support (3.8.0) - table_print (1.5.6) - term-ansicolor (1.6.0) + rspec-support (~> 3.9.0) + rspec-support (3.9.3) + sync (0.5.0) + table_print (1.5.7) + term-ansicolor (1.7.1) tins (~> 1.0) - thor (0.20.0) - tins (1.17.0) - webrick (1.4.2) + thor (0.20.3) + tins (1.25.0) + sync + webrick (1.6.0) PLATFORMS ruby @@ -86,9 +86,9 @@ DEPENDENCIES pact! pact_broker-client pry - rack (~> 2.0) + rack (~> 2.1) rake rspec BUNDLED WITH - 1.16.2 + 2.0.2 diff --git a/gemfiles/default.gemfile b/gemfiles/default.gemfile deleted file mode 100644 index 095e6608..00000000 --- a/gemfiles/default.gemfile +++ /dev/null @@ -1,5 +0,0 @@ -# This file was generated by Appraisal - -source "https://rubygems.org" - -gemspec path: "../" diff --git a/gemfiles/rspec_2.gemfile b/gemfiles/rspec_2.gemfile deleted file mode 100644 index 9b17f72a..00000000 --- a/gemfiles/rspec_2.gemfile +++ /dev/null @@ -1,7 +0,0 @@ -# This file was generated by Appraisal - -source "https://rubygems.org" - -gem "rspec", "2.14.1" - -gemspec path: "../" diff --git a/gemfiles/rspec_3.0.gemfile b/gemfiles/rspec_3.0.gemfile deleted file mode 100644 index deb36d2f..00000000 --- a/gemfiles/rspec_3.0.gemfile +++ /dev/null @@ -1,7 +0,0 @@ -# This file was generated by Appraisal - -source "https://rubygems.org" - -gem "rspec", "3.0.0" - -gemspec path: "../" diff --git a/gemfiles/ruby_under_22.gemfile b/gemfiles/ruby_under_22.gemfile deleted file mode 100644 index a22132eb..00000000 --- a/gemfiles/ruby_under_22.gemfile +++ /dev/null @@ -1,9 +0,0 @@ -# This file was generated by Appraisal - -source "https://rubygems.org" - -gem "rack", "< 2.0" -gem "rack-test", "0.6.3" -gem "activesupport", "< 5.0.0" - -gemspec path: "../" diff --git a/gemfiles/ruby_under_22_with_rspec_2.gemfile b/gemfiles/ruby_under_22_with_rspec_2.gemfile deleted file mode 100644 index a21df676..00000000 --- a/gemfiles/ruby_under_22_with_rspec_2.gemfile +++ /dev/null @@ -1,10 +0,0 @@ -# This file was generated by Appraisal - -source "https://rubygems.org" - -gem "rspec", "2.14.1" -gem "rack", "< 2.0" -gem "rack-test", "0.6.3" -gem "activesupport", "< 5.0.0" - -gemspec path: "../" diff --git a/lib/pact/cli.rb b/lib/pact/cli.rb index ba2ee8c5..5541139e 100755 --- a/lib/pact/cli.rb +++ b/lib/pact/cli.rb @@ -4,6 +4,9 @@ module Pact class CLI < Thor + def self.exit_on_failure? # Thor 1.0 deprecation guard + false + end desc 'verify', "Verify a pact" method_option :pact_helper, aliases: "-h", desc: "Pact helper file", :required => true @@ -11,12 +14,16 @@ class CLI < Thor method_option :ignore_failures, type: :boolean, default: false, desc: "Process will always exit with exit code 0", hide: true method_option :pact_broker_username, aliases: "-u", desc: "Pact broker user name" method_option :pact_broker_password, aliases: "-w", desc: "Pact broker password" + method_option :pact_broker_token, aliases: "-k", desc: "Pact broker token" method_option :backtrace, aliases: "-b", desc: "Show full backtrace", :default => false, :type => :boolean + method_option :verbose, aliases: "-v", desc: "Show verbose HTTP logging", :default => false, :type => :boolean method_option :interactions_replay_order, aliases: "-o", desc: "Interactions replay order: randomised or recorded (default)", default: Pact.configuration.interactions_replay_order method_option :description, aliases: "-d", desc: "Interaction description filter" method_option :provider_state, aliases: "-s", desc: "Provider state filter" + method_option :interaction_index, type: :numeric, desc: "Index filter" + method_option :pact_broker_interaction_id, desc: "Pact Broker interaction ID filter" method_option :format, aliases: "-f", banner: "FORMATTER", desc: "RSpec formatter. Defaults to custom Pact formatter. [j]son may also be used." method_option :out, aliases: "-o", banner: "FILE", desc: "Write output to a file instead of $stdout." diff --git a/lib/pact/cli/run_pact_verification.rb b/lib/pact/cli/run_pact_verification.rb index 203f11ef..333120b5 100644 --- a/lib/pact/cli/run_pact_verification.rb +++ b/lib/pact/cli/run_pact_verification.rb @@ -3,7 +3,6 @@ module Pact module Cli class RunPactVerification - attr_reader :options def initialize options @@ -15,6 +14,7 @@ def self.call options end def call + configure_output initialize_rspec setup_load_path load_pact_helper @@ -27,6 +27,7 @@ def initialize_rspec # With RSpec3, if the pact_helper loads a library that adds its own formatter before we set one, # we will get a ProgressFormatter too, and get little dots sprinkled throughout our output. # Load a NilFormatter here to prevent that. + require 'rspec' require 'pact/rspec' ::RSpec.configuration.add_formatter Pact::RSpec.formatter_class.const_get('NilFormatter') end @@ -44,23 +45,32 @@ def load_pact_helper end def run_specs - exit_code = if options[:pact_uri] - run_with_pact_uri + exit_code = if options[:pact_uri].is_a?(String) + run_with_pact_url_string + elsif options[:pact_uri] + run_with_pact_uri_object # from pact-provider-verifier else - run_with_configured_pacts + run_with_configured_pacts_from_pact_helper end exit exit_code end - def run_with_pact_uri + def run_with_pact_url_string pact_repository_uri_options = {} + pact_repository_uri_options[:username] = ENV['PACT_BROKER_USERNAME'] if ENV['PACT_BROKER_USERNAME'] + pact_repository_uri_options[:password] = ENV['PACT_BROKER_PASSWORD'] if ENV['PACT_BROKER_PASSWORD'] + pact_repository_uri_options[:token] = ENV['PACT_BROKER_TOKEN'] pact_repository_uri_options[:username] = options[:pact_broker_username] if options[:pact_broker_username] pact_repository_uri_options[:password] = options[:pact_broker_password] if options[:pact_broker_password] pact_uri = ::Pact::Provider::PactURI.new(options[:pact_uri], pact_repository_uri_options) Pact::Provider::PactSpecRunner.new([pact_uri], pact_spec_options).run end - def run_with_configured_pacts + def run_with_pact_uri_object + Pact::Provider::PactSpecRunner.new([options[:pact_uri]], pact_spec_options).run + end + + def run_with_configured_pacts_from_pact_helper pact_urls = Pact.provider_world.pact_urls raise "Please configure a pact to verify" if pact_urls.empty? Pact::Provider::PactSpecRunner.new(pact_urls, pact_spec_options).run @@ -69,6 +79,7 @@ def run_with_configured_pacts def pact_spec_options { full_backtrace: options[:backtrace], + verbose: options[:verbose] || ENV['VERBOSE'] == 'true', criteria: SpecCriteria.call(options), format: options[:format], out: options[:out], @@ -76,6 +87,14 @@ def pact_spec_options request_customizer: options[:request_customizer] } end + + def configure_output + if options[:format] == 'json' && !options[:out] + # Don't want to mess up the JSON parsing with messages to stdout, so send it to stderr + require 'pact/configuration' + Pact.configuration.output_stream = Pact.configuration.error_stream + end + end end end end diff --git a/lib/pact/cli/spec_criteria.rb b/lib/pact/cli/spec_criteria.rb index f9b84a2e..b2212af6 100644 --- a/lib/pact/cli/spec_criteria.rb +++ b/lib/pact/cli/spec_criteria.rb @@ -6,8 +6,11 @@ def self.call options criteria = {} criteria[:description] = Regexp.new(options[:description]) if options[:description] + criteria[:_id] = options[:pact_broker_interaction_id] if options[:pact_broker_interaction_id] + criteria[:index] = options[:interaction_index] if options[:interaction_index] provider_state = options[:provider_state] + if provider_state if provider_state.length == 0 criteria[:provider_state] = nil #Allow PACT_PROVIDER_STATE="" to mean no provider state diff --git a/lib/pact/consumer/configuration/dsl.rb b/lib/pact/consumer/configuration/dsl.rb index 6a212da0..3017c9f3 100644 --- a/lib/pact/consumer/configuration/dsl.rb +++ b/lib/pact/consumer/configuration/dsl.rb @@ -1,7 +1,6 @@ require 'pact/consumer/configuration/service_consumer' module Pact - module Consumer module DSL def service_consumer name, &block diff --git a/lib/pact/consumer/configuration/mock_service.rb b/lib/pact/consumer/configuration/mock_service.rb index b4b4fd7f..f64d74d7 100644 --- a/lib/pact/consumer/configuration/mock_service.rb +++ b/lib/pact/consumer/configuration/mock_service.rb @@ -11,13 +11,14 @@ class MockService extend Pact::DSL - attr_accessor :port, :standalone, :verify, :provider_name, :consumer_name, :pact_specification_version + attr_accessor :port, :host, :standalone, :verify, :provider_name, :consumer_name, :pact_specification_version def initialize name, consumer_name, provider_name @name = name @consumer_name = consumer_name @provider_name = provider_name @port = nil + @host = "localhost" @standalone = false @verify = true @pact_specification_version = '2' @@ -29,6 +30,10 @@ def port port self.port = port end + def host host + self.host = host + end + def standalone standalone self.standalone = standalone end @@ -52,7 +57,7 @@ def finalize private def register_mock_service - url = "http://localhost#{port.nil? ? '' : ":#{port}"}" + url = "http://#{host}#{port.nil? ? '' : ":#{port}"}" ret = Pact::MockService::AppManager.instance.register_mock_service_for(provider_name, url, mock_service_options) raise "pact-mock_service(v#{Pact::MockService::VERSION}) does not support 'find available port' feature" unless ret @port = ret @@ -71,6 +76,7 @@ def create_consumer_contract_builder :provider_name => provider_name, :pactfile_write_mode => Pact.configuration.pactfile_write_mode, :port => port, + :host => host, :pact_dir => Pact.configuration.pact_dir } Pact::Consumer::ConsumerContractBuilder.new consumer_contract_builder_fields diff --git a/lib/pact/consumer/consumer_contract_builder.rb b/lib/pact/consumer/consumer_contract_builder.rb index c186b7ce..be556d6b 100644 --- a/lib/pact/consumer/consumer_contract_builder.rb +++ b/lib/pact/consumer/consumer_contract_builder.rb @@ -21,8 +21,12 @@ def initialize(attributes) pactfile_write_mode: attributes[:pactfile_write_mode].to_s, pact_dir: attributes.fetch(:pact_dir) } - @mock_service_client = Pact::MockService::Client.new(attributes[:port]) - @mock_service_base_url = "http://localhost:#{attributes[:port]}" + @mock_service_client = Pact::MockService::Client.new(attributes[:port], attributes[:host]) + @mock_service_base_url = "http://#{attributes[:host]}:#{attributes[:port]}" + end + + def without_writing_to_pact + interaction_builder.without_writing_to_pact end def given(provider_state) diff --git a/lib/pact/consumer/interaction_builder.rb b/lib/pact/consumer/interaction_builder.rb index 7916b712..78398ea7 100644 --- a/lib/pact/consumer/interaction_builder.rb +++ b/lib/pact/consumer/interaction_builder.rb @@ -13,6 +13,12 @@ def initialize &block @callback = block end + def without_writing_to_pact + interaction.metadata ||= {} + interaction.metadata[:write_to_pact] = false + self + end + def upon_receiving description @interaction.description = description self diff --git a/lib/pact/consumer/spec_hooks.rb b/lib/pact/consumer/spec_hooks.rb index cf7c637b..8fa915a5 100644 --- a/lib/pact/consumer/spec_hooks.rb +++ b/lib/pact/consumer/spec_hooks.rb @@ -15,8 +15,8 @@ def before_all def before_each example_description Pact.consumer_world.register_pact_example_ran Pact.configuration.logger.info "Clearing all expectations" - Pact::MockService::AppManager.instance.ports_of_mock_services.each do | port | - Pact::MockService::Client.clear_interactions port, example_description + Pact::MockService::AppManager.instance.urls_of_mock_services.each do | url | + Pact::MockService::Client.clear_interactions url, example_description end end @@ -29,7 +29,7 @@ def after_each example_description def after_suite if Pact.consumer_world.any_pact_examples_ran? - Pact.consumer_world.consumer_contract_builders.each { | c | c.write_pact } + Pact.consumer_world.consumer_contract_builders.each(&:write_pact) Pact::Doc::Generate.call Pact::MockService::AppManager.instance.kill_all Pact::MockService::AppManager.instance.clear_all @@ -37,4 +37,4 @@ def after_suite end end end -end \ No newline at end of file +end diff --git a/lib/pact/doc/sort_interactions.rb b/lib/pact/doc/sort_interactions.rb index 089f033c..398c11f2 100644 --- a/lib/pact/doc/sort_interactions.rb +++ b/lib/pact/doc/sort_interactions.rb @@ -3,7 +3,7 @@ module Doc class SortInteractions def self.call interactions - interactions.sort{|a, b| sortable_id(a) <=> sortable_id(b)} + interactions.sort_by { |interaction| sortable_id(interaction) } end private @@ -11,7 +11,6 @@ def self.call interactions def self.sortable_id interaction "#{interaction.description.downcase} #{interaction.response.status} #{(interaction.provider_state || '').downcase}" end - end end -end \ No newline at end of file +end diff --git a/lib/pact/hal/authorization_header_redactor.rb b/lib/pact/hal/authorization_header_redactor.rb new file mode 100644 index 00000000..a0c37537 --- /dev/null +++ b/lib/pact/hal/authorization_header_redactor.rb @@ -0,0 +1,32 @@ +require 'delegate' + +module Pact + module Hal + class AuthorizationHeaderRedactor < SimpleDelegator + def puts(*args) + __getobj__().puts(*redact_args(args)) + end + + def print(*args) + __getobj__().puts(*redact_args(args)) + end + + def <<(*args) + __getobj__().send(:<<, *redact_args(args)) + end + + private + + attr_reader :redactions + + def redact_args(args) + args.collect{ | s| redact(s) } + end + + def redact(string) + return string unless string.is_a?(String) + string.gsub(/Authorization: .*\\r\\n/, "Authorization: [redacted]\\r\\n") + end + end + end +end diff --git a/lib/pact/hal/http_client.rb b/lib/pact/hal/http_client.rb index 21428439..1e78b22d 100644 --- a/lib/pact/hal/http_client.rb +++ b/lib/pact/hal/http_client.rb @@ -1,21 +1,27 @@ require 'pact/retry' +require 'pact/hal/authorization_header_redactor' require 'net/http' +require 'rack' +require 'openssl' module Pact module Hal class HttpClient - attr_accessor :username, :password, :verbose + attr_accessor :username, :password, :verbose, :token def initialize options @username = options[:username] @password = options[:password] @verbose = options[:verbose] + @token = options[:token] end def get href, params = {}, headers = {} - query = params.collect{ |(key, value)| "#{CGI::escape(key)}=#{CGI::escape(value)}" }.join("&") uri = URI(href) - uri.query = query + if params && params.any? + existing_params = Rack::Utils.parse_nested_query(uri.query) + uri.query = Rack::Utils.build_nested_query(existing_params.merge(params)) + end perform_request(create_request(uri, 'Get', nil, headers), uri) end @@ -31,22 +37,28 @@ def post href, body = nil, headers = {} def create_request uri, http_method, body = nil, headers = {} request = Net::HTTP.const_get(http_method).new(uri.request_uri) - request['Content-Type'] = "application/json" if ['Post', 'Put', 'Patch'].include?(http_method) - request['Accept'] = "application/hal+json" headers.each do | key, value | request[key] = value end - request.body = body if body request.basic_auth username, password if username + request['Authorization'] = "Bearer #{token}" if token request end def perform_request request, uri response = Retry.until_true do http = Net::HTTP.new(uri.host, uri.port, :ENV) - http.set_debug_output(Pact.configuration.output_stream) if verbose + http.set_debug_output(output_stream) if verbose? http.use_ssl = (uri.scheme == 'https') + http.ca_file = ENV['SSL_CERT_FILE'] if ENV['SSL_CERT_FILE'] && ENV['SSL_CERT_FILE'] != '' + http.ca_path = ENV['SSL_CERT_DIR'] if ENV['SSL_CERT_DIR'] && ENV['SSL_CERT_DIR'] != '' + if disable_ssl_verification? + if verbose? + Pact.configuration.output_stream.puts("SSL verification is disabled") + end + http.verify_mode = OpenSSL::SSL::VERIFY_NONE + end http.start do |http| http.request request end @@ -54,6 +66,18 @@ def perform_request request, uri Response.new(response) end + def output_stream + AuthorizationHeaderRedactor.new(Pact.configuration.output_stream) + end + + def verbose? + verbose || ENV['VERBOSE'] == 'true' + end + + def disable_ssl_verification? + ENV['PACT_DISABLE_SSL_VERIFICATION'] == 'true' || ENV['PACT_BROKER_DISABLE_SSL_VERIFICATION'] == 'true' + end + class Response < SimpleDelegator def body bod = raw_body @@ -68,9 +92,17 @@ def raw_body __getobj__().body end + def status + code.to_i + end + def success? __getobj__().code.start_with?("2") end + + def json? + self['content-type'] && self['content-type'] =~ /json/ + end end end end diff --git a/lib/pact/hal/link.rb b/lib/pact/hal/link.rb index 24e8cafb..d248b432 100644 --- a/lib/pact/hal/link.rb +++ b/lib/pact/hal/link.rb @@ -6,6 +6,15 @@ module Hal class Link attr_reader :request_method, :href + DEFAULT_GET_HEADERS = { + "Accept" => "application/hal+json" + }.freeze + + DEFAULT_POST_HEADERS = { + "Accept" => "application/hal+json", + "Content-Type" => "application/json" + }.freeze + def initialize(attrs, http_client) @attrs = attrs @request_method = attrs.fetch(:method, :get).to_sym @@ -14,40 +23,80 @@ def initialize(attrs, http_client) end def run(payload = nil) - response = case request_method - when :get - get(payload) - when :put - put(payload) - when :post - post(payload) - end + case request_method + when :get + get(payload) + when :put + put(payload) + when :post + post(payload) + end + end + + def title_or_name + title || name + end + + def title + @attrs['title'] + end + + def name + @attrs['name'] end def get(payload = {}, headers = {}) - wrap_response(href, @http_client.get(href, payload, headers)) + wrap_response(href, @http_client.get(href, payload, DEFAULT_GET_HEADERS.merge(headers))) + end + + def get!(*args) + get(*args).assert_success! end def put(payload = nil, headers = {}) - wrap_response(href, @http_client.put(href, payload ? JSON.dump(payload) : nil, headers)) + wrap_response(href, @http_client.put(href, payload ? payload.to_json : nil, DEFAULT_POST_HEADERS.merge(headers))) end def post(payload = nil, headers = {}) - wrap_response(href, @http_client.post(href, payload ? JSON.dump(payload) : nil, headers)) + wrap_response(href, @http_client.post(href, payload ? payload.to_json : nil, DEFAULT_POST_HEADERS.merge(headers))) + end + + def post!(payload = nil, headers = {}) + post(payload, headers).assert_success! end def expand(params) expanded_url = expand_url(params, href) new_attrs = @attrs.merge('href' => expanded_url) - Link.new(new_attrs, @http_client) + Link.new(new_attrs, http_client) + end + + def with_query(query) + if query && query.any? + uri = URI(href) + existing_query_params = Rack::Utils.parse_nested_query(uri.query) + uri.query = Rack::Utils.build_nested_query(existing_query_params.merge(query)) + new_attrs = attrs.merge('href' => uri.to_s) + Link.new(new_attrs, http_client) + else + self + end end private + attr_reader :attrs, :http_client + def wrap_response(href, http_response) require 'pact/hal/entity' # avoid circular reference + require 'pact/hal/non_json_entity' + if http_response.success? - Entity.new(href, http_response.body, @http_client, http_response) + if http_response.json? + Entity.new(href, http_response.body, @http_client, http_response) + else + NonJsonEntity.new(href, http_response.raw_body, @http_client, http_response) + end else ErrorEntity.new(href, http_response.raw_body, @http_client, http_response) end diff --git a/lib/pact/hal/non_json_entity.rb b/lib/pact/hal/non_json_entity.rb new file mode 100644 index 00000000..83c96da7 --- /dev/null +++ b/lib/pact/hal/non_json_entity.rb @@ -0,0 +1,28 @@ +module Pact + module Hal + class NonJsonEntity + def initialize(href, body, http_client, response = nil) + @href = href + @body = body + @client = http_client + @response = response + end + + def success? + true + end + + def response + @response + end + + def body + @body + end + + def assert_success! + self + end + end + end +end diff --git a/lib/pact/hash_refinements.rb b/lib/pact/hash_refinements.rb new file mode 100644 index 00000000..3e738521 --- /dev/null +++ b/lib/pact/hash_refinements.rb @@ -0,0 +1,17 @@ +module Pact + module HashRefinements + refine Hash do + def compact + h = {} + each do |key, value| + h[key] = value unless value == nil + end + h + end unless Hash.method_defined? :compact + + def compact! + reject! {|_key, value| value == nil} + end unless Hash.method_defined? :compact! + end + end +end diff --git a/lib/pact/pact_broker.rb b/lib/pact/pact_broker.rb index 45293c07..c91f7ed2 100644 --- a/lib/pact/pact_broker.rb +++ b/lib/pact/pact_broker.rb @@ -1,19 +1,25 @@ require 'pact/pact_broker/fetch_pacts' -require 'pact/pact_broker/fetch_pending_pacts' +require 'pact/pact_broker/fetch_pact_uris_for_verification' +require 'pact/provider/pact_uri' # -# @public Use by Pact Provider Verifier +# @public Used by Pact Provider Verifier # module Pact module PactBroker extend self + # Keep for backwards compatibility with pact-provider-verifier < 1.23.1 def fetch_pact_uris *args Pact::PactBroker::FetchPacts.call(*args).collect(&:uri) end - def fetch_pending_pact_uris *args - Pact::PactBroker::FetchPendingPacts.call(*args).collect(&:uri) + def fetch_pact_uris_for_verification *args + Pact::PactBroker::FetchPactURIsForVerification.call(*args) + end + + def build_pact_uri(*args) + Pact::Provider::PactURI.new(*args) end end end diff --git a/lib/pact/pact_broker/fetch_pact_uris_for_verification.rb b/lib/pact/pact_broker/fetch_pact_uris_for_verification.rb new file mode 100644 index 00000000..631562d3 --- /dev/null +++ b/lib/pact/pact_broker/fetch_pact_uris_for_verification.rb @@ -0,0 +1,101 @@ +require 'pact/hal/entity' +require 'pact/hal/http_client' +require 'pact/provider/pact_uri' +require 'pact/errors' +require 'pact/pact_broker/fetch_pacts' +require 'pact/pact_broker/notices' +require 'pact/pact_broker/pact_selection_description' +require "pact/hash_refinements" + +module Pact + module PactBroker + class FetchPactURIsForVerification + using Pact::HashRefinements + + include PactSelectionDescription + attr_reader :provider, :consumer_version_selectors, :provider_version_branch, :provider_version_tags, :broker_base_url, :http_client_options, :http_client, :options + + PACTS_FOR_VERIFICATION_RELATION = 'pb:provider-pacts-for-verification'.freeze + PACTS_FOR_VERIFICATION_RELATION_BETA = 'beta:provider-pacts-for-verification'.freeze + PACTS = 'pacts'.freeze + HREF = 'href'.freeze + LINKS = '_links'.freeze + SELF = 'self'.freeze + EMBEDDED = '_embedded'.freeze + + def initialize(provider, consumer_version_selectors, provider_version_branch, provider_version_tags, broker_base_url, http_client_options, options = {}) + @provider = provider + @consumer_version_selectors = consumer_version_selectors || [] + @provider_version_branch = provider_version_branch + @provider_version_tags = [*provider_version_tags] + @http_client_options = http_client_options + @broker_base_url = broker_base_url + @http_client = Pact::Hal::HttpClient.new(http_client_options) + @options = options + end + + def self.call(provider, consumer_version_selectors, provider_version_branch, provider_version_tags, broker_base_url, http_client_options, options = {}) + new(provider, consumer_version_selectors, provider_version_branch, provider_version_tags, broker_base_url, http_client_options, options).call + end + + def call + if index.can?(PACTS_FOR_VERIFICATION_RELATION) || index.can?(PACTS_FOR_VERIFICATION_RELATION_BETA) + log_message + pacts_for_verification + else + old_selectors = consumer_version_selectors.collect do | selector | + { name: selector[:tag], all: !selector[:latest], fallback: selector[:fallbackTag]} + end + # Fall back to old method of fetching pacts + FetchPacts.call(provider, old_selectors, broker_base_url, http_client_options) + end + end + + private + + def index + @index_entity ||= Pact::Hal::Link.new({ "href" => broker_base_url }, http_client).get.assert_success! + end + + def pacts_for_verification + pacts_for_verification_entity.response.body[EMBEDDED][PACTS].collect do | pact | + metadata = { + pending: pact["verificationProperties"]["pending"], + notices: extract_notices(pact), + short_description: pact["shortDescription"] + } + Pact::Provider::PactURI.new(pact[LINKS][SELF][HREF], http_client_options, metadata) + end + end + + def pacts_for_verification_entity + index + ._link(PACTS_FOR_VERIFICATION_RELATION, PACTS_FOR_VERIFICATION_RELATION_BETA) + .expand(provider: provider) + .post!(query) + end + + def query + q = {} + q["includePendingStatus"] = options[:include_pending_status] + q["consumerVersionSelectors"] = consumer_version_selectors if consumer_version_selectors.any? + q["providerVersionTags"] = provider_version_tags if provider_version_tags.any? + q["providerVersionBranch"] = provider_version_branch + q["includeWipPactsSince"] = options[:include_wip_pacts_since] + q.compact + end + + def extract_notices(pact) + Notices.new((pact["verificationProperties"]["notices"] || []).collect{ |notice| symbolize_keys(notice) }) + end + + def symbolize_keys(hash) + hash.each_with_object({}){ |(k,v), h| h[k.to_sym] = v } + end + + def log_message + Pact.configuration.output_stream.puts "INFO: #{pact_selection_description(provider, consumer_version_selectors, options, broker_base_url)}" + end + end + end +end diff --git a/lib/pact/pact_broker/fetch_pending_pacts.rb b/lib/pact/pact_broker/fetch_pending_pacts.rb deleted file mode 100644 index 10faee5d..00000000 --- a/lib/pact/pact_broker/fetch_pending_pacts.rb +++ /dev/null @@ -1,58 +0,0 @@ -require 'pact/hal/entity' -require 'pact/hal/http_client' -require 'pact/provider/pact_uri' -require 'pact/errors' - -module Pact - module PactBroker - class FetchPendingPacts - attr_reader :provider, :tags, :broker_base_url, :http_client_options, :http_client, :index_entity - - PENDING_PROVIDER_RELATION = 'beta:pending-provider-pacts'.freeze - WIP_PROVIDER_RELATION = 'beta:wip-provider-pacts'.freeze # deprecated - PACTS = 'pacts'.freeze - PB_PACTS = 'pb:pacts'.freeze - HREF = 'href'.freeze - - def initialize(provider, broker_base_url, http_client_options) - @provider = provider - @http_client_options = http_client_options - @broker_base_url = broker_base_url - @http_client = Pact::Hal::HttpClient.new(http_client_options) - end - - def self.call(provider, broker_base_url, http_client_options) - new(provider, broker_base_url, http_client_options).call - end - - def call - if index.success? - pending_pacts_for_provider - else - raise Pact::Error.new("Error retrieving #{broker_base_url} status=#{index_entity.response.code} #{index_entity.response.raw_body}") - end - end - - private - - def index - @index_entity ||= Pact::Hal::Link.new({ "href" => broker_base_url }, http_client).get.assert_success! - end - - def pending_pacts_for_provider - link = index_entity._link(WIP_PROVIDER_RELATION, PENDING_PROVIDER_RELATION) - if link - get_pact_urls(link.expand(provider: provider).get) - else - [] - end - end - - def get_pact_urls(link_by_provider) - link_by_provider.fetch(PB_PACTS, PACTS).collect do |pact| - Pact::Provider::PactURI.new(pact[HREF], http_client_options) - end - end - end - end -end diff --git a/lib/pact/pact_broker/notices.rb b/lib/pact/pact_broker/notices.rb new file mode 100644 index 00000000..4d854bcf --- /dev/null +++ b/lib/pact/pact_broker/notices.rb @@ -0,0 +1,34 @@ +module Pact + module PactBroker + class Notices < Array + def before_verification_notices + select { | notice | notice[:when].nil? || notice[:when].start_with?('before_verification') } + end + + def before_verification_notices_text + before_verification_notices.collect{ | notice | notice[:text] } + end + + def after_verification_notices(success, published) + select { | notice | notice[:when] == "after_verification:success_#{success}_published_#{published}" || notice[:when] == "after_verification" } + .collect do | notice | + notice.merge(:when => simplify_notice_when(notice[:when])) + end + end + + def after_verification_notices_text(success, published) + after_verification_notices(success, published).collect{ | notice | notice[:text] } + end + + def all_notices(success, published) + before_verification_notices + after_verification_notices(success, published) + end + + private + + def simplify_notice_when(when_key) + when_key.split(":").first + end + end + end +end diff --git a/lib/pact/pact_broker/pact_selection_description.rb b/lib/pact/pact_broker/pact_selection_description.rb new file mode 100644 index 00000000..6b6a8110 --- /dev/null +++ b/lib/pact/pact_broker/pact_selection_description.rb @@ -0,0 +1,66 @@ +module Pact + module PactBroker + module PactSelectionDescription + def pact_selection_description(provider, consumer_version_selectors, options, broker_base_url) + message = "Fetching pacts for #{provider} from #{broker_base_url} with the selection criteria: " + if consumer_version_selectors.any? + desc = consumer_version_selectors.collect do |selector| + desc = nil + if selector[:tag] + desc = !selector[:latest] ? "all for tag #{selector[:tag]}" : "latest for tag #{selector[:tag]}" + desc = "#{desc} of #{selector[:consumer]}" if selector[:consumer] + elsif selector[:branch] + desc = "latest from branch #{selector[:branch]}" + desc = "#{desc} of #{selector[:consumer]}" if selector[:consumer] + elsif selector[:mainBranch] + desc = "latest from main branch" + desc = "#{desc} of #{selector[:consumer]}" if selector[:consumer] + elsif selector[:deployed] + if selector[:environment] + desc = "currently deployed to #{selector[:environment]}" + else + desc = "currently deployed" + end + desc = "#{selector[:consumer]} #{desc}" if selector[:consumer] + elsif selector[:released] + if selector[:environment] + desc = "currently released to #{selector[:environment]}" + else + desc = "currently released" + end + desc = "#{selector[:consumer]} #{desc}" if selector[:consumer] + elsif selector[:deployedOrReleased] + if selector[:environment] + desc = "currently deployed or released to #{selector[:environment]}" + else + desc = "currently deployed or released" + end + desc = "#{selector[:consumer]} #{desc}" if selector[:consumer] + elsif selector[:environment] + desc = "currently in #{selector[:environment]}" + desc = "#{selector[:consumer]} #{desc}" if selector[:consumer] + elsif selector[:matchingBranch] + desc = "matching current branch" + desc = "#{desc} for #{selector[:consumer]}" if selector[:consumer] + elsif selector[:matchingTag] + desc = "matching tag" + desc = "#{desc} for #{selector[:consumer]}" if selector[:consumer] + else + desc = selector.to_s + end + + fallback = selector[:fallback] || selector[:fallbackTag] + desc = "#{desc} (or #{fallback} if not found)" if fallback + + desc + end.join(", ") + if options[:include_wip_pacts_since] + desc = "#{desc}, work in progress pacts created after #{options[:include_wip_pacts_since]}" + end + message << "#{desc}" + end + message + end + end + end +end diff --git a/lib/pact/provider/configuration/dsl.rb b/lib/pact/provider/configuration/dsl.rb index 82e1ab8f..51b7f8f2 100644 --- a/lib/pact/provider/configuration/dsl.rb +++ b/lib/pact/provider/configuration/dsl.rb @@ -1,4 +1,5 @@ require 'pact/provider/configuration/service_provider_dsl' +require 'pact/provider/configuration/message_provider_dsl' module Pact @@ -8,6 +9,10 @@ module DSL def service_provider name, &block Configuration::ServiceProviderDSL.build(name, &block) end + + def message_provider name, &block + Configuration::MessageProviderDSL.build(name, &block) + end end end -end \ No newline at end of file +end diff --git a/lib/pact/provider/configuration/message_provider_dsl.rb b/lib/pact/provider/configuration/message_provider_dsl.rb new file mode 100644 index 00000000..0a886ae1 --- /dev/null +++ b/lib/pact/provider/configuration/message_provider_dsl.rb @@ -0,0 +1,59 @@ +require 'pact/provider/configuration/service_provider_dsl' + +module Pact + module Provider + module Configuration + class MessageProviderDSL < ServiceProviderDSL + class RackToMessageAdapter + def initialize(message_builder) + @message_builder = message_builder + end + + def call(env) + request_body_json = JSON.parse(env['rack.input'].read) + contents = @message_builder.call(request_body_json['description']) + [200, {"Content-Type" => "application/json"}, [{ contents: contents }.to_json]] + end + end + + def initialize name + super + @mapper_block = lambda { |args| } + end + + dsl do + def app &block + self.app_block = block + end + + def app_version application_version + self.application_version = application_version + end + + def app_version_tags tags + self.tags = tags + end + + def publish_verification_results publish_verification_results + self.publish_verification_results = publish_verification_results + Pact::RSpec.with_rspec_2 do + Pact.configuration.error_stream.puts "WARN: Publishing of verification results is currently not supported with rspec 2. If you would like this functionality, please feel free to submit a PR!" + end + end + + def honours_pact_with consumer_name, options = {}, &block + create_pact_verification consumer_name, options, &block + end + + def honours_pacts_from_pact_broker &block + create_pact_verification_from_broker &block + end + + def builder &block + self.app_block = lambda { RackToMessageAdapter.new(block) } + end + end + end + end + end +end diff --git a/lib/pact/provider/configuration/pact_verification_from_broker.rb b/lib/pact/provider/configuration/pact_verification_from_broker.rb index c2778723..9a1a6346 100644 --- a/lib/pact/provider/configuration/pact_verification_from_broker.rb +++ b/lib/pact/provider/configuration/pact_verification_from_broker.rb @@ -1,7 +1,8 @@ require 'pact/shared/dsl' require 'pact/provider/world' -require 'pact/pact_broker/fetch_pacts' +require 'pact/pact_broker/fetch_pact_uris_for_verification' require 'pact/errors' +require 'pact/utils/string' module Pact module Provider @@ -14,11 +15,16 @@ class PactVerificationFromBroker # in parent scope, it will clash with these ones, # so put an underscore in front of the name to be safer. - attr_accessor :_provider_name, :_pact_broker_base_url, :_consumer_version_tags, :_basic_auth_options, :_verbose + attr_accessor :_provider_name, :_pact_broker_base_url, :_consumer_version_tags, :_provider_version_branch, :_provider_version_tags, :_basic_auth_options, :_enable_pending, :_include_wip_pacts_since, :_verbose, :_consumer_version_selectors - def initialize(provider_name) + def initialize(provider_name, provider_version_branch, provider_version_tags) @_provider_name = provider_name + @_provider_version_branch = provider_version_branch + @_provider_version_tags = provider_version_tags @_consumer_version_tags = [] + @_consumer_version_selectors = [] + @_enable_pending = false + @_include_wip_pacts_since = nil @_verbose = false end @@ -32,6 +38,22 @@ def consumer_version_tags consumer_version_tags self._consumer_version_tags = *consumer_version_tags end + def consumer_version_selectors consumer_version_selectors + self._consumer_version_selectors = *consumer_version_selectors + end + + def enable_pending enable_pending + self._enable_pending = enable_pending + end + + def include_wip_pacts_since since + self._include_wip_pacts_since = if since.respond_to?(:xmlschema) + since.xmlschema + else + since + end + end + def verbose verbose self._verbose = verbose end @@ -45,10 +67,50 @@ def finalize private def create_pact_verification - fetch_pacts = Pact::PactBroker::FetchPacts.new(_provider_name, _consumer_version_tags, _pact_broker_base_url, _basic_auth_options.merge(verbose: _verbose)) + fetch_pacts = Pact::PactBroker::FetchPactURIsForVerification.new( + _provider_name, + consumer_version_selectors, + _provider_version_branch, + _provider_version_tags, + _pact_broker_base_url, + _basic_auth_options.merge(verbose: _verbose), + { include_pending_status: _enable_pending, include_wip_pacts_since: _include_wip_pacts_since } + ) + Pact.provider_world.add_pact_uri_source fetch_pacts end + def consumer_version_selectors + convert_tags_to_selectors + convert_consumer_version_selectors + end + + def convert_tags_to_selectors + _consumer_version_tags.collect do | tag | + if tag.is_a?(Hash) + { + tag: tag.fetch(:name), + latest: !tag[:all], + fallbackTag: tag[:fallback] + } + elsif tag.is_a?(String) + { + tag: tag, + latest: true + } + else + raise Pact::Error.new("The value supplied for consumer_version_tags must be a String or a Hash. Found #{tag.class}") + end + end + end + + def convert_consumer_version_selectors + _consumer_version_selectors.collect do | selector | + selector.each_with_object({}) do | (key, value), new_selector | + new_selector[Pact::Utils::String.camelcase(key.to_s).to_sym] = value + end + end + end + def validate raise Pact::Error.new("Please provide a pact_broker_base_url from which to retrieve the pacts") unless _pact_broker_base_url end diff --git a/lib/pact/provider/configuration/service_provider_config.rb b/lib/pact/provider/configuration/service_provider_config.rb index 2b3068a2..8a89ba5c 100644 --- a/lib/pact/provider/configuration/service_provider_config.rb +++ b/lib/pact/provider/configuration/service_provider_config.rb @@ -4,9 +4,11 @@ module Configuration class ServiceProviderConfig attr_accessor :application_version + attr_reader :branch - def initialize application_version, tags, publish_verification_results, &app_block + def initialize application_version, branch, tags, publish_verification_results, &app_block @application_version = application_version + @branch = branch @tags = [*tags] @publish_verification_results = publish_verification_results @app_block = app_block diff --git a/lib/pact/provider/configuration/service_provider_dsl.rb b/lib/pact/provider/configuration/service_provider_dsl.rb index bdd29ce4..fccb45d2 100644 --- a/lib/pact/provider/configuration/service_provider_dsl.rb +++ b/lib/pact/provider/configuration/service_provider_dsl.rb @@ -15,7 +15,7 @@ class ServiceProviderDSL extend Pact::DSL - attr_accessor :name, :app_block, :application_version, :tags, :publish_verification_results + attr_accessor :name, :app_block, :application_version, :branch, :tags, :publish_verification_results CONFIG_RU_APP = lambda { unless File.exist? Pact.configuration.config_ru_path @@ -44,6 +44,10 @@ def app_version_tags tags self.tags = tags end + def app_version_branch branch + self.branch = branch + end + def publish_verification_results publish_verification_results self.publish_verification_results = publish_verification_results Pact::RSpec.with_rspec_2 do @@ -65,7 +69,7 @@ def create_pact_verification consumer_name, options, &block end def create_pact_verification_from_broker(&block) - PactVerificationFromBroker.build(name, &block) + PactVerificationFromBroker.build(name, branch, tags, &block) end def finalize @@ -85,7 +89,7 @@ def application_version_blank? end def create_service_provider - Pact.configuration.provider = ServiceProviderConfig.new(application_version, tags, publish_verification_results, &@app_block) + Pact.configuration.provider = ServiceProviderConfig.new(application_version, branch, tags, publish_verification_results, &@app_block) end end end diff --git a/lib/pact/provider/help/content.rb b/lib/pact/provider/help/content.rb index 26452ea2..631ef28f 100644 --- a/lib/pact/provider/help/content.rb +++ b/lib/pact/provider/help/content.rb @@ -5,8 +5,8 @@ module Provider module Help class Content - def initialize pact_jsons - @pact_jsons = pact_jsons + def initialize pact_sources + @pact_sources = pact_sources end def text @@ -15,7 +15,7 @@ def text private - attr_reader :pact_jsons + attr_reader :pact_sources def help_text temp_dir = Pact.configuration.tmp_dir @@ -28,7 +28,7 @@ def template_string end def pact_diffs - pact_jsons.collect do | pact_json | + pact_sources.collect do | pact_json | PactDiff.call(pact_json) end.compact.join("\n") end diff --git a/lib/pact/provider/help/pact_diff.rb b/lib/pact/provider/help/pact_diff.rb index cf967a36..f9677890 100644 --- a/lib/pact/provider/help/pact_diff.rb +++ b/lib/pact/provider/help/pact_diff.rb @@ -1,26 +1,24 @@ +require 'pact/hal/entity' + module Pact module Provider module Help - class PactDiff - class PrintPactDiffError < StandardError; end - attr_reader :pact_json, :output + attr_reader :pact_source, :output - def initialize pact_json - @pact_json = pact_json + def initialize pact_source + @pact_source = pact_source end - def self.call pact_json - new(pact_json).call + def self.call pact_source + new(pact_source).call end def call begin - if diff_rel && diff_url - header + "\n" + get_diff - end + header + "\n" + get_diff rescue PrintPactDiffError => e return e.message end @@ -32,35 +30,13 @@ def header "The following changes have been made since the previous distinct version of this pact, and may be responsible for verification failure:\n" end - def pact_hash - @pact_hash ||= json_load(pact_json) - end - - def links - pact_hash['_links'] || pact_hash['links'] - end - - def diff_rel - return nil unless links - key = links.keys.find { | key | key =~ /diff/ && key =~ /distinct/ && key =~ /previous/} - key ? links[key] : nil - end - - def diff_url - diff_rel['href'] - end - def get_diff begin - open(diff_url) { | file | file.read } + pact_source.hal_entity._link!("pb:diff-previous-distinct").get!(nil, "Accept" => "text/plain").body rescue StandardError => e - raise PrintPactDiffError.new("Tried to retrieve diff with previous pact from #{diff_url}, but received response code #{e}.") + raise PrintPactDiffError.new("Tried to retrieve diff with previous pact, but received error #{e.class} #{e.message}.") end end - - def json_load json - JSON.load(json, nil, { max_nesting: 50 }) - end end end end diff --git a/lib/pact/provider/help/write.rb b/lib/pact/provider/help/write.rb index 29e06a95..0931c4dd 100644 --- a/lib/pact/provider/help/write.rb +++ b/lib/pact/provider/help/write.rb @@ -9,23 +9,25 @@ class Write HELP_FILE_NAME = 'help.md' - def self.call pact_jsons, reports_dir = Pact.configuration.reports_dir - new(pact_jsons, reports_dir).call + def self.call pact_sources, reports_dir = Pact.configuration.reports_dir + new(pact_sources, reports_dir).call end - def initialize pact_jsons, reports_dir - @pact_jsons = pact_jsons + def initialize pact_sources, reports_dir + @pact_sources = pact_sources @reports_dir = File.expand_path(reports_dir) end def call clean_reports_dir write + rescue StandardError => e + Pact.configuration.error_stream.puts("ERROR: Error generating help output - #{e.class} #{e.message} \n" + e.backtrace.join("\n")) end private - attr_reader :reports_dir, :pact_jsons + attr_reader :reports_dir, :pact_sources def clean_reports_dir raise "Cleaning report dir #{reports_dir} would delete project!" if reports_dir_contains_pwd @@ -46,9 +48,8 @@ def help_path end def help_text - Content.new(pact_jsons).text + Content.new(pact_sources).text end - end end end diff --git a/lib/pact/provider/pact_helper_locator.rb b/lib/pact/provider/pact_helper_locator.rb index 43f48b5d..d71f936c 100644 --- a/lib/pact/provider/pact_helper_locator.rb +++ b/lib/pact/provider/pact_helper_locator.rb @@ -1,22 +1,24 @@ module Pact module Provider module PactHelperLocater - PACT_HELPER_FILE_PATTERNS = [ + PACT_HELPER_FILE_PATTERNS = [ "spec/**/*service*consumer*/pact_helper.rb", "spec/**/*consumer*/pact_helper.rb", "spec/**/pact_helper.rb", - "**/pact_helper.rb"] + "test/**/*service*consumer*/pact_helper.rb", + "test/**/*consumer*/pact_helper.rb", + "test/**/pact_helper.rb", + "**/pact_helper.rb" + ] - NO_PACT_HELPER_FOUND_MSG = "Please create a pact_helper.rb file that can be found using one of the following patterns: #{PACT_HELPER_FILE_PATTERNS.join(", ")}" - - def self.pact_helper_path - pact_helper_search_results = [] - PACT_HELPER_FILE_PATTERNS.find { | pattern | (pact_helper_search_results.concat(Dir.glob(pattern))).any? } - raise NO_PACT_HELPER_FOUND_MSG if pact_helper_search_results.empty? - File.join(Dir.pwd, pact_helper_search_results[0]) - end + NO_PACT_HELPER_FOUND_MSG = "Please create a pact_helper.rb file that can be found using one of the following patterns: #{PACT_HELPER_FILE_PATTERNS.join(", ")}" + def self.pact_helper_path + pact_helper_search_results = [] + PACT_HELPER_FILE_PATTERNS.find { | pattern | (pact_helper_search_results.concat(Dir.glob(pattern))).any? } + raise NO_PACT_HELPER_FOUND_MSG if pact_helper_search_results.empty? + File.join(Dir.pwd, pact_helper_search_results[0]) + end end end end - diff --git a/lib/pact/provider/pact_source.rb b/lib/pact/provider/pact_source.rb index 44b3cb33..a1a14e27 100644 --- a/lib/pact/provider/pact_source.rb +++ b/lib/pact/provider/pact_source.rb @@ -1,10 +1,13 @@ require 'pact/consumer_contract/pact_file' +require 'pact/hal/http_client' +require 'pact/hal/entity' +require 'pact/consumer_contract' module Pact module Provider class PactSource - attr_reader :uri + attr_reader :uri # PactURI class def initialize uri @uri = uri @@ -17,6 +20,21 @@ def pact_json def pact_hash @pact_hash ||= JSON.load(pact_json, nil, { max_nesting: 50 }) end + + def pending? + uri.metadata[:pending] + end + + def consumer_contract + @consumer_contract ||= Pact::ConsumerContract.from_json(pact_json) + end + + def hal_entity + http_client_keys = [:username, :password, :token] + http_client_options = uri.options.reject{ |k, _| !http_client_keys.include?(k) } + http_client = Pact::Hal::HttpClient.new(http_client_options) + Pact::Hal::Entity.new(uri, pact_hash, http_client) + end end end end diff --git a/lib/pact/provider/pact_spec_runner.rb b/lib/pact/provider/pact_spec_runner.rb index 7db51560..cc7e50a9 100644 --- a/lib/pact/provider/pact_spec_runner.rb +++ b/lib/pact/provider/pact_spec_runner.rb @@ -9,9 +9,9 @@ require 'pact/provider/help/write' require 'pact/provider/verification_results/publish_all' require 'pact/provider/rspec/pact_broker_formatter' - -require_relative 'rspec' - +require 'pact/provider/rspec/json_formatter' +require 'pact/provider/rspec' +require 'pact/provider/rspec/calculate_exit_code' module Pact module Provider @@ -75,12 +75,14 @@ def configure_rspec end # For the Pact::Provider::RSpec::PactBrokerFormatter + Pact.provider_world.verbose = options[:verbose] Pact.provider_world.pact_sources = pact_sources jsons = pact_jsons executing_with_ruby = executing_with_ruby? config.after(:suite) do | suite | - Pact::Provider::Help::Write.call(jsons) if executing_with_ruby + Pact.provider_world.failed_examples = suite.reporter.failed_examples + Pact::Provider::Help::Write.call(Pact.provider_world.pact_sources) if executing_with_ruby end end @@ -91,7 +93,12 @@ def run_specs ::RSpec::Core::CommandLine.new(NoConfigurationOptions.new) .run(::RSpec.configuration.output_stream, ::RSpec.configuration.error_stream) end - options[:ignore_failures] ? 0 : exit_code + + if options[:ignore_failures] + 0 + else + Pact::Provider::RSpec::CalculateExitCode.call(pact_sources, Pact.provider_world.failed_examples) + end end def rspec_runner_options @@ -118,12 +125,12 @@ def pact_jsons def initialize_specs pact_sources.each do | pact_source | - options = { - criteria: @options[:criteria], - ignore_failures: @options[:ignore_failures], - request_customizer: @options[:request_customizer] + spec_options = { + criteria: options[:criteria], + ignore_failures: options[:ignore_failures], + request_customizer: options[:request_customizer] } - honour_pactfile pact_source.uri, ordered_pact_json(pact_source.pact_json), options + honour_pactfile pact_source, ordered_pact_json(pact_source.pact_json), spec_options end end @@ -134,11 +141,12 @@ def configure_output output = options[:out] || Pact.configuration.output_stream if options[:format] - ::RSpec.configuration.add_formatter options[:format], output - if !options[:out] - # Don't want to mess up the JSON parsing with messages to stdout, so send it to stderr - Pact.configuration.output_stream = Pact.configuration.error_stream - end + formatter = options[:format] == 'json' ? Pact::Provider::RSpec::JsonFormatter : options[:format] + # Send formatted output to $stdout for parsing, unless a file is specified + output = options[:out] || $stdout + ::RSpec.configuration.add_formatter formatter, output + # Don't want to mess up the JSON parsing with INFO and DEBUG messages to stdout, so send it to stderr + Pact.configuration.output_stream = Pact.configuration.error_stream if !options[:out] else # Sometimes the formatter set in the cli.rb get set with an output of StringIO.. don't know why formatter_class = Pact::RSpec.formatter_class @@ -147,8 +155,6 @@ def configure_output end ::RSpec.configuration.full_backtrace = @options[:full_backtrace] - - ::RSpec.configuration.failure_color = :yellow if @options[:ignore_failures] end def ordered_pact_json(pact_json) diff --git a/lib/pact/provider/pact_uri.rb b/lib/pact/provider/pact_uri.rb index c5f36474..56640a6e 100644 --- a/lib/pact/provider/pact_uri.rb +++ b/lib/pact/provider/pact_uri.rb @@ -1,21 +1,23 @@ module Pact module Provider class PactURI - attr_reader :uri, :options + attr_reader :uri, :options, :metadata - def initialize (uri, options={}) + def initialize (uri, options = nil, metadata = nil) @uri = uri - @options = options + @options = options || {} + @metadata = metadata || {} # make sure it's not nil if nil is passed in end def == other other.is_a?(PactURI) && uri == other.uri && - options == other.options + options == other.options && + metadata == other.metadata end def basic_auth? - !!username + !!username && !!password end def username @@ -27,12 +29,27 @@ def password end def to_s - if(basic_auth?) - URI(@uri).tap { |x| x.userinfo="#{username}:*****"}.to_s + if basic_auth? && http_or_https_uri? + begin + URI(@uri).tap { |x| x.userinfo="#{username}:*****"}.to_s + rescue URI::InvalidComponentError + URI(@uri).tap { |x| x.userinfo="*****:*****"}.to_s + end + elsif personal_access_token? && http_or_https_uri? + URI(@uri).tap { |x| x.userinfo="*****"}.to_s else - @uri + uri end end + + private def personal_access_token? + !!username && !password + end + + private def http_or_https_uri? + uri.start_with?('http://', 'https://') + end + end end end diff --git a/lib/pact/provider/request.rb b/lib/pact/provider/request.rb index 3aaf0662..ad4b1998 100644 --- a/lib/pact/provider/request.rb +++ b/lib/pact/provider/request.rb @@ -54,7 +54,7 @@ def reified_body end def rack_request_header_for header - with_http_prefix(header.to_s.upcase).gsub('-', '_') + with_http_prefix(header.to_s.upcase).tr('-', '_') end def rack_request_value_for value diff --git a/lib/pact/provider/rspec.rb b/lib/pact/provider/rspec.rb index 9ac48d50..a6e7372c 100644 --- a/lib/pact/provider/rspec.rb +++ b/lib/pact/provider/rspec.rb @@ -17,27 +17,43 @@ def app end module ClassMethods + EMPTY_ARRAY = [].freeze include ::RSpec::Core::DSL - def honour_pactfile pact_uri, pact_json, options - pact_description = options[:ignore_failures] ? "Pending pact" : "pact" - Pact.configuration.output_stream.puts "INFO: Reading #{pact_description} at #{pact_uri}" - Pact.configuration.output_stream.puts "INFO: Filtering interactions by: #{options[:criteria]}" if options[:criteria] && options[:criteria].any? + def honour_pactfile pact_source, pact_json, options + pact_uri = pact_source.uri + Pact.configuration.output_stream.puts "INFO: Reading pact at #{pact_uri}" consumer_contract = Pact::ConsumerContract.from_json(pact_json) - ::RSpec.describe "Verifying a #{pact_description} between #{consumer_contract.consumer.name} and #{consumer_contract.provider.name}", pactfile_uri: pact_uri do - honour_consumer_contract consumer_contract, options.merge(pact_json: pact_json, pact_uri: pact_uri) + suffix = pact_uri.metadata[:pending] ? " [PENDING]": "" + example_group_description = "Verifying a pact between #{consumer_contract.consumer.name} and #{consumer_contract.provider.name}#{suffix}" + example_group_metadata = { pactfile_uri: pact_uri, pact_criteria: options[:criteria] } + + ::RSpec.describe example_group_description, example_group_metadata do + honour_consumer_contract consumer_contract, options.merge( + pact_json: pact_json, + pact_uri: pact_uri, + pact_source: pact_source, + consumer_contract: consumer_contract, + criteria: options[:criteria] + ) end end def honour_consumer_contract consumer_contract, options = {} - describe_consumer_contract consumer_contract, options.merge(consumer: consumer_contract.consumer.name) + describe_consumer_contract consumer_contract, options.merge(consumer: consumer_contract.consumer.name, pact_context: InteractionContext.new) end private def describe_consumer_contract consumer_contract, options - consumer_interactions(consumer_contract, options).each do |interaction| + consumer_interactions(consumer_contract, options).tap{ |interactions| + if interactions.empty? + # If there are no interactions, the documentation formatter never fires to print this out, + # so print it out here. + Pact.configuration.output_stream.puts "DEBUG: All interactions for #{options[:pact_uri]} have been filtered out by criteria: #{options[:criteria]}" if options[:criteria] && options[:criteria].any? + end + }.each do |interaction| describe_interaction_with_provider_state interaction, options end end @@ -46,7 +62,7 @@ def consumer_interactions(consumer_contract, options) if options[:criteria].nil? consumer_contract.interactions else - consumer_contract.find_interactions options[:criteria] + consumer_contract.find_interactions(options[:criteria]) end end @@ -74,12 +90,15 @@ def describe_interaction interaction, options pact_interaction: interaction, pact_interaction_example_description: interaction_description_for_rerun_command(interaction), pact_uri: options[:pact_uri], - pact_ignore_failures: options[:ignore_failures] + pact_source: options[:pact_source], + pact_ignore_failures: options[:pact_source].pending? || options[:ignore_failures], + pact_consumer_contract: options[:consumer_contract] } describe description_for(interaction), metadata do interaction_context = InteractionContext.new + pact_context = options[:pact_context] before do | example | interaction_context.run_once :before do diff --git a/lib/pact/provider/rspec/calculate_exit_code.rb b/lib/pact/provider/rspec/calculate_exit_code.rb new file mode 100644 index 00000000..472d2599 --- /dev/null +++ b/lib/pact/provider/rspec/calculate_exit_code.rb @@ -0,0 +1,18 @@ +module Pact + module Provider + module RSpec + module CalculateExitCode + def self.call(pact_sources, failed_examples) + any_non_pending_failures = pact_sources.any? do |pact_source| + if pact_source.pending? + nil + else + failed_examples.select { |e| e.metadata[:pact_source] == pact_source }.any? + end + end + any_non_pending_failures ? 1 : 0 + end + end + end + end +end diff --git a/lib/pact/provider/rspec/formatter_rspec_3.rb b/lib/pact/provider/rspec/formatter_rspec_3.rb index 00a4ee62..8c3560c8 100644 --- a/lib/pact/provider/rspec/formatter_rspec_3.rb +++ b/lib/pact/provider/rspec/formatter_rspec_3.rb @@ -6,13 +6,13 @@ module Pact module Provider module RSpec - class Formatter < ::RSpec::Core::Formatters::DocumentationFormatter class NilFormatter < ::RSpec::Core::Formatters::BaseFormatter Pact::RSpec.with_rspec_3 do ::RSpec::Core::Formatters.register self, :start, :example_group_started, :close end + def dump_summary(summary) end end @@ -24,6 +24,26 @@ def dump_summary(summary) C = ::Term::ANSIColor + def example_group_started(notification) + # This is the metadata on the top level "Verifying a pact between X and Y" describe block + if @group_level == 0 + Pact.configuration.output_stream.puts + pact_uri = notification.group.metadata[:pactfile_uri] + ::RSpec.configuration.failure_color = pact_uri.metadata[:pending] ? :yellow : :red + + if pact_uri.metadata[:notices] + pact_uri.metadata[:notices].before_verification_notices_text.each do | text | + Pact.configuration.output_stream.puts("DEBUG: #{text}") + end + end + + criteria = notification.group.metadata[:pact_criteria] + Pact.configuration.output_stream.puts "DEBUG: Filtering interactions by: #{criteria}" if criteria && criteria.any? + end + super + end + + def dump_summary(summary) output.puts "\n" + colorized_totals_line(summary) return if summary.failure_count == 0 @@ -35,28 +55,26 @@ def dump_summary(summary) private def interactions_count(summary) - summary.examples.collect{ |e| e.metadata[:pact_interaction_example_description] }.uniq.size + summary.examples.collect{ |e| interaction_unique_key(e) }.uniq.size end def failed_interactions_count(summary) - summary.failed_examples.collect{ |e| e.metadata[:pact_interaction_example_description] }.uniq.size + failed_interaction_examples(summary).size end - def ignore_failures?(summary) - summary.failed_examples.any?{ |e| e.metadata[:pact_ignore_failures] } + def pending_interactions_count(summary) + pending_interaction_examples(summary).size end def failure_title summary - if ignore_failures?(summary) - "#{failed_interactions_count(summary)} pending" - else - ::RSpec::Core::Formatters::Helpers.pluralize(failed_interactions_count(summary), "failure") - end + ::RSpec::Core::Formatters::Helpers.pluralize(failed_interactions_count(summary), "failure") end def totals_line summary line = ::RSpec::Core::Formatters::Helpers.pluralize(interactions_count(summary), "interaction") line << ", " << failure_title(summary) + pending_count = pending_interactions_count(summary) + line << ", " << "#{pending_count} pending" if pending_count > 0 line end @@ -69,13 +87,20 @@ def color_for_summary summary end def print_rerun_commands summary - if ignore_failures?(summary) + if pending_interactions_count(summary) > 0 + set_rspec_failure_color(:yellow) output.puts("\nPending interactions: (Failures listed here are expected and do not affect your suite's status)\n\n") - else - output.puts("\nFailed interactions:\n\n") + interaction_rerun_commands(pending_interaction_examples(summary)).each do | message | + output.puts(message) + end + set_rspec_failure_color(:red) end - interaction_rerun_commands(summary).each do | message | - output.puts(message) + + if failed_interactions_count(summary) > 0 + output.puts("\nFailed interactions:\n\n") + interaction_rerun_commands(failed_interaction_examples(summary)).each do | message | + output.puts(message) + end end end @@ -85,25 +110,64 @@ def print_missing_provider_states end end - def interaction_rerun_commands summary - summary.failed_examples.collect do |example| + def pending_interaction_examples(summary) + one_failed_example_per_interaction(summary).select do | example | + example.metadata[:pactfile_uri].metadata[:pending] + end + end + + def failed_interaction_examples(summary) + one_failed_example_per_interaction(summary).select do | example | + !example.metadata[:pactfile_uri].metadata[:pending] + end + end + + def one_failed_example_per_interaction(summary) + summary.failed_examples.group_by{| e| interaction_unique_key(e)}.values.collect(&:first) + end + + def interaction_rerun_commands examples + examples.collect do |example| interaction_rerun_command_for example - end.uniq.compact + end.compact.uniq + end + + def interaction_unique_key(example) + # pending is just to make the counting easier, it isn't required for the unique key + { + pactfile_uri: example.metadata[:pactfile_uri], + index: example.metadata[:pact_interaction].index, + } end def interaction_rerun_command_for example example_description = example.metadata[:pact_interaction_example_description] - if ENV['PACT_INTERACTION_RERUN_COMMAND'] + + _id = example.metadata[:pact_interaction]._id + index = example.metadata[:pact_interaction].index + provider_state = example.metadata[:pact_interaction].provider_state + description = example.metadata[:pact_interaction].description + pactfile_uri = example.metadata[:pactfile_uri] + + if _id && ENV['PACT_INTERACTION_RERUN_COMMAND_FOR_BROKER'] + cmd = String.new(ENV['PACT_INTERACTION_RERUN_COMMAND_FOR_BROKER']) + cmd.gsub!("", example.metadata[:pactfile_uri].to_s) + cmd.gsub!("", "#{_id}") + colorizer.wrap("#{cmd} ", ::RSpec.configuration.failure_color) + colorizer.wrap("# #{example_description}", ::RSpec.configuration.detail_color) + elsif ENV['PACT_INTERACTION_RERUN_COMMAND'] cmd = String.new(ENV['PACT_INTERACTION_RERUN_COMMAND']) - provider_state = example.metadata[:pact_interaction].provider_state - description = example.metadata[:pact_interaction].description - pactfile_uri = example.metadata[:pactfile_uri] cmd.gsub!("", pactfile_uri.to_s) cmd.gsub!("", description) cmd.gsub!("", "#{provider_state}") + cmd.gsub!("", "#{index}") colorizer.wrap("#{cmd} ", ::RSpec.configuration.failure_color) + colorizer.wrap("# #{example_description}", ::RSpec.configuration.detail_color) else - colorizer.wrap("* #{example_description}", ::RSpec.configuration.failure_color) + message = if _id + "* #{example_description} (to re-run just this interaction, set environment variable PACT_BROKER_INTERACTION_ID=\"#{_id}\")" + else + "* #{example_description} (to re-run just this interaction, set environment variables PACT_DESCRIPTION=\"#{description}\" PACT_PROVIDER_STATE=\"#{provider_state}\")" + end + colorizer.wrap(message, ::RSpec.configuration.failure_color) end end @@ -122,6 +186,10 @@ def colorizer def executing_with_ruby? ENV['PACT_EXECUTING_LANGUAGE'] == 'ruby' end + + def set_rspec_failure_color color + ::RSpec.configuration.failure_color = color + end end end end diff --git a/lib/pact/provider/rspec/json_formatter.rb b/lib/pact/provider/rspec/json_formatter.rb new file mode 100644 index 00000000..e774cfdc --- /dev/null +++ b/lib/pact/provider/rspec/json_formatter.rb @@ -0,0 +1,100 @@ +require 'rspec/core/formatters/json_formatter' + +module Pact + module Provider + module RSpec + class JsonFormatter < ::RSpec::Core::Formatters::JsonFormatter + ::RSpec::Core::Formatters.register self, :message, :dump_summary, :dump_profile, :stop, :seed, :close + + def dump_summary(summary) + super(create_custom_summary(summary)) + output_hash[:summary][:pacts] = pacts(summary) + end + + def format_example(example) + { + :id => example.id, + :interaction_index => example.metadata[:pact_interaction].index, + :description => example.description, + :full_description => example.full_description, + :status => calculate_status(example), + :file_path => example.metadata[:file_path], + :line_number => example.metadata[:line_number], + :run_time => example.execution_result.run_time, + :mismatches => extract_differences(example), + :pact_url => example.metadata[:pact_uri].uri + } + end + + def stop(notification) + output_hash[:examples] = notification.examples.map do |example| + format_example(example).tap do |hash| + e = example.exception + if e + hash[:exception] = { + class: e.class.name, + message: e.message, + } + # No point providing a backtrace for a mismatch, too much noise + if !e.is_a?(::RSpec::Expectations::ExpectationNotMetError) + hash[:exception][:backtrace] + end + end + end + end + end + + def calculate_status(example) + if example.execution_result.status == :failed && example.metadata[:pact_ignore_failures] + 'pending' + else + example.execution_result.status.to_s + end + end + + # There will most likely be only one pact associated with this RSpec execution, because + # the most likely user of this formatter is the Go implementation that parses the JSON + # and builds Go tests from them. + # If the JSON formatter is used by someone else and they have multiple pacts, all the notices + # for the pacts will be mushed together in one collection, so it will be hard to know which notice + # belongs to which pact. + def pacts(summary) + unique_pact_metadatas(summary).collect do | example_metadata | + pact_uri = example_metadata[:pact_uri] + notices = (pact_uri.metadata[:notices] && pact_uri.metadata[:notices].before_verification_notices) || [] + { + notices: notices, + url: pact_uri.uri, + consumer_name: example_metadata[:pact_consumer_contract].consumer.name, + provider_name: example_metadata[:pact_consumer_contract].provider.name, + short_description: pact_uri.metadata[:short_description] + } + end + end + + def unique_pact_metadatas(summary) + summary.examples.collect(&:metadata).group_by{ | metadata | metadata[:pact_uri].uri }.values.collect(&:first) + end + + def create_custom_summary(summary) + ::RSpec::Core::Notifications::SummaryNotification.new( + summary.duration, + summary.examples, + summary.examples.select{ | example | example.execution_result.status == :failed && !example.metadata[:pact_ignore_failures] }, + summary.examples.select{ | example | example.execution_result.status == :failed && example.metadata[:pact_ignore_failures] }, + summary.load_time, + summary.errors_outside_of_examples_count + ) + end + + def extract_differences(example) + if example.metadata[:pact_diff] + Pact::Matchers::ExtractDiffMessages.call(example.metadata[:pact_diff]).to_a + else + [] + end + end + end + end + end +end diff --git a/lib/pact/provider/rspec/matchers.rb b/lib/pact/provider/rspec/matchers.rb index 23b12b03..6b082693 100644 --- a/lib/pact/provider/rspec/matchers.rb +++ b/lib/pact/provider/rspec/matchers.rb @@ -8,7 +8,7 @@ module Pact module RSpec module Matchers module RSpec2Delegator - # For backwards compatiblity with rspec-2 + # For backwards compatibility with rspec-2 def method_missing(method, *args, &block) if method_name == :failure_message_for_should failure_message method, *args, &block diff --git a/lib/pact/provider/rspec/pact_broker_formatter.rb b/lib/pact/provider/rspec/pact_broker_formatter.rb index c2659ddd..e655b8fd 100644 --- a/lib/pact/provider/rspec/pact_broker_formatter.rb +++ b/lib/pact/provider/rspec/pact_broker_formatter.rb @@ -25,7 +25,7 @@ def stop(notification) end def close(_notification) - Pact::Provider::VerificationResults::PublishAll.call(Pact.provider_world.pact_sources, output_hash) + Pact::Provider::VerificationResults::PublishAll.call(Pact.provider_world.pact_sources, output_hash, { verbose: Pact.provider_world.verbose }) end private @@ -66,7 +66,7 @@ def format_example(example) if example.metadata[:pact_diff] hash[:differences] = Pact::Matchers::ExtractDiffMessages.call(example.metadata[:pact_diff]) .to_a - .collect{ | description | {description: description} } + .collect{ | description | { description: description } } end end end diff --git a/lib/pact/provider/test_methods.rb b/lib/pact/provider/test_methods.rb index 470b2157..f27466af 100644 --- a/lib/pact/provider/test_methods.rb +++ b/lib/pact/provider/test_methods.rb @@ -21,7 +21,11 @@ def replay_interaction interaction, request_customizer = nil logger.info "Sending #{request.method.upcase} request to path: \"#{request.path}\" with headers: #{request.headers}, see debug logs for body" logger.debug "body :#{request.body}" - response = self.send(request.method.downcase, *args) + response = if self.respond_to?(:custom_request) + self.custom_request(request.method.upcase, *args) + else + self.send(request.method.downcase, *args) + end logger.info "Received response with status: #{response.status}, headers: #{response.headers}, see debug logs for body" logger.debug "body: #{response.body}" end @@ -29,7 +33,9 @@ def replay_interaction interaction, request_customizer = nil def parse_body_from_response rack_response case rack_response.headers['Content-Type'] when /json/ - JSON.load(rack_response.body) + # For https://github.com/pact-foundation/pact-net/issues/237 + # Only required for the pact-ruby-standalone ¯\_(ツ)_/¯ + JSON.load("[#{rack_response.body}]").first else rack_response.body end @@ -46,7 +52,7 @@ def set_up_provider_states provider_states, consumer, options = {} def tear_down_provider_states provider_states, consumer, options = {} # If there are no provider state, execute with an nil state to ensure global and base states are executed Pact.configuration.provider_state_tear_down.call(nil, consumer, options) if provider_states.nil? || provider_states.empty? - provider_states.reverse.each do | provider_state | + provider_states.reverse_each do | provider_state | Pact.configuration.provider_state_tear_down.call(provider_state.name, consumer, options.merge(params: provider_state.params)) end end diff --git a/lib/pact/provider/verification_results/create.rb b/lib/pact/provider/verification_results/create.rb index 2ae5ced2..bf296729 100644 --- a/lib/pact/provider/verification_results/create.rb +++ b/lib/pact/provider/verification_results/create.rb @@ -28,7 +28,23 @@ def any_failures? end def publishable? - executed_interactions_count == all_interactions_count && all_interactions_count > 0 + if defined?(@publishable) + @publishable + else + @publishable = pact_source.consumer_contract.interactions.all? do | interaction | + examples_for_pact_uri.any?{ |e| example_is_for_interaction?(e, interaction) } + end && examples_for_pact_uri.count > 0 + end + end + + def example_is_for_interaction?(example, interaction) + # Use the Pact Broker id if supported + if interaction._id + example[:pact_interaction]._id == interaction._id + else + # fall back to object equality (based on the field values of the interaction) + example[:pact_interaction] == interaction + end end def examples_for_pact_uri @@ -39,18 +55,6 @@ def count_failures_for_pact_uri examples_for_pact_uri.count{ |e| e[:status] != 'passed' } end - def executed_interactions_count - examples_for_pact_uri - .collect { |e| e[:pact_interaction].object_id } - .uniq - .count - end - - def all_interactions_count - interactions = (pact_source.pact_hash['interactions'] || pact_source.pact_hash['messages']) - interactions ? interactions.count : 0 - end - def test_results_hash_for_pact_uri { tests: examples_for_pact_uri.collect{ |e| clean_example(e) }, diff --git a/lib/pact/provider/verification_results/publish.rb b/lib/pact/provider/verification_results/publish.rb index f0f91c79..59bb47e5 100644 --- a/lib/pact/provider/verification_results/publish.rb +++ b/lib/pact/provider/verification_results/publish.rb @@ -16,29 +16,28 @@ class Publish PUBLISH_RELATION = 'pb:publish-verification-results'.freeze PROVIDER_RELATION = 'pb:provider'.freeze VERSION_TAG_RELATION = 'pb:version-tag'.freeze + BRANCH_VERSION_RELATION = 'pb:branch-version'.freeze - def self.call pact_source, verification_result - new(pact_source, verification_result).call + def self.call pact_source, verification_result, options = {} + new(pact_source, verification_result, options).call end - def initialize pact_source, verification_result + def initialize pact_source, verification_result, options = {} @pact_source = pact_source @verification_result = verification_result - - http_client_options = {} - if pact_source.uri.basic_auth? - http_client_options[:username] = pact_source.uri.username - http_client_options[:password] = pact_source.uri.password - end - - @http_client = Pact::Hal::HttpClient.new(http_client_options) + http_client_options = pact_source.uri.options.reject{ |k, v| ![:username, :password, :token].include?(k) } + @http_client = Pact::Hal::HttpClient.new(http_client_options.merge(verbose: options[:verbose])) @pact_entity = Pact::Hal::Entity.new(pact_source.uri, pact_source.pact_hash, http_client) end def call if can_publish_verification_results? + create_branch_version_if_configured tag_versions_if_configured publish_verification_results + true + else + false end end @@ -75,8 +74,28 @@ def tag_versions_if_configured end end + def create_branch_version_if_configured + if Pact.configuration.provider.branch + branch_version_link = provider_entity._link(BRANCH_VERSION_RELATION) + if branch_version_link + version_number = Pact.configuration.provider.application_version + branch = Pact.configuration.provider.branch + + Pact.configuration.output_stream.puts "INFO: Creating #{provider_name} version #{version_number} with branch \"#{branch}\"" + branch_entity = branch_version_link.expand( + version: version_number, + branch: branch + ).put + unless branch_entity.success? + raise PublicationError.new("Error returned from tagging request: status=#{branch_entity.response.code} body=#{branch_entity.response.body}") + end + else + raise PublicationError.new("This version of the Pact Broker does not support version branches. Please update to version 2.58.0 or later.") + end + end + end + def tag_versions - provider_entity = pact_entity.get(PROVIDER_RELATION) tag_link = provider_entity._link(VERSION_TAG_RELATION) || hacky_tag_url(provider_entity) provider_application_version = Pact.configuration.provider.application_version @@ -84,7 +103,7 @@ def tag_versions Pact.configuration.output_stream.puts "INFO: Tagging version #{provider_application_version} of #{provider_name} as #{tag.inspect}" tag_entity = tag_link.expand(version: provider_application_version, tag: tag).put unless tag_entity.success? - raise PublicationError.new("Error returned from tagging request #{tag_entity.response.code} #{tag_entity.response.body}") + raise PublicationError.new("Error returned from tagging request: status=#{tag_entity.response.code} body=#{tag_entity.response.body}") end end end @@ -114,6 +133,10 @@ def consumer_name def provider_name pact_source.pact_hash['provider']['name'] end + + def provider_entity + @provider_entity ||= pact_entity.get(PROVIDER_RELATION) + end end end end diff --git a/lib/pact/provider/verification_results/publish_all.rb b/lib/pact/provider/verification_results/publish_all.rb index 61891e1d..94faab99 100644 --- a/lib/pact/provider/verification_results/publish_all.rb +++ b/lib/pact/provider/verification_results/publish_all.rb @@ -6,18 +6,24 @@ module Provider module VerificationResults class PublishAll - def self.call pact_sources, test_results_hash - new(pact_sources, test_results_hash).call + def self.call pact_sources, test_results_hash, options = {} + new(pact_sources, test_results_hash, options).call end - def initialize pact_sources, test_results_hash + def initialize pact_sources, test_results_hash, options = {} @pact_sources = pact_sources @test_results_hash = test_results_hash + @options = options end def call verification_results.collect do | (pact_source, verification_result) | - Publish.call(pact_source, verification_result) + published = false + begin + published = Publish.call(pact_source, verification_result, { verbose: options[:verbose] }) + ensure + print_after_verification_notices(pact_source, verification_result, published) + end end end @@ -29,7 +35,15 @@ def verification_results end end - attr_reader :pact_sources, :test_results_hash + def print_after_verification_notices(pact_source, verification_result, published) + if pact_source.uri.metadata[:notices] + pact_source.uri.metadata[:notices].after_verification_notices_text(verification_result.success, published).each do | text | + Pact.configuration.output_stream.puts "DEBUG: #{text}" + end + end + end + + attr_reader :pact_sources, :test_results_hash, :options end end end diff --git a/lib/pact/provider/verification_results/verification_result.rb b/lib/pact/provider/verification_results/verification_result.rb index a39f8ade..4e746cb9 100644 --- a/lib/pact/provider/verification_results/verification_result.rb +++ b/lib/pact/provider/verification_results/verification_result.rb @@ -4,6 +4,7 @@ module Pact module Provider module VerificationResults class VerificationResult + attr_reader :success, :provider_application_version, :test_results_hash def initialize publishable, success, provider_application_version, test_results_hash @publishable = publishable @@ -31,10 +32,6 @@ def to_json(options = {}) def to_s "[success: #{success}, providerApplicationVersion: #{provider_application_version}]" end - - private - - attr_reader :success, :provider_application_version, :test_results_hash end end end diff --git a/lib/pact/provider/world.rb b/lib/pact/provider/world.rb index 73b0effd..f6168d36 100644 --- a/lib/pact/provider/world.rb +++ b/lib/pact/provider/world.rb @@ -14,7 +14,7 @@ def self.clear_provider_world module Provider class World - attr_accessor :pact_sources + attr_accessor :pact_sources, :failed_examples, :verbose def provider_states @provider_states_proxy ||= Pact::Provider::State::ProviderStateProxy.new @@ -43,7 +43,7 @@ def pact_uri_sources end def pact_uris_from_pact_uri_sources - pact_uri_sources.collect{| pact_uri_source| pact_uri_source.call }.flatten + pact_uri_sources.collect(&:call).flatten end end end diff --git a/lib/pact/tasks/task_helper.rb b/lib/pact/tasks/task_helper.rb index 1daaab0a..3f533d54 100644 --- a/lib/pact/tasks/task_helper.rb +++ b/lib/pact/tasks/task_helper.rb @@ -7,6 +7,7 @@ module Pact module TaskHelper PACT_INTERACTION_RERUN_COMMAND = "bundle exec rake pact:verify:at[] PACT_DESCRIPTION=\"\" PACT_PROVIDER_STATE=\"\"" + PACT_INTERACTION_RERUN_COMMAND_FOR_BROKER = "bundle exec rake pact:verify:at[] PACT_BROKER_INTERACTION_ID=\"\"" extend self @@ -34,6 +35,8 @@ def verify_command pact_helper, pact_uri, rspec_opts, verification_opts command_parts << "--backtrace" if ENV['BACKTRACE'] == 'true' command_parts << "--description #{Shellwords.escape(ENV['PACT_DESCRIPTION'])}" if ENV['PACT_DESCRIPTION'] command_parts << "--provider-state #{Shellwords.escape(ENV['PACT_PROVIDER_STATE'])}" if ENV['PACT_PROVIDER_STATE'] + command_parts << "--pact-broker-interaction-id #{Shellwords.escape(ENV['PACT_BROKER_INTERACTION_ID'])}" if ENV['PACT_BROKER_INTERACTION_ID'] + command_parts << "--interaction-index #{Shellwords.escape(ENV['PACT_INTERACTION_INDEX'])}" if ENV['PACT_INTERACTION_INDEX'] command_parts.flatten.join(" ") end @@ -41,7 +44,9 @@ def execute_cmd command Pact.configuration.output_stream.puts command temporarily_set_env_var 'PACT_EXECUTING_LANGUAGE', 'ruby' do temporarily_set_env_var 'PACT_INTERACTION_RERUN_COMMAND', PACT_INTERACTION_RERUN_COMMAND do - exit_status = system(command) ? 0 : 1 + temporarily_set_env_var 'PACT_INTERACTION_RERUN_COMMAND_FOR_BROKER', PACT_INTERACTION_RERUN_COMMAND_FOR_BROKER do + exit_status = system(command) ? 0 : 1 + end end end end diff --git a/lib/pact/tasks/verification_task.rb b/lib/pact/tasks/verification_task.rb index 644abf4d..bac650ab 100644 --- a/lib/pact/tasks/verification_task.rb +++ b/lib/pact/tasks/verification_task.rb @@ -31,6 +31,7 @@ class VerificationTask < ::Rake::TaskLib attr_reader :pact_spec_configs attr_accessor :rspec_opts attr_accessor :ignore_failures + attr_accessor :_pact_helper def initialize(name) @rspec_opts = nil @@ -41,6 +42,10 @@ def initialize(name) rake_task end + def pact_helper(pact_helper) + @pact_spec_configs << { pact_helper: pact_helper } + end + def uri(uri, options = {}) @pact_spec_configs << {uri: uri, pact_helper: options[:pact_helper]} end @@ -82,6 +87,7 @@ def rake_task Pact::TaskHelper.handle_verification_failure do exit_statuses.count{ | status | status != 0 } end + end end end diff --git a/lib/pact/utils/string.rb b/lib/pact/utils/string.rb new file mode 100644 index 00000000..ddead806 --- /dev/null +++ b/lib/pact/utils/string.rb @@ -0,0 +1,35 @@ +# Can't use refinements because of Travelling Ruby + +module Pact + module Utils + module String + + extend self + + # ripped from rubyworks/facets, thank you + def camelcase(string, *separators) + case separators.first + when Symbol, TrueClass, FalseClass, NilClass + first_letter = separators.shift + end + + separators = ['_', '\s'] if separators.empty? + + str = string.dup + + separators.each do |s| + str = str.gsub(/(?:#{s}+)([a-z])/){ $1.upcase } + end + + case first_letter + when :upper, true + str = str.gsub(/(\A|\s)([a-z])/){ $1 + $2.upcase } + when :lower, false + str = str.gsub(/(\A|\s)([A-Z])/){ $1 + $2.downcase } + end + + str + end + end + end +end \ No newline at end of file diff --git a/lib/pact/version.rb b/lib/pact/version.rb index 13fbfa62..7d65286b 100644 --- a/lib/pact/version.rb +++ b/lib/pact/version.rb @@ -1,4 +1,4 @@ # Remember to bump pact-provider-proxy when this changes major version module Pact - VERSION = "1.37.0" + VERSION = "1.61.0" end diff --git a/pact.gemspec b/pact.gemspec index cdb486fd..b49fd3ce 100644 --- a/pact.gemspec +++ b/pact.gemspec @@ -1,5 +1,4 @@ -# -*- encoding: utf-8 -*- -lib = File.expand_path('../lib', __FILE__) +lib = File.expand_path("lib", __dir__) $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) require 'pact/version' @@ -15,30 +14,35 @@ Gem::Specification.new do |gem| gem.required_ruby_version = '>= 2.0' gem.files = `git ls-files bin lib pact.gemspec CHANGELOG.md LICENSE.txt`.split($/) - gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) } + gem.executables = gem.files.grep(%r{^bin/}).map { |f| File.basename(f) } gem.test_files = gem.files.grep(%r{^(test|spec|features)/}) gem.require_paths = ["lib"] gem.license = 'MIT' - gem.add_runtime_dependency 'randexp', '~> 0.1.7' - gem.add_runtime_dependency 'rspec', '>=2.14' - gem.add_runtime_dependency 'rack-test', '~> 0.6', '>= 0.6.3' - gem.add_runtime_dependency 'thor' - gem.add_runtime_dependency 'json','> 1.8.5' - gem.add_runtime_dependency 'webrick' + gem.metadata = { + 'changelog_uri' => 'https://github.com/pact-foundation/pact-ruby/blob/master/CHANGELOG.md', + 'source_code_uri' => 'https://github.com/pact-foundation/pact-ruby', + 'bug_tracker_uri' => 'https://github.com/pact-foundation/pact-ruby/issues', + 'documentation_uri' => 'https://github.com/pact-foundation/pact-ruby/blob/master/README.md' + } + + gem.add_runtime_dependency 'rspec', '~> 3.0' + gem.add_runtime_dependency 'rack-test', '>= 0.6.3', '< 2.0.0' + gem.add_runtime_dependency 'thor', '>= 0.20', '< 2.0' + gem.add_runtime_dependency 'webrick', '~> 1.3' gem.add_runtime_dependency 'term-ansicolor', '~> 1.0' - gem.add_runtime_dependency 'pact-support', '~> 1.8' - gem.add_runtime_dependency 'pact-mock_service', '~> 2.10' + gem.add_runtime_dependency 'pact-support', '~> 1.16', '>= 1.16.9' + gem.add_runtime_dependency 'pact-mock_service', '~> 3.0', '>= 3.3.1' - gem.add_development_dependency 'rake', '~> 10.0.3' + gem.add_development_dependency 'rake', '~> 13.0' gem.add_development_dependency 'webmock', '~> 3.0' - #gem.add_development_dependency 'pry-byebug' gem.add_development_dependency 'fakefs', '0.5' # 0.6.0 blows up gem.add_development_dependency 'hashie', '~> 2.0' - gem.add_development_dependency 'activesupport' + gem.add_development_dependency 'activesupport', '~> 5.2' gem.add_development_dependency 'faraday', '~> 0.13' - gem.add_development_dependency 'appraisal', '~> 2.2' gem.add_development_dependency 'conventional-changelog', '~> 1.3' gem.add_development_dependency 'bump', '~> 0.5' + gem.add_development_dependency 'pact-message', '~> 0.8' + gem.add_development_dependency 'rspec-its', '~> 1.3' end diff --git a/script/release.sh b/script/release.sh index 8b19ca25..0fee7506 100755 --- a/script/release.sh +++ b/script/release.sh @@ -1,9 +1,9 @@ #!/bin/bash set -e - +unset X_PACT_DEVELOPMENT bundle exec bump ${1:-minor} --no-commit -bundle exec appraisal update bundle exec rake generate_changelog -git add CHANGELOG.md lib/pact/version.rb gemfiles +git add CHANGELOG.md lib/pact/version.rb git commit -m "chore(release): version $(ruby -r ./lib/pact/version.rb -e "puts Pact::VERSION")" && git push -bundle exec rake release +bundle exec rake tag_for_release +echo "Releasing from https://travis-ci.org/pact-foundation/pact-ruby" diff --git a/script/trigger-release.sh b/script/trigger-release.sh new file mode 100755 index 00000000..086ed39c --- /dev/null +++ b/script/trigger-release.sh @@ -0,0 +1,30 @@ +#!/bin/sh +set -x +# Script to trigger release of gem via the pact-foundation/release-gem action +# Requires a Github API token with repo scope stored in the +# environment variable GITHUB_ACCESS_TOKEN_FOR_PF_RELEASES + +: "${GITHUB_ACCESS_TOKEN_FOR_PF_RELEASES:?Please set environment variable GITHUB_ACCESS_TOKEN_FOR_PF_RELEASES}" + +if [ -n "$1" ]; then + increment="\"${1}\"" +else + increment="null" +fi + +repository_slug=$(git remote get-url $(git remote show) | cut -d':' -f2 | sed 's/\.git//') + +output=$(curl -v https://api.github.com/repos/${repository_slug}/dispatches \ + -H 'Accept: application/vnd.github.everest-preview+json' \ + -H "Authorization: Bearer $GITHUB_ACCESS_TOKEN_FOR_PF_RELEASES" \ + -d "{\"event_type\": \"release-triggered\", \"client_payload\": {\"increment\": ${increment}}}" 2>&1) + +if ! echo "${output}" | grep "HTTP\/.* 204" > /dev/null; then + echo "$output" | sed "s/${GITHUB_ACCESS_TOKEN_FOR_PF_RELEASES}/********/g" + echo "Failed to trigger release" + exit 1 +else + echo "Release workflow triggered" +fi + +echo "See https://github.com/${repository_slug}/actions?query=workflow%3A%22Release+gem%22" diff --git a/spec/features/consumer_with_file_upload_spec.rb b/spec/features/consumer_with_file_upload_spec.rb index 41687003..c8aa174b 100644 --- a/spec/features/consumer_with_file_upload_spec.rb +++ b/spec/features/consumer_with_file_upload_spec.rb @@ -32,12 +32,11 @@ let(:do_request) { connection.post { |req| req.body = payload } } - describe "when the content matches" do - - let(:body) do - "-------------RubyMultipartPost-05e76cbc2adb42ac40344eb9b35e98bc\r\nContent-Disposition: form-data; name=\"file\"; filename=\"text.txt\"\r\nContent-Length: 14\r\nContent-Type: text/plain\r\nContent-Transfer-Encoding: binary\r\n\r\nThis is a file\r\n-------------RubyMultipartPost-05e76cbc2adb42ac40344eb9b35e98bc--\r\n\r\n" - end + let(:body) do + "-------------RubyMultipartPost-05e76cbc2adb42ac40344eb9b35e98bc\r\nContent-Disposition: form-data; name=\"file\"; filename=\"text.txt\"\r\nContent-Length: 14\r\nContent-Type: text/plain\r\nContent-Transfer-Encoding: binary\r\n\r\n#{File.read(file_to_upload)}\r\n-------------RubyMultipartPost-05e76cbc2adb42ac40344eb9b35e98bc--\r\n" + end + describe "when the content matches" do it "returns the mocked response and verification passes" do file_upload_service. upon_receiving("a request to upload a file").with({ @@ -60,22 +59,15 @@ end describe "when the content does not match" do - - let(:body) do - "-------------RubyMultipartPost-05e76cbc2adb42ac40344eb9b35e98bc\r\nContent-Disposition: form-data; name=\"file\"; filename=\"TEXT.txt\"\r\nContent-Length: 14\r\nContent-Type: text/plain\r\nContent-Transfer-Encoding: binary\r\n\r\nThis is a file\r\n-------------RubyMultipartPost-05e76cbc2adb42ac40344eb9b35e98bc--\r\n\r\n" - end - it "the verification fails" do file_upload_service. upon_receiving("a request to upload another file").with({ method: :post, path: '/files', - query: "foo=bar", - body: body, + body: body.gsub('text.txt', 'wrong.txt'), headers: { "Content-Type" => Pact.term(/multipart\/form\-data/, "multipart/form-data; boundary=-----------RubyMultipartPost-05e76cbc2adb42ac40344eb9b35e98bc"), - "Content-Length" => Pact.like("299"), - "Missing" => "header" + "Content-Length" => Pact.like("299") } }). will_respond_with({ diff --git a/spec/features/foo_bar_spec.rb b/spec/features/foo_bar_spec.rb index 0ffd6882..ce424d45 100644 --- a/spec/features/foo_bar_spec.rb +++ b/spec/features/foo_bar_spec.rb @@ -17,6 +17,7 @@ has_pact_with "Bar" do mock_service :bar_service do pact_specification_version "2" + host "127.0.0.1" port 4638 end end diff --git a/spec/features/production_spec.rb b/spec/features/production_spec.rb index ca4ccef8..c2afde97 100644 --- a/spec/features/production_spec.rb +++ b/spec/features/production_spec.rb @@ -2,6 +2,7 @@ require 'pact/provider/rspec' require 'pact/consumer_contract' require 'features/provider_states/zebras' +require 'pact/provider/pact_source' module Pact::Provider @@ -81,7 +82,10 @@ def call(env) end end - honour_consumer_contract pact + pact_uri = Pact::Provider::PactURI.new("http://dummy-uri") + pact_source = Pact::Provider::PactSource.new(pact_uri) + + honour_consumer_contract pact, pact_uri: pact_uri, pact_source: pact_source end @@ -115,8 +119,10 @@ def call(env) end end + pact_uri = Pact::Provider::PactURI.new("http://dummy-uri") + pact_source = Pact::Provider::PactSource.new(pact_uri) - honour_consumer_contract consumer_contract + honour_consumer_contract consumer_contract, pact_uri: pact_uri, pact_source: pact_source end context "that is a string" do @@ -147,8 +153,10 @@ def call(env) end end + pact_uri = Pact::Provider::PactURI.new("http://dummy-uri") + pact_source = Pact::Provider::PactSource.new(pact_uri) - honour_consumer_contract consumer_contract + honour_consumer_contract consumer_contract, pact_uri: pact_uri, pact_source: pact_source end end diff --git a/spec/integration/publish_verification_spec.rb b/spec/integration/publish_verification_spec.rb index 0d3f9a4d..0f996b31 100644 --- a/spec/integration/publish_verification_spec.rb +++ b/spec/integration/publish_verification_spec.rb @@ -2,7 +2,6 @@ require 'pact/provider/pact_uri' describe "publishing verifications" do - before do allow(Pact.configuration).to receive(:provider).and_return(provider_configuration) allow($stdout).to receive(:puts) @@ -12,17 +11,24 @@ double('provider_configuration', application_version: '1.2.3', publish_verification_results?: true, + branch: nil, tags: []) end let(:pact_sources) do - [instance_double('Pact::Provider::PactSource', pact_hash: pact_hash, uri: pact_uri)] + [instance_double('Pact::Provider::PactSource', consumer_contract: consumer_contract, pact_hash: pact_hash, uri: pact_uri)] end let(:pact_uri) do - instance_double('Pact::Provider::PactURI', uri: 'pact.json', basic_auth?: false) + instance_double('Pact::Provider::PactURI', uri: 'pact.json', options: {}, metadata: metadata) end + let(:consumer_contract) { instance_double('Pact::ConsumerContract', interactions: [pact_interaction])} + let(:pact_interaction) { instance_double('Pact::Interaction', _id: "1") } + + let(:metadata) { { notices: notices} } + let(:notices) { instance_double('Pact::PactBroker::Notices', after_verification_notices_text: ['hello'] ) } + let(:pact_hash) do { 'interactions' => [{}], @@ -50,7 +56,8 @@ { testDescription: '1', status: 'passed', - pact_uri: pact_uri + pact_uri: pact_uri, + pact_interaction: pact_interaction } ] } @@ -59,7 +66,7 @@ subject { Pact::Provider::VerificationResults::PublishAll.call(pact_sources, test_results_hash) } let!(:request) do - stub_request(:post, 'http://publish').to_return(status: 200, body: created_verification_body) + stub_request(:post, 'http://publish').to_return(status: 200, headers: {'Content-Type' => 'application/hal+json'}, body: created_verification_body) end it "publishes the results" do diff --git a/spec/lib/pact/cli/spec_criteria_spec.rb b/spec/lib/pact/cli/spec_criteria_spec.rb index c9bcb214..9ba242fe 100644 --- a/spec/lib/pact/cli/spec_criteria_spec.rb +++ b/spec/lib/pact/cli/spec_criteria_spec.rb @@ -3,34 +3,41 @@ module Pact module Cli describe SpecCriteria do - describe "#spec_criteria" do let(:env_description) { "pact description set in ENV"} let(:env_provider_state) { "provider state set in ENV"} - let(:env_criteria){ {:description=>/#{env_description}/, :provider_state=>/#{env_provider_state}/} } + let(:env_pact_broker_interaction_id) { "interaction id set in ENV" } + let(:interaction_index) { 2 } + let(:env_criteria) do + { + :description=>/#{env_description}/, + :provider_state=>/#{env_provider_state}/, + :_id => env_pact_broker_interaction_id, + :index => interaction_index + } + end let(:defaults) { {:description => default_description, :provider_state => default_provider_state} } let(:subject) { Pact::App.new } context "when options are defined" do - before do - - allow(ENV).to receive(:[]) - allow(ENV).to receive(:[]).with("PACT_DESCRIPTION").and_return(env_description) - allow(ENV).to receive(:[]).with("PACT_PROVIDER_STATE").and_return(env_provider_state) + let(:options) do + { + description: env_description, + provider_state: env_provider_state, + pact_broker_interaction_id: env_pact_broker_interaction_id, + interaction_index: interaction_index + } end - let(:options) { {description: env_description, provider_state: env_provider_state} } - it "returns the env vars as regexes" do expect(Pact::Cli::SpecCriteria.call(options)).to eq(env_criteria) end end context "when ENV variables are not defined" do - let(:options) { {} } it "returns an empty hash" do @@ -39,8 +46,7 @@ module Cli end context "when provider state is an empty string" do - - let(:options) { {provider_state: ''} } + let(:options) { { provider_state: '' } } it "returns a nil provider state so that it matches a nil provider state on the interaction" do expect(Pact::Cli::SpecCriteria.call(options)[:provider_state]).to be_nil diff --git a/spec/lib/pact/consumer/consumer_contract_builder_spec.rb b/spec/lib/pact/consumer/consumer_contract_builder_spec.rb index 62e867b8..ebb5789a 100644 --- a/spec/lib/pact/consumer/consumer_contract_builder_spec.rb +++ b/spec/lib/pact/consumer/consumer_contract_builder_spec.rb @@ -16,6 +16,7 @@ module Consumer provider_name: provider_name, pactfile_write_mode: :overwrite, port: 2222, + host: 'localhost', pact_dir: pact_dir) end @@ -70,6 +71,7 @@ module Consumer pact_dir: './spec/pacts', consumer_name: consumer_name, provider_name: provider_name, + host: 'localhost', port: 1234 ) end diff --git a/spec/lib/pact/consumer/interaction_builder_spec.rb b/spec/lib/pact/consumer/interaction_builder_spec.rb index a16ab53c..0927da9e 100644 --- a/spec/lib/pact/consumer/interaction_builder_spec.rb +++ b/spec/lib/pact/consumer/interaction_builder_spec.rb @@ -88,6 +88,21 @@ module Consumer subject.will_respond_with response end end + + describe "without_writing_to_pact" do + it "sets the write_to_pact key to false on metadata" do + mock_metadata = {} + expect(interaction).to receive(:metadata).and_return(nil, mock_metadata) + + subject.without_writing_to_pact + + expect(mock_metadata).to eq({ write_to_pact: false }) + end + + it "returns itself" do + expect(subject.without_writing_to_pact).to be(subject) + end + end end end end diff --git a/spec/lib/pact/hal/authorization_header_redactor_spec.rb b/spec/lib/pact/hal/authorization_header_redactor_spec.rb new file mode 100644 index 00000000..1b43eb70 --- /dev/null +++ b/spec/lib/pact/hal/authorization_header_redactor_spec.rb @@ -0,0 +1,15 @@ +require 'pact/hal/authorization_header_redactor' + +module Pact + module Hal + describe AuthorizationHeaderRedactor do + let(:stream) { StringIO.new } + let(:stream_redactor) { AuthorizationHeaderRedactor.new(stream) } + + it "redacts the authorizaton header" do + stream_redactor << "\\r\\nAuthorization: Bearer TOKEN\\r\\n" + expect(stream.string).to eq "\\r\\nAuthorization: [redacted]\\r\\n" + end + end + end +end diff --git a/spec/lib/pact/hal/entity_spec.rb b/spec/lib/pact/hal/entity_spec.rb index 4eb4744d..9663565f 100644 --- a/spec/lib/pact/hal/entity_spec.rb +++ b/spec/lib/pact/hal/entity_spec.rb @@ -9,7 +9,7 @@ module Hal end let(:provider_response) do - double('response', body: provider_hash, success?: true) + double('response', body: provider_hash, success?: true, json?: true) end let(:provider_hash) do @@ -42,7 +42,7 @@ module Hal let(:post_provider) { entity.post("pb:provider", {'some' => 'data'} ) } it "executes an http request" do - expect(http_client).to receive(:post).with("http://provider", '{"some":"data"}', {}) + expect(http_client).to receive(:post).with("http://provider", '{"some":"data"}', {"Accept" => "application/hal+json", "Content-Type" => "application/json"}) post_provider end @@ -54,7 +54,7 @@ module Hal let(:post_provider) { entity._link("pb:version-tag").expand(version: "1", tag: "prod").post({'some' => 'data'} ) } it "posts to the expanded URL" do - expect(http_client).to receive(:post).with("http://provider/version/1/tag/prod", '{"some":"data"}', {}) + expect(http_client).to receive(:post).with("http://provider/version/1/tag/prod", '{"some":"data"}', {"Accept" => "application/hal+json", "Content-Type" => "application/json"}) post_provider end end diff --git a/spec/lib/pact/hal/http_client_spec.rb b/spec/lib/pact/hal/http_client_spec.rb index de45d6d3..f79f03cb 100644 --- a/spec/lib/pact/hal/http_client_spec.rb +++ b/spec/lib/pact/hal/http_client_spec.rb @@ -3,18 +3,17 @@ module Pact module Hal describe HttpClient do - before do allow(Retry).to receive(:until_true) { |&block| block.call } end - subject { HttpClient.new(username: 'foo', password: 'bar' ) } + subject { HttpClient.new(username: 'foo', password: 'bar') } describe "get" do let!(:request) do stub_request(:get, "http://example.org/"). with( headers: { - 'Accept'=>'application/hal+json', + 'Accept'=>'*/*', 'Authorization'=>'Basic Zm9vOmJhcg==' }). to_return(status: 200, body: response_body, headers: {'Content-Type' => 'application/json'}) @@ -40,8 +39,38 @@ module Hal do_get expect(request).to have_been_made end + + context "when there are existing params on the URL" do + let!(:request) do + stub_request(:get, "http://example.org/?foo=hello+world&bar=wiffle&a=b"). + to_return(status: 200) + end + + let(:do_get) { subject.get('http://example.org?foo=bar&a=b', { 'foo' => 'hello world', 'bar' => 'wiffle' }) } + + it "merges them in" do + do_get + expect(request).to have_been_made + end + end end + context "with broker token set" do + let!(:request) do + stub_request(:any, /.*/). + with( headers: { + 'Authorization'=>'Bearer mytoken123' + }). + to_return(status: 200, body: response_body, headers: {'Content-Type' => 'application/json'}) + end + + subject { HttpClient.new(token: 'mytoken123') } + + it "sets a bearer authorization header" do + do_get + expect(request).to have_been_made + end + end it "retries on failure" do expect(Retry).to receive(:until_true) @@ -57,9 +86,8 @@ module Hal let!(:request) do stub_request(:post, "http://example.org/"). with( headers: { - 'Accept'=>'application/hal+json', - 'Authorization'=>'Basic Zm9vOmJhcg==', - 'Content-Type'=>'application/json' + 'Accept'=>'*/*', + 'Authorization'=>'Basic Zm9vOmJhcg==' }, body: request_body). to_return(status: 200, body: response_body, headers: {'Content-Type' => 'application/json'}) diff --git a/spec/lib/pact/hal/link_spec.rb b/spec/lib/pact/hal/link_spec.rb index 3b560904..441153f1 100644 --- a/spec/lib/pact/hal/link_spec.rb +++ b/spec/lib/pact/hal/link_spec.rb @@ -10,7 +10,7 @@ module Hal end let(:response) do - instance_double('Pact::Hal::HttpClient::Response', success?: success, body: response_body, raw_body: response_body.to_json) + instance_double('Pact::Hal::HttpClient::Response', success?: success, body: response_body, raw_body: response_body.to_json, json?: true) end let(:success) { true } @@ -19,9 +19,10 @@ module Hal instance_double('Pact::Hal::Entity') end + let(:href) { 'http://foo/{bar}' } let(:attrs) do { - 'href' => 'http://foo/{bar}', + 'href' => href, 'title' => 'title', method: :post } @@ -78,7 +79,7 @@ module Hal let(:do_get) { subject.get({ 'foo' => 'bar' }) } it "executes an HTTP Get request" do - expect(http_client).to receive(:get).with('http://foo/{bar}', { 'foo' => 'bar' }, {}) + expect(http_client).to receive(:get).with('http://foo/{bar}', { 'foo' => 'bar' }, { 'Accept' => 'application/hal+json' }) do_get end end @@ -88,12 +89,20 @@ module Hal context "with custom headers" do it "executes an HTTP Post request with the custom headers" do - expect(http_client).to receive(:post).with('http://foo/{bar}', '{"foo":"bar"}', { 'Accept' => 'foo' }) + expect(http_client).to receive(:post).with('http://foo/{bar}', '{"foo":"bar"}', { 'Accept' => 'foo', 'Content-Type' => 'application/json' }) do_post end end end + describe "#with_query" do + let(:href) { "http://example.org?a=1&b=2" } + + it "returns a link with the new query merged into the existing query" do + expect(subject.with_query("a" => "5", "c" => "3").href).to eq "http://example.org?a=5&b=2&c=3" + end + end + describe "#expand" do it "returns a duplicate Link with the expanded href" do expect(subject.expand(bar: 'wiffle').href).to eq "http://foo/wiffle" diff --git a/spec/lib/pact/pact_broker/fetch_pending_pacts_spec.rb b/spec/lib/pact/pact_broker/fetch_pact_uris_for_verification_spec.rb similarity index 51% rename from spec/lib/pact/pact_broker/fetch_pending_pacts_spec.rb rename to spec/lib/pact/pact_broker/fetch_pact_uris_for_verification_spec.rb index ea16ae4b..e0690663 100644 --- a/spec/lib/pact/pact_broker/fetch_pending_pacts_spec.rb +++ b/spec/lib/pact/pact_broker/fetch_pact_uris_for_verification_spec.rb @@ -1,8 +1,8 @@ -require 'pact/pact_broker/fetch_pending_pacts' +require 'pact/pact_broker/fetch_pact_uris_for_verification' module Pact module PactBroker - describe FetchPendingPacts do + describe FetchPactURIsForVerification do describe "call" do before do allow(Pact.configuration).to receive(:output_stream).and_return(double('output stream').as_null_object) @@ -11,7 +11,11 @@ module PactBroker let(:provider) { "Foo"} let(:broker_base_url) { "http://broker.org" } let(:http_client_options) { {} } - subject { FetchPendingPacts.call(provider, broker_base_url, http_client_options)} + let(:consumer_version_selectors) { [{ tag: "cmaster", latest: true, fallbackTag: 'blah' }] } + let(:provider_version_branch) { "pbranch" } + let(:provider_version_tags) { ["pmaster"] } + + subject { FetchPactURIsForVerification.call(provider, consumer_version_selectors, provider_version_branch, provider_version_tags, broker_base_url, http_client_options)} context "when there is an error retrieving the index resource" do before do @@ -32,8 +36,20 @@ module PactBroker end end - context "when the pb:pending-provider-pacts relation does not exist" do + context "when a single tag is provided instead of an array" do + + let(:provider_version_tags) { "pmaster" } + + subject { FetchPactURIsForVerification.new(provider, consumer_version_selectors, provider_version_branch, provider_version_tags, broker_base_url, http_client_options)} + + it "wraps an array around it" do + expect(subject.provider_version_tags).to eq ["pmaster"] + end + end + + context "when the beta:provider-pacts-for-verification relation does not exist" do before do + allow(FetchPacts).to receive(:call) stub_request(:get, "http://broker.org/").to_return(status: 200, body: response_body, headers: response_headers) end @@ -44,8 +60,9 @@ module PactBroker }.to_json end - it "returns an empty list" do - expect(subject).to eq [] + it "calls the old fetch pacts code" do + expect(FetchPacts).to receive(:call).with(provider, [{ name: "cmaster", all: false, fallback: "blah" }], broker_base_url, http_client_options) + subject end end end diff --git a/spec/lib/pact/pact_broker/fetch_pacts_spec.rb b/spec/lib/pact/pact_broker/fetch_pacts_spec.rb index 096960d0..50d8fc31 100644 --- a/spec/lib/pact/pact_broker/fetch_pacts_spec.rb +++ b/spec/lib/pact/pact_broker/fetch_pacts_spec.rb @@ -33,7 +33,7 @@ module PactBroker context "when there is a HAL relation missing" do before do - stub_request(:get, "http://broker.org/").to_return(status: 200, body: {"_links" => {} }.to_json, headers: {}) + stub_request(:get, "http://broker.org/").to_return(status: 200, body: {"_links" => {} }.to_json, headers: {"Content-Type" => "application/hal+json"}) end it "raises a Pact::Error" do diff --git a/spec/lib/pact/pact_broker/notices_spec.rb b/spec/lib/pact/pact_broker/notices_spec.rb new file mode 100644 index 00000000..31a69e86 --- /dev/null +++ b/spec/lib/pact/pact_broker/notices_spec.rb @@ -0,0 +1,58 @@ +require 'pact/pact_broker/notices' + +module Pact + module PactBroker + describe Notices do + + let(:notice_hashes) do + [ + { text: "foo", when: "before_verification" } + ] + end + + subject(:notices) { Notices.new(notice_hashes) } + + it "behaves like an array" do + expect(subject.size).to eq notice_hashes.size + end + + describe "before_verification_notices" do + let(:notice_hashes) do + [ + { text: "foo", when: "before_verification" }, + { text: "bar", when: "blah" }, + ] + end + + its(:before_verification_notices_text) { is_expected.to eq [ "foo" ] } + end + + describe "after_verification_notices_text" do + let(:notice_hashes) do + [ + { text: "foo", when: "after_verification:success_false_published_true" }, + { text: "bar", when: "blah" }, + ] + end + + subject { notices.after_verification_notices_text(false, true) } + + it { is_expected.to eq [ "foo" ] } + end + + describe "after_verification_notices" do + let(:notice_hashes) do + [ + { text: "meep", when: "after_verification" }, + { text: "foo", when: "after_verification:success_false_published_true" }, + { text: "bar", when: "blah" }, + ] + end + + subject { notices.after_verification_notices(false, true) } + + it { is_expected.to eq [{ text: "meep", when: "after_verification" }, { text: "foo", when: "after_verification" }] } + end + end + end +end diff --git a/spec/lib/pact/pact_broker/pact_selection_description_spec.rb b/spec/lib/pact/pact_broker/pact_selection_description_spec.rb new file mode 100644 index 00000000..cb93143c --- /dev/null +++ b/spec/lib/pact/pact_broker/pact_selection_description_spec.rb @@ -0,0 +1,91 @@ +require 'pact/pact_broker/pact_selection_description' + +module Pact + module PactBroker + describe PactSelectionDescription do + include PactSelectionDescription + + describe "#pact_selection_description" do + let(:provider) { "Bar" } + let(:consumer_version_selectors) { [{ tag: "cmaster", latest: true, fallbackTag: "master" }, { tag: "prod" }] } + let(:options) do + { + include_wip_pacts_since: "2020-01-01" + } + end + let(:broker_base_url) { "http://broker" } + + subject { pact_selection_description(provider, consumer_version_selectors, options, broker_base_url) } + + it { is_expected.to eq "Fetching pacts for Bar from http://broker with the selection criteria: latest for tag cmaster (or master if not found), all for tag prod, work in progress pacts created after 2020-01-01" } + + describe "when consumer selector specifies a consumer name" do + let(:consumer_version_selectors) { [{ tag: "cmaster", latest: true, consumer: "Foo" }] } + + it { is_expected.to eq "Fetching pacts for Bar from http://broker with the selection criteria: latest for tag cmaster of Foo, work in progress pacts created after 2020-01-01" } + end + + describe "for branch" do + let(:consumer_version_selectors) { [{ branch: "feat/x", consumer: "Foo" }] } + + it { is_expected.to include "latest from branch feat/x of Foo" } + end + + describe "for main branch" do + let(:consumer_version_selectors) { [{ mainBranch: true, consumer: "Foo" }] } + + it { is_expected.to include "latest from main branch of Foo" } + end + + describe "for deployedOrReleased" do + let(:consumer_version_selectors) { [{ deployedOrReleased: true }] } + + it { is_expected.to include "currently deployed or released" } + end + + describe "for released in environment" do + let(:consumer_version_selectors) { [{ released: true, environment: "production" }] } + + it { is_expected.to include "currently released to production" } + end + + describe "for deployed in environment" do + let(:consumer_version_selectors) { [{ deployed: true, environment: "production" }] } + + it { is_expected.to include "currently deployed to production" } + end + + describe "for deployedOrReleased in environment" do + let(:consumer_version_selectors) { [{ deployedOrReleased: true, environment: "production" }] } + + it { is_expected.to include "currently deployed or released to production" } + end + + describe "in environment" do + let(:consumer_version_selectors) { [{ environment: "production" }] } + + it { is_expected.to include "in production" } + end + + describe "matching branch" do + let(:consumer_version_selectors) { [{ matchingBranch: true, consumer: "Foo" }] } + + it { is_expected.to include "matching current branch for Foo" } + end + + describe "matching tag" do + let(:consumer_version_selectors) { [{ matchingTag: true, consumer: "Foo" }] } + + it { is_expected.to include "matching tag for Foo" } + end + + describe "unknown" do + let(:consumer_version_selectors) { [{ branchPattern: "*foo" }] } + + it { is_expected.to include "branchPattern" } + it { is_expected.to include "*foo" } + end + end + end + end +end diff --git a/spec/lib/pact/pact_broker_spec.rb b/spec/lib/pact/pact_broker_spec.rb index f47a5c23..f19f99db 100644 --- a/spec/lib/pact/pact_broker_spec.rb +++ b/spec/lib/pact/pact_broker_spec.rb @@ -3,41 +3,22 @@ module Pact module PactBroker - describe ".fetch_pact_uris" do + describe ".fetch_pact_uris_for_verification" do before do - allow(Pact::PactBroker::FetchPacts).to receive(:call).and_return([pact_uri]) + allow(Pact::PactBroker::FetchPactURIsForVerification).to receive(:call).and_return([pact_uri]) end let(:pact_uri) { Pact::Provider::PactURI.new("http://pact") } - subject { Pact::PactBroker.fetch_pact_uris("foo") } - - it "calls Pact::PactBroker::FetchPacts" do - expect(Pact::PactBroker::FetchPacts).to receive(:call).with("foo") - subject - end - - it "returns a list of string URLs" do - expect(subject).to eq ["http://pact"] - end - end - - describe ".fetch_pending_pact_uris" do - before do - allow(Pact::PactBroker::FetchPendingPacts).to receive(:call).and_return([pact_uri]) - end - - let(:pact_uri) { Pact::Provider::PactURI.new("http://pact") } - - subject { Pact::PactBroker.fetch_pending_pact_uris("foo") } + subject { Pact::PactBroker.fetch_pact_uris_for_verification("foo") } it "calls Pact::PactBroker::FetchPendingPacts" do - expect(Pact::PactBroker::FetchPendingPacts).to receive(:call).with("foo") + expect(Pact::PactBroker::FetchPactURIsForVerification).to receive(:call).with("foo") subject end - it "returns a list of string URLs" do - expect(subject).to eq ["http://pact"] + it "returns a list of pact uris" do + expect(subject).to eq [pact_uri] end end end diff --git a/spec/lib/pact/provider/configuration/message_provider_dsl_spec.rb b/spec/lib/pact/provider/configuration/message_provider_dsl_spec.rb new file mode 100644 index 00000000..b41f5895 --- /dev/null +++ b/spec/lib/pact/provider/configuration/message_provider_dsl_spec.rb @@ -0,0 +1,198 @@ +require "spec_helper" +require "pact/provider/configuration/service_provider_dsl" +require "pact/provider/pact_uri" +require "pact/pact_broker/fetch_pacts" + +module Pact + module Provider + module Configuration + describe MessageProviderDSL do + describe "initialize" do + context "with an object instead of a block" do + subject do + described_class.build "name" do + app "blah" + end + end + + it "raises an error" do + expect { subject }.to raise_error /wrong number of arguments/ + end + end + + end + + describe "validate" do + context "when no name is provided" do + subject do + described_class.new " " do + app { Object.new } + end + end + + it "raises an error" do + expect { subject.send(:validate) }.to raise_error("Please provide a name for the Provider") + end + end + + context "when nil name is provided" do + subject do + described_class.new nil do + app { Object.new } + end + end + + it "raises an error" do + expect { subject.send(:validate) }.to raise_error(Pact::Provider::Configuration::Error, "Please provide a name for the Provider") + end + end + + context "when publish_verification_results is true" do + context "when no application version is provided" do + subject do + described_class.build "name" do + publish_verification_results true + end + end + + it "raises an error" do + expect { subject.send(:validate) }.to raise_error(Pact::Provider::Configuration::Error, "Please set the app_version when publish_verification_results is true") + end + end + + context "when an application version is provided" do + subject do + described_class.build "name" do + app_version "1.2.3" + publish_verification_results true + end + end + + it "does not raise an error" do + expect { subject.send(:validate) }.to_not raise_error + end + end + end + end + + describe "honours_pact_with" do + before do + Pact.clear_provider_world + end + let(:pact_url) { "blah" } + + context "with no optional params" do + subject do + described_class.build "some-provider" do + app {} + honours_pact_with "some-consumer" do + pact_uri pact_url + end + end + end + + it "adds a verification to the Pact.provider_world" do + subject + pact_uri = Pact::Provider::PactURI.new(pact_url) + expect(Pact.provider_world.pact_verifications.first) + .to eq(Pact::Provider::PactVerification.new("some-consumer", pact_uri, :head)) + end + end + + context "with all params specified" do + let(:pact_uri_options) do + { + username: "pact_user", + password: "pact_pw" + } + end + subject do + described_class.build "some-provider" do + app {} + honours_pact_with "some-consumer", ref: :prod do + pact_uri pact_url, pact_uri_options + end + end + end + + it "adds a verification to the Pact.provider_world" do + subject + pact_uri = Pact::Provider::PactURI.new(pact_url, pact_uri_options) + expect(Pact.provider_world.pact_verifications.first) + .to eq(Pact::Provider::PactVerification.new("some-consumer", pact_uri , :prod)) + end + end + end + + describe "honours_pacts_from_pact_broker" do + before do + Pact.clear_provider_world + end + let(:pact_url) { "blah" } + + context "with all params specified" do + let(:tag_1) { "master" } + + let(:tag_2) do + { + name: "tag-name", + all: false, + fallback: "master" + } + end + + let(:options) do + { + pact_broker_base_url: "some-url", + consumer_version_tags: [tag_1, tag_2] + } + end + + subject do + described_class.build "some-provider" do + app {} + app_version_tags ["dev"] + honours_pacts_from_pact_broker do + end + end + end + + it "builds a PactVerificationFromBroker" do + expect(PactVerificationFromBroker).to receive(:build).with("some-provider", nil, ["dev"]) + subject + end + end + end + + describe "builder" do + context "when builder is initialize with a object instead of a block" do + subject do + described_class.build "some-provider" do + builder "foo" + end + end + + it "raises an error" do + expect { subject }.to raise_error /wrong number of arguments/ + end + end + end + + describe "CONFIG_RU_APP" do + context "when a config.ru file does not exist" do + let(:path_that_does_not_exist) { "./tmp/this/path/does/not/exist/probably" } + + before do + allow(Pact.configuration).to receive(:config_ru_path).and_return(path_that_does_not_exist) + end + + it "raises an error with some helpful text" do + expect { described_class::CONFIG_RU_APP.call } + .to raise_error /Could not find config\.ru file.*#{Regexp.escape(path_that_does_not_exist)}/ + end + end + end + end + end + end +end diff --git a/spec/lib/pact/provider/configuration/pact_verification_from_broker_spec.rb b/spec/lib/pact/provider/configuration/pact_verification_from_broker_spec.rb index 095beed2..79c5c1ae 100644 --- a/spec/lib/pact/provider/configuration/pact_verification_from_broker_spec.rb +++ b/spec/lib/pact/provider/configuration/pact_verification_from_broker_spec.rb @@ -6,7 +6,10 @@ module Configuration describe PactVerificationFromBroker do describe 'build' do let(:provider_name) {'provider-name'} + let(:provider_version_branch) { 'main' } + let(:provider_version_tags) { ['master'] } let(:base_url) { "http://broker.org" } + let(:since) { "2020-01-01" } let(:basic_auth_options) do { username: 'pact_broker_username', @@ -14,26 +17,39 @@ module Configuration } end let(:tags) { ['master'] } + let(:fetch_pacts) { double('FetchPacts') } before do - allow(Pact::PactBroker::FetchPacts).to receive(:new).and_return(fetch_pacts) + allow(Pact::PactBroker::FetchPactURIsForVerification).to receive(:new).and_return(fetch_pacts) allow(Pact.provider_world).to receive(:add_pact_uri_source) end context "with valid values" do subject do - PactVerificationFromBroker.build(provider_name) do + PactVerificationFromBroker.build(provider_name, provider_version_branch, provider_version_tags) do pact_broker_base_url base_url, basic_auth_options consumer_version_tags tags + enable_pending true + include_wip_pacts_since since verbose true end end let(:fetch_pacts) { double('FetchPacts') } - let(:options) { basic_auth_options.merge(verbose: true) } - - it "creates a instance of Pact::PactBroker::FetchPacts" do - expect(Pact::PactBroker::FetchPacts).to receive(:new).with(provider_name, tags, base_url, options) + let(:basic_auth_opts) { basic_auth_options.merge(verbose: true) } + let(:options) { { include_pending_status: true, include_wip_pacts_since: "2020-01-01" }} + let(:consumer_version_selectors) { [ { tag: 'master', latest: true }] } + + it "creates a instance of Pact::PactBroker::FetchPactURIsForVerification" do + expect(Pact::PactBroker::FetchPactURIsForVerification).to receive(:new).with( + provider_name, + consumer_version_selectors, + provider_version_branch, + provider_version_tags, + base_url, + basic_auth_opts, + options + ) subject end @@ -41,17 +57,35 @@ module Configuration expect(Pact.provider_world).to receive(:add_pact_uri_source).with(fetch_pacts) subject end + + context "when since is a Date" do + let(:since) { Date.new(2020, 1, 1) } + + it "converts it to a string" do + expect(Pact::PactBroker::FetchPactURIsForVerification).to receive(:new).with( + anything, + anything, + anything, + anything, + anything, + anything, + { + include_pending_status: true, + include_wip_pacts_since: since.xmlschema + } + ) + subject + end + end end context "with a missing base url" do subject do - PactVerificationFromBroker.build(provider_name) do + PactVerificationFromBroker.build(provider_name, provider_version_branch, provider_version_tags) do end end - let(:fetch_pacts) { double('FetchPacts') } - it "raises an error" do expect { subject }.to raise_error Pact::Error, /Please provide a pact_broker_base_url/ end @@ -59,46 +93,87 @@ module Configuration context "with a non array object for consumer_version_tags" do subject do - PactVerificationFromBroker.build(provider_name) do + PactVerificationFromBroker.build(provider_name, provider_version_branch, provider_version_tags) do pact_broker_base_url base_url consumer_version_tags "master" end end - let(:fetch_pacts) { double('FetchPacts') } - it "coerces the value into an array" do - expect(Pact::PactBroker::FetchPacts).to receive(:new).with(anything, ["master"], anything, anything) + expect(Pact::PactBroker::FetchPactURIsForVerification).to receive(:new).with(anything, [{ tag: "master", latest: true}], anything, anything, anything, anything, anything) subject end end context "when no consumer_version_tags are provided" do subject do - PactVerificationFromBroker.build(provider_name) do + PactVerificationFromBroker.build(provider_name, provider_version_branch, provider_version_tags) do pact_broker_base_url base_url end end - let(:fetch_pacts) { double('FetchPacts') } + it "creates an instance of FetchPacts with an empty array for the consumer_version_tags" do + expect(Pact::PactBroker::FetchPactURIsForVerification).to receive(:new).with(anything, [], anything, anything, anything, anything, anything) + subject + end + end + + context "when the old format of selector is supplied to the consumer_verison_tags" do + let(:tags) { [{ name: 'main', all: true, fallback: 'fallback' }] } + + subject do + PactVerificationFromBroker.build(provider_name, provider_version_branch, provider_version_tags) do + pact_broker_base_url base_url + consumer_version_tags tags + end + end - it "creates an instance of FetchPacts with an emtpy array for the consumer_version_tags" do - expect(Pact::PactBroker::FetchPacts).to receive(:new).with(anything, [], anything, anything) + it "converts them to selectors" do + expect(Pact::PactBroker::FetchPactURIsForVerification).to receive(:new).with(anything, [{ tag: "main", latest: false, fallbackTag: 'fallback'}], anything, anything, anything, anything, anything) subject end end - context "when no verbose flag is provided" do + context "when an invalid class is used for the consumer_version_tags" do + let(:tags) { [true] } + subject do - PactVerificationFromBroker.build(provider_name) do + PactVerificationFromBroker.build(provider_name, provider_version_branch, provider_version_tags) do pact_broker_base_url base_url + consumer_version_tags tags end end - let(:fetch_pacts) { double('FetchPacts') } + it "raises an error" do + expect { subject }.to raise_error Pact::Error, "The value supplied for consumer_version_tags must be a String or a Hash. Found TrueClass" + end + end + + context "when consumer_version_selectors are provided" do + let(:tags) { [{ tag: 'main', latest: true, fallback_tag: 'fallback' }] } + + subject do + PactVerificationFromBroker.build(provider_name, provider_version_branch, provider_version_tags) do + pact_broker_base_url base_url + consumer_version_selectors tags + end + end + + it "converts the casing of the key names" do + expect(Pact::PactBroker::FetchPactURIsForVerification).to receive(:new).with(anything, [{ tag: "main", latest: true, fallbackTag: 'fallback'}], anything, anything, anything, anything, anything) + subject + end + end + + context "when no verbose flag is provided" do + subject do + PactVerificationFromBroker.build(provider_name, provider_version_branch, provider_version_tags) do + pact_broker_base_url base_url + end + end - it "creates an instance of FetchPacts with verbose: false" do - expect(Pact::PactBroker::FetchPacts).to receive(:new).with(anything, anything, anything, hash_including(verbose: false)) + it "creates an instance of FetchPactURIsForVerification with verbose: false" do + expect(Pact::PactBroker::FetchPactURIsForVerification).to receive(:new).with(anything, anything, anything, anything, anything, hash_including(verbose: false), anything) subject end end diff --git a/spec/lib/pact/provider/configuration/service_provider_config_spec.rb b/spec/lib/pact/provider/configuration/service_provider_config_spec.rb index fd5e46cb..2ea62092 100644 --- a/spec/lib/pact/provider/configuration/service_provider_config_spec.rb +++ b/spec/lib/pact/provider/configuration/service_provider_config_spec.rb @@ -1,21 +1,18 @@ -require 'spec_helper' require 'pact/provider/configuration/service_provider_config' module Pact module Provider module Configuration describe ServiceProviderConfig do - describe "app" do let(:app_block) { ->{ Object.new } } - subject { ServiceProviderConfig.new("1.2.3'", [], true, &app_block) } + subject { ServiceProviderConfig.new("1.2.3'", "main", [], true, &app_block) } it "should execute the app_block each time" do expect(subject.app.object_id).to_not equal(subject.app.object_id) end - end end end diff --git a/spec/lib/pact/provider/configuration/service_provider_dsl_spec.rb b/spec/lib/pact/provider/configuration/service_provider_dsl_spec.rb index d78b890f..e5b52669 100644 --- a/spec/lib/pact/provider/configuration/service_provider_dsl_spec.rb +++ b/spec/lib/pact/provider/configuration/service_provider_dsl_spec.rb @@ -156,13 +156,15 @@ module Configuration subject do ServiceProviderDSL.build 'some-provider' do app {} + app_version_branch 'main' + app_version_tags ['dev'] honours_pacts_from_pact_broker do end end end it 'builds a PactVerificationFromBroker' do - expect(PactVerificationFromBroker).to receive(:build).with('some-provider') + expect(PactVerificationFromBroker).to receive(:build).with('some-provider', 'main', ['dev']) subject end end diff --git a/spec/lib/pact/provider/help/content_spec.rb b/spec/lib/pact/provider/help/content_spec.rb index 1aaf17af..928623ba 100644 --- a/spec/lib/pact/provider/help/content_spec.rb +++ b/spec/lib/pact/provider/help/content_spec.rb @@ -4,19 +4,17 @@ module Pact module Provider module Help describe Content do - describe "#text" do - - let(:pact_1_json) { { some: 'json'}.to_json } - let(:pact_2_json) { { some: 'other json'}.to_json } - let(:pact_jsons) { [pact_1_json, pact_2_json] } - before do - allow(PactDiff).to receive(:call).with(pact_1_json).and_return('diff 1') - allow(PactDiff).to receive(:call).with(pact_2_json).and_return(nil) + allow(PactDiff).to receive(:call).with(pact_source_1).and_return('diff 1') + allow(PactDiff).to receive(:call).with(pact_source_2).and_return(nil) end - subject { Content.new(pact_jsons) } + let(:pact_source_1) { { some: 'json'}.to_json } + let(:pact_source_2) { { some: 'other json'}.to_json } + let(:pact_sources) { [pact_source_1, pact_source_2] } + + subject { Content.new(pact_sources) } it "displays the log path" do expect(subject.text).to include Pact.configuration.log_path @@ -29,7 +27,6 @@ module Help it "displays the diff" do expect(subject.text).to include 'diff 1' end - end end end diff --git a/spec/lib/pact/provider/help/pact_diff_spec.rb b/spec/lib/pact/provider/help/pact_diff_spec.rb deleted file mode 100644 index e32a8fa2..00000000 --- a/spec/lib/pact/provider/help/pact_diff_spec.rb +++ /dev/null @@ -1,128 +0,0 @@ -require 'pact/provider/help/pact_diff' - -module Pact - module Provider - module Help - - describe PactDiff do - - let(:href) { 'http://pact-broker/diff' } - let(:pact_json) do - { - '_links' => { - 'pb:diff-previous-distinct' => { - 'href' => href - } - } - }.to_json - end - - let(:output) { subject } - let(:diff) { {some: 'diff'}.to_json } - let(:diff_stream) { double('diff stream', read: diff) } - - describe ".call" do - - before do - stub_request(:get, "http://pact-broker/diff") - .to_return(status: 200, body: diff, headers: {}) - end - - subject { PactDiff.(pact_json) } - - context "when there are no links" do - let(:pact_json) { {}.to_json } - - it "returns nothing" do - subject - expect(output).to be_nil - end - end - - context "when the diff rel is not found" do - let(:pact_json) do - { - '_links' => {} - }.to_json - end - - it "returns nothing" do - subject - expect(output).to be_nil - end - end - - context "when the diff rel does not have a href" do - let(:pact_json) do - { - '_links' => { - 'pb:diff-previous-distinct' => {} - } - }.to_json - end - - it "returns nothing" do - subject - expect(output).to be_nil - end - end - - context "when the diff rel changes because Beth can't make up her mind" do - let(:pact_json) do - { - '_links' => { - 'distinct-diff-previous' => { - 'href' => href - } - } - }.to_json - end - - it "returns something" do - subject - expect(output).to_not be_nil - end - end - - context "when the diff resource exists" do - - it "returns a message" do - subject - expect(output).to include("The following changes") - end - - it "returns the diff" do - subject - expect(output).to include('some') - expect(output).to include('diff') - end - end - - context "when the diff resource doesn't exist" do - before do - stub_request(:get, "http://pact-broker/diff") - .to_return(status: 404) - end - - it "returns a warning" do - subject - expect(output).to include "Tried to retrieve diff with previous pact from #{href}, but received response code 404" - end - end - - context "when a redirect is received" do - before do - stub_request(:get, "http://pact-broker/diff") - .to_return(status: 301, body: diff, headers: {}) - end - - xit "follows the redirect" do - subject - expect(output).to_not include "301" - end - end - end - end - end - end -end diff --git a/spec/lib/pact/provider/help/write_spec.rb b/spec/lib/pact/provider/help/write_spec.rb index 9148c704..4a11a7b6 100644 --- a/spec/lib/pact/provider/help/write_spec.rb +++ b/spec/lib/pact/provider/help/write_spec.rb @@ -7,7 +7,7 @@ module Help describe "#call" do - let(:pact_jsons) { double('pact jsons') } + let(:pact_sources) { double('pact jsons') } let(:reports_dir) { "./tmp/reports" } let(:text) { "help text" } @@ -16,12 +16,12 @@ module Help allow_any_instance_of(Content).to receive(:text).and_return(text) end - subject { Write.call(pact_jsons, reports_dir) } + subject { Write.call(pact_sources, reports_dir) } let(:actual_contents) { File.read(File.join(reports_dir, Write::HELP_FILE_NAME)) } - it "passes the pact_jsons into the Content" do - expect(Content).to receive(:new).with(pact_jsons).and_return(double(text: '')) + it "passes the pact_sources into the Content" do + expect(Content).to receive(:new).with(pact_sources).and_return(double(text: '')) subject end diff --git a/spec/lib/pact/provider/pact_helper_locator_spec.rb b/spec/lib/pact/provider/pact_helper_locator_spec.rb index 7c39d8f9..72049a08 100644 --- a/spec/lib/pact/provider/pact_helper_locator_spec.rb +++ b/spec/lib/pact/provider/pact_helper_locator_spec.rb @@ -20,6 +20,12 @@ def make_pactfile dir '/spec/serviceconsumers', '/spec/consumer', '/spec', + '/test/blah/service-consumers', + '/test/consumers', + '/test/blah/service_consumers', + '/test/serviceconsumers', + '/test/consumer', + '/test', '/blah', '/blah/consumer', '' diff --git a/spec/lib/pact/provider/pact_uri_spec.rb b/spec/lib/pact/provider/pact_uri_spec.rb index 357e88de..f7b6df6c 100644 --- a/spec/lib/pact/provider/pact_uri_spec.rb +++ b/spec/lib/pact/provider/pact_uri_spec.rb @@ -23,13 +23,46 @@ end describe '#to_s' do - context 'with userinfo provided' do + context 'with basic auth provided' do let(:password) { 'my_password' } let(:options) { { username: username, password: password } } - it 'should include user name and password' do + it 'should include user name and and hide password' do expect(pact_uri.to_s).to eq('http://pact:*****@uri') end + + context 'when basic auth credentials have been set for a local file (eg. via environment variables, unintentionally)' do + let(:uri) { '/some/file thing.json' } + + it 'does not blow up' do + expect(pact_uri.to_s).to eq uri + end + end + + context "with a username that has an @ symbol" do + let(:username) { "foo@bar" } + + it 'does not blow up' do + expect(pact_uri.to_s).to eq "http://*****:*****@uri" + end + end + end + + context 'with personal access token provided' do + let(:pat) { 'should_be_secret' } + let(:options) { { username: pat } } + + it 'should hide the pat' do + expect(pact_uri.to_s).to eq('http://*****@uri') + end + + context 'when pat credentials have been set for a local file (eg. via environment variables, unintentionally)' do + let(:uri) { '/some/file thing.json' } + + it 'does not blow up' do + expect(pact_uri.to_s).to eq uri + end + end end context 'without userinfo' do diff --git a/spec/lib/pact/provider/rspec/calculate_exit_code_spec.rb b/spec/lib/pact/provider/rspec/calculate_exit_code_spec.rb new file mode 100644 index 00000000..4e062442 --- /dev/null +++ b/spec/lib/pact/provider/rspec/calculate_exit_code_spec.rb @@ -0,0 +1,56 @@ +require 'pact/provider/rspec/calculate_exit_code' + +module Pact + module Provider + module RSpec + module CalculateExitCode + describe ".call" do + let(:pact_source_1) { double('pact_source_1', pending?: pending_1) } + let(:pending_1) { nil } + let(:pact_source_2) { double('pact_source_2', pending?: pending_2) } + let(:pending_2) { nil } + let(:pact_source_3) { double('pact_source_3', pending?: pending_3) } + let(:pending_3) { nil } + let(:pact_sources) { [pact_source_1, pact_source_2, pact_source_3]} + + let(:failed_examples) { [ example_1, example_2, example_3 ] } + let(:example_1) { double('example_1', metadata: { pact_source: pact_source_1 }) } + let(:example_2) { double('example_2', metadata: { pact_source: pact_source_1 }) } + let(:example_3) { double('example_3', metadata: { pact_source: pact_source_2 }) } + + subject { CalculateExitCode.call(pact_sources, failed_examples ) } + + context "when all pacts are pending" do + let(:pending_1) { true } + let(:pending_2) { true } + let(:pending_3) { true } + + it "returns 0" do + expect(subject).to eq 0 + end + end + + context "when a non pending pact has no failures" do + let(:pending_1) { true } + let(:pending_2) { true } + let(:pending_3) { false } + + it "returns 0" do + expect(subject).to eq 0 + end + end + + context "when a non pending pact no failures" do + let(:pending_1) { true } + let(:pending_2) { false } + let(:pending_3) { false } + + it "returns 1" do + expect(subject).to eq 1 + end + end + end + end + end + end +end diff --git a/spec/lib/pact/provider/rspec/formatter_rspec_3_spec.rb b/spec/lib/pact/provider/rspec/formatter_rspec_3_spec.rb index 643e3dc1..791bfbf7 100644 --- a/spec/lib/pact/provider/rspec/formatter_rspec_3_spec.rb +++ b/spec/lib/pact/provider/rspec/formatter_rspec_3_spec.rb @@ -10,8 +10,10 @@ module Provider module RSpec describe Formatter do - let(:interaction) { InteractionFactory.create 'provider_state' => 'a state', 'description' => 'a description'} - let(:pactfile_uri) { 'pact_file_uri' } + let(:interaction) { InteractionFactory.create 'provider_state' => 'a state', 'description' => 'a description', '_id' => id, 'index' => 2 } + let(:interaction_2) { InteractionFactory.create 'provider_state' => 'a state', 'description' => 'a description 2', '_id' => "#{id}2", 'index' => 3 } + let(:id) { nil } + let(:pactfile_uri) { Pact::Provider::PactURI.new('pact_file_uri') } let(:description) { 'an interaction' } let(:pact_json) { {some: 'pact json'}.to_json } let(:metadata) do @@ -20,20 +22,22 @@ module RSpec pactfile_uri: pactfile_uri, pact_interaction_example_description: description, pact_json: pact_json, - pact_ignore_failures: ignore_failures + pact_ignore_failures: ignore_failures, } end - let(:metadata_2) { metadata.merge(pact_interaction_example_description: 'another interaction')} + let(:metadata_2) { metadata.merge(pact_interaction: interaction_2)} let(:example) { double("Example", metadata: metadata) } let(:example_2) { double("Example", metadata: metadata_2) } let(:failed_examples) { [example, example] } let(:examples) { [example, example, example_2]} let(:output) { StringIO.new } - let(:rerun_command) { "rake pact:verify:at[pact_file_uri] PACT_DESCRIPTION=\"a description\" PACT_PROVIDER_STATE=\"a state\" # an interaction" } + let(:rerun_command) { 'PACT_DESCRIPTION="a description" PACT_PROVIDER_STATE="a state" # an interaction' } + let(:broker_rerun_command) { "rake pact:verify:at[pact_file_uri] PACT_BROKER_INTERACTION_ID=\"1234\" # an interaction" } let(:missing_provider_states) { 'missing_provider_states'} let(:summary) { double("summary", failure_count: 1, failed_examples: failed_examples, examples: examples)} let(:pact_executing_language) { 'ruby' } let(:pact_interaction_rerun_command) { Pact::TaskHelper::PACT_INTERACTION_RERUN_COMMAND } + let(:pact_interaction_rerun_command_for_broker) { Pact::TaskHelper::PACT_INTERACTION_RERUN_COMMAND_FOR_BROKER } let(:ignore_failures) { nil } subject { Formatter.new output } @@ -43,10 +47,12 @@ module RSpec before do allow(ENV).to receive(:[]).with('PACT_INTERACTION_RERUN_COMMAND').and_return(pact_interaction_rerun_command) allow(ENV).to receive(:[]).with('PACT_EXECUTING_LANGUAGE').and_return(pact_executing_language) + allow(ENV).to receive(:[]).with('PACT_INTERACTION_RERUN_COMMAND_FOR_BROKER').and_return(pact_interaction_rerun_command_for_broker) allow(PrintMissingProviderStates).to receive(:call) allow(Pact::Provider::Help::PromptText).to receive(:call).and_return("some help") allow(subject).to receive(:failed_examples).and_return(failed_examples) allow(Pact.provider_world.provider_states).to receive(:missing_provider_states).and_return(missing_provider_states) + allow(subject).to receive(:set_rspec_failure_color) subject.dump_summary summary end @@ -69,15 +75,46 @@ module RSpec end end - context "when PACT_INTERACTION_RERUN_COMMAND is not set" do + context "when PACT_INTERACTION_RERUN_COMMAND_FOR_BROKER is set" do + context "when the _id is populated" do + let(:id) { "1234" } + + it "prints a list of rerun commands" do + expect(output_result).to include(broker_rerun_command) + end + + it "only prints unique commands" do + expect(output_result.scan(broker_rerun_command).size).to eq 1 + end + end + + context "when the _id is not populated" do + it "prints a list of rerun commands using the provider state and description" do + expect(output_result).to include(rerun_command) + end + end + end + + context "when PACT_INTERACTION_RERUN_COMMAND and PACT_INTERACTION_RERUN_COMMAND_FOR_BROKER are not set" do let(:pact_interaction_rerun_command) { nil } + let(:pact_interaction_rerun_command_for_broker) { nil } + + context "when the _id is populated" do + let(:id) { "1234" } + + it "prints a list of failed interactions" do + expect(output_result).to include('* an interaction (to re-run just this interaction, set environment variable PACT_BROKER_INTERACTION_ID="1234")') + end + end - it "prints a list of failed interactions" do - expect(output_result).to include("* #{description}\n") + context "when the _id is not populated" do + it "prints a list of failed interactions" do + expect(output_result).to include('* an interaction (to re-run just this interaction, set environment variables PACT_DESCRIPTION="a description" PACT_PROVIDER_STATE="a state")') + end end it "only prints unique commands" do - expect(output_result.scan("* #{description}\n").size).to eq 1 + expect(output_result.scan("* #{description}").size).to eq 1 end end @@ -106,7 +143,7 @@ module RSpec end context "when ignore_failures is true" do - let(:ignore_failures) { true } + let(:pactfile_uri) { Pact::Provider::PactURI.new('pact_file_uri', {}, { pending: true}) } it "reports failures as pending" do expect(output_result).to include("1 pending") diff --git a/spec/lib/pact/provider/verification_results/create_spec.rb b/spec/lib/pact/provider/verification_results/create_spec.rb index 55b5e846..7da50fa5 100644 --- a/spec/lib/pact/provider/verification_results/create_spec.rb +++ b/spec/lib/pact/provider/verification_results/create_spec.rb @@ -14,21 +14,25 @@ module VerificationResults double('provider_configuration', application_version: '1.2.3') end let(:pact_source_1) do - instance_double('Pact::Provider::PactSource', uri: pact_uri_1, pact_hash: pact_hash_1) + instance_double('Pact::Provider::PactSource', uri: pact_uri_1, consumer_contract: consumer_contract) end + let(:consumer_contract) { instance_double('Pact::ConsumerContract', interactions: interactions)} + let(:interactions) { [interaction_1] } + let(:interaction_1) { instance_double('Pact::Interaction', _id: "1") } + let(:interaction_2) { instance_double('Pact::Interaction', _id: "2") } let(:pact_uri_1) { instance_double('Pact::Provider::PactURI', uri: URI('foo')) } let(:pact_uri_2) { instance_double('Pact::Provider::PactURI', uri: URI('bar')) } let(:example_1) do { pact_uri: pact_uri_1, - pact_interaction: double('interaction'), + pact_interaction: interaction_1, status: 'passed' } end let(:example_2) do { pact_uri: pact_uri_2, - pact_interaction: double('interaction'), + pact_interaction: interaction_2, status: 'passed' } end @@ -37,11 +41,6 @@ module VerificationResults tests: [example_1, example_2] } end - let(:pact_hash_1) do - { - 'interactions' => [{}] - } - end subject { Create.call(pact_source_1, test_results_hash) } @@ -78,11 +77,9 @@ module VerificationResults end context "when not every interaction has been executed" do - let(:pact_hash_1) do - { - 'interactions' => [{}, {}] - } - end + let(:interaction_3) { instance_double('Pact::Interaction', _id: "3") } + let(:interactions) { [interaction_1, interaction_2]} + it "sets publishable to false" do expect(VerificationResult).to receive(:new).with(false, anything, anything, anything) subject diff --git a/spec/lib/pact/provider/verification_results/publish_spec.rb b/spec/lib/pact/provider/verification_results/publish_spec.rb index 15565527..1e5b67ba 100644 --- a/spec/lib/pact/provider/verification_results/publish_spec.rb +++ b/spec/lib/pact/provider/verification_results/publish_spec.rb @@ -9,7 +9,8 @@ module VerificationResults let(:stubbed_publish_verification_url) { 'http://broker/something/provider/Bar/verifications' } let(:tag_version_url) { 'http://tag-me/{tag}' } let(:pact_source) { instance_double("Pact::Provider::PactSource", pact_hash: pact_hash, uri: pact_url)} - let(:pact_url) { instance_double("Pact::Provider::PactURI", basic_auth?: basic_auth, username: 'username', password: 'password')} + let(:pact_url) { instance_double("Pact::Provider::PactURI", options: options) } + let(:options) { { username: 'username', password: 'password' } } let(:provider_url) { 'http://provider' } let(:basic_auth) { false } let(:pact_hash) do @@ -47,6 +48,9 @@ module VerificationResults }, 'pb:version-tag' => { 'href' => 'http://provider/version/{version}/tag/{tag}' + }, + 'pb:branch-version' => { + 'href' => 'http://provider/branches/{branch}/versions/{version}' } } }.to_json @@ -64,6 +68,7 @@ module VerificationResults let(:verification_json) { '{"foo": "bar"}' } let(:publish_verification_results) { false } let(:publishable) { true } + let(:branch) { nil } let(:tags) { [] } let(:verification) do instance_double("Pact::Verifications::Verification", @@ -74,15 +79,16 @@ module VerificationResults end let(:provider_configuration) do - double('provider config', publish_verification_results?: publish_verification_results, tags: tags, application_version: '1.2.3') + double('provider config', publish_verification_results?: publish_verification_results, branch: branch, tags: tags, application_version: '1.2.3') end before do allow($stdout).to receive(:puts) allow($stderr).to receive(:puts) allow(Pact.configuration).to receive(:provider).and_return(provider_configuration) - stub_request(:post, stubbed_publish_verification_url).to_return(status: 200, body: created_verification_body) + stub_request(:post, stubbed_publish_verification_url).to_return(status: 200, headers: { 'Content-Type' => 'application/hal+json'}, body: created_verification_body) stub_request(:put, 'http://provider/version/1.2.3/tag/foo').to_return(status: 200, headers: { 'Content-Type' => 'application/hal+json'}, body: tag_body) + stub_request(:put, "http://provider/branches/main/versions/1.2.3").to_return(status: 200, body: "{}", headers: { 'Content-Type' => 'application/hal+json' }) stub_request(:get, provider_url).to_return(status: 200, headers: { 'Content-Type' => 'application/hal+json'}, body: provider_body) allow(Retry).to receive(:until_true) { |&block| block.call } end @@ -116,6 +122,35 @@ module VerificationResults end end + context "with a branch" do + let(:branch) { "main" } + + it "creates the branch version" do + subject + expect(WebMock).to have_requested(:put, 'http://provider/branches/main/versions/1.2.3').with(headers: {'Content-Type' => 'application/json'}) + end + + context "when there is an error creating the branch version" do + before do + stub_request(:put, "http://provider/branches/main/versions/1.2.3").to_return(status: 500, body: { some: "error" }.to_json, headers: { 'Content-Type' => 'application/hal+json' }) + end + + it "raises an error" do + expect { subject }.to raise_error PublicationError, /500.*some.*error/ + end + end + + context "when the broker does not support creating branch versions" do + let(:provider_body) do + {}.to_json + end + + it "raises an error" do + expect { subject }.to raise_error PublicationError, /does not support/ + end + end + end + context "with tags" do let(:tags) { ['foo'] } @@ -154,13 +189,21 @@ module VerificationResults end context "when basic auth is configured on the pact URL" do - let(:basic_auth) { true } - it "sets the username and password for the pubication URL" do + it "sets the username and password for the publication URL" do subject expect(WebMock).to have_requested(:post, publish_verification_url).with(basic_auth: ['username', 'password']) end end + context "when a token is configured on the pact URL" do + let(:options) { {token: 'token'} } + + it "sets the authorization header" do + subject + expect(WebMock).to have_requested(:post, publish_verification_url).with(headers: { 'Authorization' => 'Bearer token'}) + end + end + context "when an HTTP error is returned" do it "raises a PublicationError" do stub_request(:post, stubbed_publish_verification_url).to_return(status: 500, body: '{}') @@ -177,7 +220,7 @@ module VerificationResults context "with https" do before do - stub_request(:post, publish_verification_url).to_return(status: 200, body: created_verification_body) + stub_request(:post, publish_verification_url).to_return(status: 200, headers: { 'Content-Type' => 'application/json' }, body: created_verification_body) end let(:publish_verification_url) { stubbed_publish_verification_url.gsub('http', 'https') } diff --git a/spec/service_providers/pact_ruby_fetch_pacts_for_verification_test.rb b/spec/service_providers/pact_ruby_fetch_pacts_for_verification_test.rb new file mode 100644 index 00000000..6260dce9 --- /dev/null +++ b/spec/service_providers/pact_ruby_fetch_pacts_for_verification_test.rb @@ -0,0 +1,114 @@ +require_relative 'helper' +require 'pact/pact_broker/fetch_pact_uris_for_verification' + +describe Pact::PactBroker::FetchPactURIsForVerification, pact: true do + before do + allow($stdout).to receive(:puts) + end + + let(:get_headers) { { "Accept" => 'application/hal+json' } } + let(:post_headers) do + { + "Accept" => 'application/hal+json', + "Content-Type" => "application/json" + } + end + let(:pacts_for_verification_relation) { Pact::PactBroker::FetchPactURIsForVerification::PACTS_FOR_VERIFICATION_RELATION } + let(:body) do + { + "providerVersionBranch" => "main", + "providerVersionTags" => ["pdev"], + "consumerVersionSelectors" => [{ "tag" => "cdev", "latest" => true}], + "includePendingStatus" => true + } + end + let(:provider_version_branch) { "main" } + let(:provider_version_tags) { %w[pdev] } + let(:consumer_version_selectors) { [ { tag: "cdev", latest: true }] } + let(:options) { { include_pending_status: true }} + + subject { Pact::PactBroker::FetchPactURIsForVerification.call(provider, consumer_version_selectors, provider_version_branch, provider_version_tags, broker_base_url, basic_auth_options, options) } + + describe 'fetch pacts' do + let(:provider) { 'Bar' } + let(:broker_base_url) { pact_broker.mock_service_base_url} + let(:basic_auth_options) { { username: 'username', password: 'password' } } + + before do + pact_broker + .given('the relation for retrieving pacts for verifications exists in the index resource') + .upon_receiving('a request for the index resource') + .with( + method: :get, + path: '/', + headers: get_headers + ) + .will_respond_with( + status: 200, + headers: { "Content-Type" => Pact.term("application/hal+json", /hal/) }, + body: { + _links: { + pacts_for_verification_relation => { + href: Pact.term( + generate: broker_base_url + '/pacts/provider/{provider}/for-verification', + matcher: %r{/pacts/provider/{provider}/for-verification$} + ) + } + } + } + ) + end + + context 'retrieving pacts for verification by provider' do + before do + pact_broker + .given('Foo has a pact tagged cdev with provider Bar') + .upon_receiving('a request to retrieve the pacts for verification for a provider') + .with( + method: :post, + path: '/pacts/provider/Bar/for-verification', + body: body, + headers: post_headers + ) + .will_respond_with( + status: 200, + headers: { "Content-Type" => Pact.term("application/hal+json", /hal/) }, + body: { + "_embedded" => { + "pacts" => [{ + "shortDescription" => "a description", + "verificationProperties" => { + "pending" => Pact.like(true), + "notices" => Pact.each_like("text" => "some text") + }, + '_links' => { + "self" => { + "href" => Pact.term('http://pact-broker-url-for-foo', %r{http://.*}) + } + } + }] + } + } + ) + end + + let(:expected_metadata) do + { + pending: true, + notices: [ + text: "some text" + ], + short_description: "a description" + } + end + + it 'returns the array of pact urls' do + expect(subject).to eq( + [ + Pact::Provider::PactURI.new('http://pact-broker-url-for-foo', basic_auth_options, expected_metadata) + ] + ) + end + end + end +end diff --git a/spec/service_providers/pact_ruby_fetch_pacts_test.rb b/spec/service_providers/pact_ruby_fetch_pacts_test.rb index 65305310..9379eecb 100644 --- a/spec/service_providers/pact_ruby_fetch_pacts_test.rb +++ b/spec/service_providers/pact_ruby_fetch_pacts_test.rb @@ -26,6 +26,9 @@ ) .will_respond_with( status: 200, + headers: { + 'Content-Type' => Pact.term('application/hal+json', /json/) + }, body: { _links: { 'pb:latest-provider-pacts' => { @@ -65,6 +68,9 @@ ) .will_respond_with( status: 200, + headers: { + 'Content-Type' => Pact.term('application/hal+json', /json/) + }, body: { _links: { 'pb:pacts' => [ @@ -105,6 +111,9 @@ ) .will_respond_with( status: 200, + headers: { + 'Content-Type' => Pact.term('application/hal+json', /json/) + }, body: { _links: { 'pb:pacts' => [ @@ -128,6 +137,9 @@ ) .will_respond_with( status: 200, + headers: { + 'Content-Type' => Pact.term('application/hal+json', /json/) + }, body: { _links: { 'pb:pacts' => [ @@ -171,6 +183,9 @@ ) .will_respond_with( status: 200, + headers: { + 'Content-Type' => Pact.term('application/hal+json', /json/) + }, body: { _links: { 'pb:pacts' => [] @@ -187,6 +202,9 @@ ) .will_respond_with( status: 200, + headers: { + 'Content-Type' => Pact.term('application/hal+json', /json/) + }, body: { _links: { 'pb:pacts' => [ @@ -227,6 +245,9 @@ ) .will_respond_with( status: 200, + headers: { + 'Content-Type' => Pact.term('application/hal+json', /json/) + }, body: { _links: { 'pb:pacts' => [] @@ -256,6 +277,9 @@ ) .will_respond_with( status: 200, + headers: { + 'Content-Type' => Pact.term('application/hal+json', /json/) + }, body: { _links: { 'pb:pacts' => [ @@ -280,6 +304,9 @@ ) .will_respond_with( status: 200, + headers: { + 'Content-Type' => Pact.term('application/hal+json', /json/) + }, body: { _links: { 'pb:pacts' => [ @@ -323,6 +350,9 @@ ) .will_respond_with( status: 200, + headers: { + 'Content-Type' => Pact.term('application/hal+json', /json/) + }, body: { _links: { 'pb:pacts' => [ diff --git a/spec/service_providers/pact_ruby_fetch_wip_pacts_test.rb b/spec/service_providers/pact_ruby_fetch_wip_pacts_test.rb deleted file mode 100644 index baa5a47f..00000000 --- a/spec/service_providers/pact_ruby_fetch_wip_pacts_test.rb +++ /dev/null @@ -1,74 +0,0 @@ -require_relative 'helper' -require 'pact/pact_broker/fetch_pending_pacts' - -describe Pact::PactBroker::FetchPendingPacts, pact: true do - before do - allow($stdout).to receive(:puts) - end - - let(:get_headers) { { Accept: 'application/hal+json' } } - - describe 'fetch pacts' do - let(:provider) { 'provider-1' } - let(:broker_base_url) { pact_broker.mock_service_base_url + '/' } - let(:basic_auth_options) { { username: 'foo', password: 'bar' } } - - before do - pact_broker - .given('the relation for retrieving pending pacts exists in the index resource') - .upon_receiving('a request for the index resource') - .with( - method: :get, - path: '/', - headers: get_headers - ) - .will_respond_with( - status: 200, - body: { - _links: { - 'beta:pending-provider-pacts' => { - href: Pact.term( - generate: broker_base_url + 'pacts/provider/{provider}/pending', - matcher: %r{/pacts/provider/{provider}/pending$} - ) - } - } - } - ) - end - - context 'retrieving pending pacts by provider' do - before do - pact_broker - .given('consumer-1 has a pending pact with provider provider-1') - .upon_receiving('a request to retrieve the pending pacts for provider') - .with( - method: :get, - path: '/pacts/provider/provider-1/pending', - headers: get_headers - ) - .will_respond_with( - status: 200, - body: { - _links: { - 'pb:pacts' => [ - { - href: Pact.term('http://pact-broker-url-for-consumer-1', %r{http://.*}) - } - ] - } - } - ) - end - - it 'returns the array of pact urls' do - pacts = Pact::PactBroker::FetchPendingPacts.call(provider, broker_base_url, basic_auth_options) - expect(pacts).to eq( - [ - Pact::Provider::PactURI.new('http://pact-broker-url-for-consumer-1', basic_auth_options) - ] - ) - end - end - end -end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 7951d9e3..9800ca85 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,6 +1,6 @@ require 'rspec' +require 'rspec/its' require 'fakefs/spec_helpers' -require 'rspec' require 'pact' require 'webmock/rspec' require 'support/factories' @@ -15,7 +15,7 @@ is_jruby = defined?(RUBY_ENGINE) && RUBY_ENGINE == 'jruby' RSpec.configure do | config | - config.include(FakeFS::SpecHelpers, :fakefs => true) + config.include(FakeFS::SpecHelpers, fakefs: true) config.extend Pact::Provider::RSpec::ClassMethods config.include Pact::Provider::RSpec::InstanceMethods @@ -24,6 +24,5 @@ if config.respond_to?(:example_status_persistence_file_path=) config.example_status_persistence_file_path = "./spec/examples.txt" end - config.filter_run_excluding :skip_jruby => is_jruby + config.filter_run_excluding skip_jruby: is_jruby end - diff --git a/spec/support/bar_pact_helper.rb b/spec/support/bar_pact_helper.rb index ae03faea..8f1752f9 100644 --- a/spec/support/bar_pact_helper.rb +++ b/spec/support/bar_pact_helper.rb @@ -16,6 +16,7 @@ def call env Pact.service_provider "Bar" do app { BarApp.new } app_version '1.2.3' + app_version_branch 'master' app_version_tags ['master'] publish_verification_results true diff --git a/spec/support/foo-bar-message.json b/spec/support/foo-bar-message.json new file mode 100644 index 00000000..cca3395e --- /dev/null +++ b/spec/support/foo-bar-message.json @@ -0,0 +1,26 @@ +{ + "consumer": { + "name": "Foo" + }, + "provider": { + "name": "Bar" + }, + "messages": [ + { + "description": "a message", + "providerStates": [ + { + "name": "a world exists" + } + ], + "contents": { + "text": "Hello world" + } + } + ], + "metadata": { + "pactSpecification": { + "version": "2.0.0" + } + } +} diff --git a/spec/support/message_spec_helper.rb b/spec/support/message_spec_helper.rb new file mode 100644 index 00000000..13a0f046 --- /dev/null +++ b/spec/support/message_spec_helper.rb @@ -0,0 +1,41 @@ +require 'pact/message' + +# Example data store + +class DataStore + def self.greeting_recipient= greeting_recipient + @greeting_recipient = greeting_recipient + end + + def self.greeting_recipient + @greeting_recipient + end +end + +# Example message producer + +class BarProvider + def create_message + { + text: "Hello #{DataStore.greeting_recipient}" + } + end +end + +# Provider states + +Pact.provider_states_for "Foo" do + provider_state "a world exists" do + set_up do + DataStore.greeting_recipient = "world" + end + end +end + +CONFIG = { + "a message" => lambda { BarProvider.new.create_message } +} + +Pact.message_provider "Bar" do + builder { |description| CONFIG[description].call } +end diff --git a/tasks/foo-bar.rake b/tasks/foo-bar.rake index 87d4e5db..ebe3129c 100644 --- a/tasks/foo-bar.rake +++ b/tasks/foo-bar.rake @@ -1,4 +1,5 @@ require 'pact/tasks/verification_task' +require 'faraday' # Use for end to end manual debugging of issues. BROKER_BASE_URL = 'http://localhost:9292' @@ -22,6 +23,11 @@ task 'pact:foobar:publish' do http.request put_request end puts response.code unless response.code == '200' + + tag_response = Faraday.put("#{BROKER_BASE_URL}/pacticipants/Foo/versions/1.0.0/tags/dev", nil, { 'Content-Type' => 'application/json' }) + puts tag_response.status unless tag_response.status == 200 + tag_response = Faraday.put("#{BROKER_BASE_URL}/pacticipants/Foo/versions/1.0.0/tags/prod", nil, { 'Content-Type' => 'application/json' }) + puts tag_response.status unless tag_response.status == 200 end #'./spec/pacts/foo-bar.json' @@ -35,7 +41,7 @@ Pact::VerificationTask.new('foobar:wip') do | pact | end Pact::VerificationTask.new(:foobar_using_broker) do | pact | - pact.uri "#{BROKER_BASE_URL}/pacts/provider/Bar/consumer/Foo/version/1.0.0", :pact_helper => './spec/support/bar_pact_helper.rb', username: BROKER_USERNAME, password: BROKER_PASSWORD + pact.uri nil, :pact_helper => './spec/support/bar_pact_helper.rb', username: BROKER_USERNAME, password: BROKER_PASSWORD end Pact::VerificationTask.new('foobar_using_broker:fail') do | pact | diff --git a/tasks/message-test.rake b/tasks/message-test.rake new file mode 100644 index 00000000..680dbc0b --- /dev/null +++ b/tasks/message-test.rake @@ -0,0 +1,5 @@ +require 'pact/tasks' + +Pact::VerificationTask.new(:message) do | pact | + pact.uri 'spec/support/foo-bar-message.json', pact_helper: 'spec/support/message_spec_helper.rb' +end diff --git a/tasks/pact-test.rake b/tasks/pact-test.rake index 86fcaeb4..28247981 100644 --- a/tasks/pact-test.rake +++ b/tasks/pact-test.rake @@ -80,6 +80,7 @@ namespace :pact do Rake::Task['pact:verify:term_v2'].execute Rake::Task['pact:verify:test_app_with_provider_state_params'].execute Rake::Task['pact:verify:test_app:wip'].execute + Rake::Task['pact:verify:message'].execute end desc "All the verification tests with active support loaded" diff --git a/tasks/release.rake b/tasks/release.rake index 8f8a634b..422e4d21 100644 --- a/tasks/release.rake +++ b/tasks/release.rake @@ -7,3 +7,10 @@ task :generate_changelog do require 'pact/version' ConventionalChangelog::Generator.new.generate! version: "v#{Pact::VERSION}" end + +desc 'Tag for release' +task :tag_for_release do | t, args | + command = "git tag -a v#{Pact::VERSION} -m \"chore(release): version #{Pact::VERSION}\" && git push origin v#{Pact::VERSION}" + puts command + puts `#{command}` +end diff --git a/tasks/spec.rake b/tasks/spec.rake index 3744bbf9..e1a58139 100644 --- a/tasks/spec.rake +++ b/tasks/spec.rake @@ -6,3 +6,12 @@ RSpec::Core::RakeTask.new(:spec) RSpec::Core::RakeTask.new('spec:provider') do | task | task.pattern = "spec/service_providers/**/*_test.rb" end + +task :set_active_support_on do + ENV["LOAD_ACTIVE_SUPPORT"] = 'true' +end + +desc "This is to ensure that the gem still works even when active support JSON is loaded." +task :spec_with_active_support => [:set_active_support_on] do + Rake::Task['spec'].execute +end