diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md new file mode 100644 index 0000000..c83e0da --- /dev/null +++ b/.github/ISSUE_TEMPLATE.md @@ -0,0 +1,31 @@ +# Pre issue-raising checklist + +I have already (please mark the applicable with an `x`): + +* [ ] Read through the relevant docs at https://docs.pact.io +* [ ] Upgraded to the latest version of the gem +* [ ] Checked the CHANGELOG to see if the issue I am about to raise has been fixed +* [ ] Created an executable example that demonstrates the issue using either a: + * Dockerfile + * Git repository with a Travis or Appveyor (or similar) build + +## Software versions + +* **OS**: e.g. Mac OSX 10.11.5 +* **pact mock service:** eg. v 1.23.0 + +## Expected behaviour + +Please complete. + +## Actual behaviour + +Please complete. + +## Steps to reproduce + +Provide a repository, gist or reproducible code snippet so that we can test the problem. + +## Relevant log files + +Please ensure you set logging to `DEBUG` and attach any relevant log files here (or link from a gist). diff --git a/.github/workflows/release_gem.yml b/.github/workflows/release_gem.yml new file mode 100644 index 0000000..7cb3bb3 --- /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.hangs b/.github/workflows/test.yml.hangs new file mode 100644 index 0000000..daf37a2 --- /dev/null +++ b/.github/workflows/test.yml.hangs @@ -0,0 +1,23 @@ +name: Test + +on: [push, pull_request] + +jobs: + test: + runs-on: "ubuntu-latest" + continue-on-error: ${{ matrix.experimental }} + strategy: + fail-fast: false + matrix: + ruby_version: ["2.2", "2.7"] + experimental: [false] + include: + - ruby_version: "3.0" + experimental: true + steps: + - uses: actions/checkout@v2 + - uses: ruby/setup-ruby@v1 + with: + ruby-version: ${{ matrix.ruby_version }} + - run: "bundle install" + - run: "bundle exec rspec" diff --git a/.gitignore b/.gitignore index d3d5803..9f62c9b 100644 --- a/.gitignore +++ b/.gitignore @@ -29,6 +29,7 @@ log reports Gemfile.lock build +.byebug_history vendor/bundle/ spec/examples.txt diff --git a/.travis.yml b/.travis.yml index 81202c9..8d10553 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,28 +2,32 @@ language: ruby sudo: false rvm: - 2.2.4 -- 2.3.1 -- jruby-9.0.5.0 +- 2.7.0 env: global: secure: FqQ00zkw2heLh5XafaMMv1LXgy+DBxumbcSI3c9iDlRvi4KZQ+n+NqSp/feVEIxSMzA9FmH7ZqTVJmsA5jByrk71WiguU8RZ7NSVzonlLZK0tQ9idwzPtvc38abJwWm3cR9TJlkNxUgQ2iHXLobo4zFSEK/4s0Ob9ddf3x4BUmo= -deploy: -- provider: rubygems - api_key: - secure: EbS3ZRtfqoKrQ3pMGfkx/pqUBVUaEJE+KjUAnAy4h+6BF/6ZsY2H5vtpuDB8ypQ7au1AF2QuEoZsQZkHPngyhJ7Ebtn7XFh0c5WAB+c+mM7bSsNN+ZU176cUgY5PkS9GZ3rBZ/MEW+YyKcUpTmk+ClDx/WmofRjPFSD4n0x350Y= - gem: pact-mock_service - on: - tags: true - repo: pact-foundation/pact-mock_service -- provider: releases - api_key: - secure: O9g/8HkwonBZOthoN+NFCiZQZ+AyakmqMxb/HpuC/ZB79KZ6GTM0brr++Bm08RYLD2MX6+IC8dqA4vkl4D11VEd7TrtxdtS2huScDGTEntzPtRu2WDo4cm6/B9y/erp7Thalt08+V7dqsSBMN5FWf0c001WrG7qpdWs9BElxul8= - file: pkg/* - file_glob: true - skip_cleanup: true - on: - tags: true - repo: pact-foundation/pact-mock_service -after_deploy: -- bundle exec rake generate_release_notes[$TRAVIS_TAG] -- bundle exec rake upload_release_notes[$TRAVIS_REPO_SLUG,$TRAVIS_TAG] +jobs: + include: + - stage: release + rvm: 2.7.0 + script: echo "Releasing" + deploy: + - provider: rubygems + api_key: + secure: EbS3ZRtfqoKrQ3pMGfkx/pqUBVUaEJE+KjUAnAy4h+6BF/6ZsY2H5vtpuDB8ypQ7au1AF2QuEoZsQZkHPngyhJ7Ebtn7XFh0c5WAB+c+mM7bSsNN+ZU176cUgY5PkS9GZ3rBZ/MEW+YyKcUpTmk+ClDx/WmofRjPFSD4n0x350Y= + gem: pact-mock_service + on: + tags: true + repo: pact-foundation/pact-mock_service + - provider: releases + api_key: + secure: O9g/8HkwonBZOthoN+NFCiZQZ+AyakmqMxb/HpuC/ZB79KZ6GTM0brr++Bm08RYLD2MX6+IC8dqA4vkl4D11VEd7TrtxdtS2huScDGTEntzPtRu2WDo4cm6/B9y/erp7Thalt08+V7dqsSBMN5FWf0c001WrG7qpdWs9BElxul8= + file: pkg/* + file_glob: true + skip_cleanup: true + on: + tags: true + repo: pact-foundation/pact-mock_service + after_deploy: + - bundle exec rake generate_release_notes[$TRAVIS_TAG] + - bundle exec rake upload_release_notes[$TRAVIS_REPO_SLUG,$TRAVIS_TAG] diff --git a/CHANGELOG.md b/CHANGELOG.md index e87d88c..ae8dcba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,130 @@ + +### v3.9.1 (2021-06-03) + +#### Bug Fixes + +* check for nil body rather than falsey body when determining how to render mocked response Fixes: https://github.com/pact-foundation/pact-mock_service/issues/99 ([d26e520](/../../commit/d26e520)) + + +### v3.9.0 (2021-05-17) + +#### Features + +* pass host into WEBrick options to allow configuration (#128) ([ec234a4](/../../commit/ec234a4)) + + +### v3.8.0 (2021-02-25) + +#### Features + +* include interaction diffs in verification response ([6306693](/../../commit/6306693)) + + +### v3.7.0 (2020-11-13) + +#### Features + +* use Pact::Query.parse_string to parse query string ([6cd0733](/../../commit/6cd0733)) +* do not require files until command is executing ([ad54d0b](/../../commit/ad54d0b)) + + +### v3.6.2 (2020-08-10) + +#### Bug Fixes + +* update thor dependency (#124) ([54b3f85](/../../commit/54b3f85)) + + +### v3.6.1 (2020-04-22) + + +#### Bug Fixes + +* fix Ruby 2.7 kwargs warning (#122) ([4a46c21](/../../commit/4a46c21)) + + + +### v3.6.0 (2020-03-14) + + +#### Features + +* add 'Access-Control-Allow-Headers' = true to cors response headers (#121) ([61bd9d1](/../../commit/61bd9d1)) + + + +### v3.5.0 (2020-01-17) + + +#### Features + +* add token, username and password options to stub service (#118) ([76236d8](/../../commit/76236d8)) + + + +### v3.3.1 (2020-01-16) + + +#### Bug Fixes + +* put metadata on the correct decorator ([67ef5a6](/../../commit/67ef5a6)) + + + +### v3.3.0 (2020-01-16) + + +#### Features + +* log a warning when too many interactions are set on the mock service at once ([0ce6bef](/../../commit/0ce6bef)) + + + +### v3.2.1 (2020-01-11) + + +#### Bug Fixes + +* remove apparently unused require for thwait ([4a08fd5](/../../commit/4a08fd5)) + + + +### v3.2.0 (2019-09-19) + + +#### Features + +* **skip writing to pact** + * Use writable_interactions when writing to pact file ([44ea0c3](/../../commit/44ea0c3)) + + + +### v3.1.0 (2019-05-01) + + +#### Features + +* pact-stub-service log level cli opt ([9264a87](/../../commit/9264a87)) + + + +### v3.0.1 (2019-03-08) + + +#### Bug Fixes + +* add missing host argument to server spawn ([ee5cf90](/../../commit/ee5cf90)) + + + +### v3.0.0 (2019-02-21) + + +#### Features + +* allow mock service host to be configured ([7e2d810](/../../commit/7e2d810)) + + ### v2.11.0 (2018-08-28) diff --git a/README.md b/README.md index e67031b..a2c86bc 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # Pact Mock and Stub Service -[![Build Status](https://travis-ci.org/pact-foundation/pact-mock_service.svg?branch=master)](https://travis-ci.org/pact-foundation/pact-mock_service) +[![Build Status](https://travis-ci.com/pact-foundation/pact-mock_service.svg?branch=master)](https://travis-ci.com/pact-foundation/pact-mock_service) This codebase provides the HTTP mock and stub service used by implementations of [Pact][pact]. It is packaged as a gem, and as a standalone executable for Mac OSX and Linux and Windows. diff --git a/lib/pact/consumer/mock_service/cors_origin_header_middleware.rb b/lib/pact/consumer/mock_service/cors_origin_header_middleware.rb index 1697dd8..7bd185d 100644 --- a/lib/pact/consumer/mock_service/cors_origin_header_middleware.rb +++ b/lib/pact/consumer/mock_service/cors_origin_header_middleware.rb @@ -23,7 +23,8 @@ def shutdown private def add_cors_header env, response - [response[0], response[1].merge('Access-Control-Allow-Origin' => env.fetch('HTTP_ORIGIN','*')), response[2]] + cors_headers = { 'Access-Control-Allow-Origin' => env.fetch('HTTP_ORIGIN','*'), 'Access-Control-Allow-Credentials' => 'true'} + [response[0], response[1].merge(cors_headers), response[2]] end end end diff --git a/lib/pact/consumer/mock_service/rack_request_helper.rb b/lib/pact/consumer/mock_service/rack_request_helper.rb index 0d97211..130fc15 100644 --- a/lib/pact/consumer/mock_service/rack_request_helper.rb +++ b/lib/pact/consumer/mock_service/rack_request_helper.rb @@ -1,4 +1,6 @@ require 'cgi/core' +require 'pact/consumer_contract/query' + module Pact module Consumer @@ -11,7 +13,7 @@ module RackRequestHelper } def params_hash env - CGI::parse env["QUERY_STRING"] + Pact::Query.parse_string(env["QUERY_STRING"]) end def request_as_hash_from env diff --git a/lib/pact/consumer/server.rb b/lib/pact/consumer/server.rb index 615b92f..f54c856 100644 --- a/lib/pact/consumer/server.rb +++ b/lib/pact/consumer/server.rb @@ -34,12 +34,13 @@ def ports end end - attr_reader :app, :port, :options + attr_reader :app, :host, :port, :options - def initialize(app, port, options = {}) + def initialize(app, host, port, options = {}) @app = app @middleware = Middleware.new(@app) @server_thread = nil + @host = host @port = port @options = options end @@ -52,10 +53,6 @@ def error @middleware.error end - def host - "localhost" - end - def responsive? return false if @server_thread && @server_thread.join(0) res = get_identity @@ -70,7 +67,7 @@ def responsive? def run_default_server(app, port) require 'rack/handler/webrick' - Rack::Handler::WEBrick.run(app, webrick_opts) do |server| + Rack::Handler::WEBrick.run(app, **webrick_opts) do |server| @port = server[:Port] end end @@ -86,7 +83,7 @@ def get_identity end def webrick_opts - opts = { Port: port.nil? ? 0 : port, AccessLog: [], Logger: WEBrick::Log::new(nil, 0) } + opts = { Host: host.nil? ? 'localhost' : host, Port: port.nil? ? 0 : port, AccessLog: [], Logger: WEBrick::Log::new(nil, 0) } opts.merge!({ :SSLCertificate => OpenSSL::X509::Certificate.new(File.open(options[:sslcert]).read) }) if options[:sslcert] opts.merge!({ @@ -96,7 +93,7 @@ def webrick_opts end def ssl_opts - { SSLEnable: true, SSLCertName: [ %w[CN localhost] ] } + { SSLEnable: true, SSLCertName: [ ["CN", host] ] } end def boot diff --git a/lib/pact/consumer_contract/consumer_contract_decorator.rb b/lib/pact/consumer_contract/consumer_contract_decorator.rb index 2c7e486..b75d4c3 100644 --- a/lib/pact/consumer_contract/consumer_contract_decorator.rb +++ b/lib/pact/consumer_contract/consumer_contract_decorator.rb @@ -32,11 +32,11 @@ def to_json(options = {}) def sorted_interactions # Default order: chronological - return consumer_contract.interactions if Pact.configuration.pactfile_write_order == :chronological + return consumer_contract.writable_interactions if Pact.configuration.pactfile_write_order == :chronological # We are supporting only chronological or alphabetical order raise NotImplementedError if Pact.configuration.pactfile_write_order != :alphabetical - consumer_contract.interactions.sort{|a, b| sortable_id(a) <=> sortable_id(b)} + consumer_contract.writable_interactions.sort{|a, b| sortable_id(a) <=> sortable_id(b)} end def sortable_id interaction diff --git a/lib/pact/mock_service/app.rb b/lib/pact/mock_service/app.rb index 6b1cef8..de91fb4 100644 --- a/lib/pact/mock_service/app.rb +++ b/lib/pact/mock_service/app.rb @@ -15,12 +15,13 @@ def self.new *args end class App - def initialize options = {} logger = Logger.from_options(options) + @options = options + stubbing = options[:stub_pactfile_paths] && options[:stub_pactfile_paths].any? @name = options.fetch(:name, "MockService") - @session = Session.new(options.merge(logger: logger)) - setup_stub(options[:stub_pactfile_paths]) if options[:stub_pactfile_paths] + @session = Session.new(options.merge(logger: logger, warn_on_too_many_interactions: !stubbing)) + setup_stub(options[:stub_pactfile_paths]) if stubbing request_handlers = RequestHandlers.new(@name, logger, @session, options) @app = Rack::Builder.app do use Pact::Consumer::MockService::ErrorHandler, logger @@ -40,7 +41,7 @@ def shutdown def setup_stub stub_pactfile_paths interactions = stub_pactfile_paths.collect do | pactfile_path | $stdout.puts "INFO: Loading interactions from #{pactfile_path}" - hash_interactions = JSON.parse(Pact::PactFile.read(pactfile_path))['interactions'] + hash_interactions = JSON.parse(Pact::PactFile.read(pactfile_path, pactfile_options))['interactions'] hash_interactions.collect { | hash | Interaction.from_hash(hash) } end.flatten @session.set_expected_interactions interactions @@ -57,6 +58,20 @@ def write_pact_if_configured def to_s "#{@name} #{super.to_s}" end + + private + + def pactfile_options + { + :token => broker_token, + :username => @options[:broker_username], + :password => @options[:broker_password], + } + end + + def broker_token + @options[:broker_token] || ENV['PACT_BROKER_TOKEN'] + end end # Can't write to a file in a TRAP, might deadlock diff --git a/lib/pact/mock_service/app_manager.rb b/lib/pact/mock_service/app_manager.rb index bba48b4..3a5c1a8 100644 --- a/lib/pact/mock_service/app_manager.rb +++ b/lib/pact/mock_service/app_manager.rb @@ -1,5 +1,3 @@ -require 'thwait' - require 'net/http' require 'uri' require 'pact/logging' @@ -23,7 +21,6 @@ def initialize def register_mock_service_for(name, url, options = {}) uri = URI(url) raise "Currently only http is supported" unless uri.scheme == 'http' - raise "Currently only services on localhost are supported" unless uri.host == 'localhost' uri.port = nil if options[:find_available_port] app, registration_klass = if options[:standalone] @@ -44,21 +41,21 @@ def register_mock_service_for(name, url, options = {}) pact_specification_version: options[:pact_specification_version] ), 'AppRegistration'] end - register(app, uri.port, registration_klass) + register(app, uri.host, uri.port, registration_klass) end - def register(app, port = nil, registration_klass = 'AppRegistration') + def register(app, host, port = nil, registration_klass = 'AppRegistration') if port - existing = existing_app_on_port(port) + existing = existing_app_on_host_and_port(host, port) raise "Port #{port} is already being used by #{existing}" if existing and not existing == app end - app_registration = register_app(app, port, registration_klass) + app_registration = register_app(app, host, port, registration_klass) app_registration.spawn app_registration.port end - def ports_of_mock_services - app_registrations.find_all(&:is_a_mock_service?).collect(&:port) + def urls_of_mock_services + app_registrations.find_all(&:is_a_mock_service?).collect{ |ar| "http://#{ar.host}:#{ar.port}" } end def kill_all @@ -82,13 +79,13 @@ def app_registered_on?(port) private - def existing_app_on_port(port) - app_registration = registration_on_port(port) + def existing_app_on_host_and_port(host, port) + app_registration = registration_on_host_and_port(host, port) app_registration ? app_registration.app : nil end - def registration_on_port(port) - @app_registrations.find { |app_registration| app_registration.port == port } + def registration_on_host_and_port(host, port) + @app_registrations.find { |app_registration| app_registration.port == port && app_registration.host == host } end def pact_dir @@ -119,8 +116,8 @@ def app_registrations @app_registrations end - def register_app(app, port, registration_klass) - app_registration = Pact::MockService::const_get(registration_klass).new(app: app, port: port) + def register_app(app, host, port, registration_klass) + app_registration = Pact::MockService::const_get(registration_klass).new(app: app, host: host, port: port) app_registrations << app_registration app_registration end @@ -128,11 +125,12 @@ def register_app(app, port, registration_klass) class AppRegistration include Pact::Logging - attr_accessor :port, :app + attr_accessor :host, :port, :app def initialize(opts) @max_wait = 10 @port = opts[:port] + @host = opts[:host] @app = opts[:app] @spawned = false end @@ -160,7 +158,7 @@ def to_s def spawn logger.info "Starting app #{self}..." - @server = Pact::Server.new(app, port).boot + @server = Pact::Server.new(app, host, port).boot @port = @server.port @spawned = true logger.info "Started on port #{port}" diff --git a/lib/pact/mock_service/cli.rb b/lib/pact/mock_service/cli.rb index d709d96..5aaec32 100755 --- a/lib/pact/mock_service/cli.rb +++ b/lib/pact/mock_service/cli.rb @@ -1,14 +1,11 @@ require 'thor' -require 'webrick/https' -require 'rack/handler/webrick' -require 'fileutils' -require 'pact/mock_service/server/wait_for_server_up' -require 'pact/mock_service/cli/pidfile' -require 'socket' module Pact module MockService class CLI < Thor + def self.exit_on_failure? # Thor 1.0 deprecation guard + false + end PACT_FILE_WRITE_MODE_DESC = "`overwrite` or `merge`. Use `merge` when running multiple mock service instances in parallel for the same consumer/provider pair." + " Ensure the pact file is deleted before running tests when using this option so that interactions deleted from the code are not maintained in the file." @@ -30,6 +27,7 @@ class CLI < Thor method_option :monkeypatch, hide: true def service + require_common_dependencies require 'pact/mock_service/run' Run.(options) end @@ -48,6 +46,7 @@ def service method_option :sslkey, desc: "Specify the path to the SSL key to use when running the service over HTTPS" def control + require_common_dependencies require 'pact/mock_service/control_server/run' ControlServer::Run.(options) end @@ -70,6 +69,7 @@ def control method_option :monkeypatch, hide: true def start + require_common_dependencies start_server(mock_service_pidfile) do service end @@ -80,6 +80,7 @@ def start method_option :pid_dir, desc: "PID dir, defaults to tmp/pids", default: "tmp/pids" def stop + require_common_dependencies mock_service_pidfile.kill_process end @@ -100,6 +101,7 @@ def stop method_option :sslkey, desc: "Specify the path to the SSL key to use when running the service over HTTPS" def restart + require_common_dependencies restart_server(mock_service_pidfile) do service end @@ -107,6 +109,7 @@ def restart desc 'control-start', "Start a Pact mock service control server." method_option :port, aliases: "-p", desc: "Port on which to run the service", default: '1234' + method_option :host, aliases: "-h", desc: "Host on which to bind the service", default: 'localhost' method_option :log_dir, aliases: "-l", desc: "File to which to log output", default: "log" method_option :log_level, desc: "Log level. Options are DEBUG INFO WARN ERROR", default: "DEBUG" method_option :pact_file_write_mode, aliases: "-m", desc: PACT_FILE_WRITE_MODE_DESC, type: :string, default: 'overwrite' @@ -119,6 +122,7 @@ def restart method_option :pact_dir, aliases: "-d", desc: "Directory to which the pacts will be written", default: "." def control_start + require_common_dependencies start_server(control_server_pidfile) do control end @@ -129,11 +133,13 @@ def control_start method_option :pid_dir, desc: "PID dir, defaults to tmp/pids", default: "tmp/pids" def control_stop + require_common_dependencies control_server_pidfile.kill_process end desc 'control-restart', "Start a Pact mock service control server." method_option :port, aliases: "-p", desc: "Port on which to run the service", default: '1234' + method_option :host, aliases: "-h", desc: "Host on which to bind the service", default: 'localhost' method_option :log_dir, aliases: "-l", desc: "File to which to log output", default: "log" method_option :log_level, desc: "Log level. Options are DEBUG INFO WARN ERROR", default: "DEBUG" method_option :pact_dir, aliases: "-d", desc: "Directory to which the pacts will be written", default: "." @@ -146,6 +152,7 @@ def control_stop method_option :sslkey, desc: "Specify the path to the SSL key to use when running the service over HTTPS" def control_restart + require_common_dependencies restart_server(control_server_pidfile) do control end @@ -162,6 +169,15 @@ def version no_commands do + def require_common_dependencies + require 'webrick/https' + require 'rack/handler/webrick' + require 'fileutils' + require 'pact/mock_service/server/wait_for_server_up' + require 'pact/mock_service/cli/pidfile' + require 'socket' + end + def control_server_pidfile Pidfile.new(pid_dir: options[:pid_dir], name: control_pidfile_name) end @@ -180,14 +196,14 @@ def control_pidfile_name def start_server pidfile require 'pact/mock_service/server/spawn' - Pact::MockService::Server::Spawn.(pidfile, options[:port], options[:ssl]) do + Pact::MockService::Server::Spawn.(pidfile, options[:host], options[:port], options[:ssl]) do yield end end def restart_server pidfile require 'pact/mock_service/server/respawn' - Pact::MockService::Server::Respawn.(pidfile, options[:port], options[:ssl]) do + Pact::MockService::Server::Respawn.(pidfile, options[:host], options[:port], options[:ssl]) do yield end end diff --git a/lib/pact/mock_service/cli/custom_thor.rb b/lib/pact/mock_service/cli/custom_thor.rb index 7869c02..b18e939 100644 --- a/lib/pact/mock_service/cli/custom_thor.rb +++ b/lib/pact/mock_service/cli/custom_thor.rb @@ -11,6 +11,9 @@ class CLI < Thor # `script --help` to display the help for the default task instead of the command list # class CustomThor < ::Thor + def self.exit_on_failure? # Thor 1.0 deprecation guard + false + end no_commands do def self.start given_args = ARGV, config = {} diff --git a/lib/pact/mock_service/cli/pidfile.rb b/lib/pact/mock_service/cli/pidfile.rb index 07f1a71..fc7b8cc 100644 --- a/lib/pact/mock_service/cli/pidfile.rb +++ b/lib/pact/mock_service/cli/pidfile.rb @@ -3,9 +3,11 @@ module Pact module MockService class CLI < Thor + def self.exit_on_failure? # Thor 1.0 deprecation guard + false + end class Pidfile - attr_accessor :pid_dir, :name, :pid def initialize options diff --git a/lib/pact/mock_service/client.rb b/lib/pact/mock_service/client.rb index b699ea8..cb1e137 100644 --- a/lib/pact/mock_service/client.rb +++ b/lib/pact/mock_service/client.rb @@ -13,8 +13,8 @@ class Client MOCK_SERVICE_ADMINISTRATON_HEADERS = {'X-Pact-Mock-Service' => 'true'} - def initialize port - @http = Net::HTTP.new('localhost', port) + def initialize port, host = 'localhost' + @http = Net::HTTP.new(host, port) end def verify example_description @@ -46,8 +46,9 @@ def add_expected_interaction interaction raise AddInteractionError.new("\e[31m#{response.body}\e[m") unless response.is_a? Net::HTTPSuccess end - def self.clear_interactions port, example_description - Net::HTTP.new("localhost", port).delete("/interactions?example_description=#{CGI.escape(example_description)}", MOCK_SERVICE_ADMINISTRATON_HEADERS) + def self.clear_interactions mock_service_base_url, example_description + uri = URI(mock_service_base_url) + Net::HTTP.new(uri.host, uri.port).delete("/interactions?example_description=#{CGI.escape(example_description)}", MOCK_SERVICE_ADMINISTRATON_HEADERS) end def write_pact pacticipant_details diff --git a/lib/pact/mock_service/control_server/mock_service_creator.rb b/lib/pact/mock_service/control_server/mock_service_creator.rb index 56b708b..9dc6564 100644 --- a/lib/pact/mock_service/control_server/mock_service_creator.rb +++ b/lib/pact/mock_service/control_server/mock_service_creator.rb @@ -8,7 +8,6 @@ module Pact module MockService module ControlServer - class MockServiceCreator attr_reader :options @@ -22,7 +21,7 @@ def call env consumer_name = env['HTTP_X_PACT_CONSUMER'] provider_name = env['HTTP_X_PACT_PROVIDER'] port = FindAPort.available_port - mock_service = Pact::MockService::Spawn.(consumer_name, provider_name, port, options) + mock_service = Pact::MockService::Spawn.(consumer_name, provider_name, options[:host] || 'localhost', port, options) delegator = Delegator.new(mock_service, consumer_name, provider_name) @mock_services.add(delegator) delegator.call(env) diff --git a/lib/pact/mock_service/control_server/run.rb b/lib/pact/mock_service/control_server/run.rb index 1184408..0524e2b 100644 --- a/lib/pact/mock_service/control_server/run.rb +++ b/lib/pact/mock_service/control_server/run.rb @@ -24,7 +24,7 @@ def call # server, and can't shut it down. So, keep a manual reference to the Webrick server, and # shut it down directly rather than use Rack::Handler::WEBrick.shutdown # Ruby! - Rack::Handler::WEBrick.run(control_server, webbrick_opts) do | server | + Rack::Handler::WEBrick.run(control_server, **webbrick_opts) do | server | @webrick_server = server end end @@ -55,6 +55,7 @@ def control_server_options unique_pact_file_names: options[:unique_pact_file_names], cors_enabled: options[:cors] || false, ssl: options[:ssl], + host: options[:host], pact_specification_version: options[:pact_specification_version] } end diff --git a/lib/pact/mock_service/interaction_decorator.rb b/lib/pact/mock_service/interaction_decorator.rb index 3c5fb29..0ae0eb6 100644 --- a/lib/pact/mock_service/interaction_decorator.rb +++ b/lib/pact/mock_service/interaction_decorator.rb @@ -28,6 +28,7 @@ def to_hash hash[:providerState] = interaction.provider_state if interaction.provider_state hash[:request] = decorate_request.as_json hash[:response] = decorate_response.as_json + hash[:metadata] = interaction.metadata hash end diff --git a/lib/pact/mock_service/interactions/verification.rb b/lib/pact/mock_service/interactions/verification.rb index 6012064..a3b4a9a 100644 --- a/lib/pact/mock_service/interactions/verification.rb +++ b/lib/pact/mock_service/interactions/verification.rb @@ -38,6 +38,10 @@ def missing_interactions expected_interactions - actual_interactions.matched_interactions - @actual_interactions.interaction_mismatches.collect(&:candidate_interactions).flatten end + def interaction_mismatches + actual_interactions.interaction_mismatches + end + private attr_reader :expected_interactions, :actual_interactions diff --git a/lib/pact/mock_service/request_handlers/interaction_replay.rb b/lib/pact/mock_service/request_handlers/interaction_replay.rb index 09986ab..ab0a2ce 100644 --- a/lib/pact/mock_service/request_handlers/interaction_replay.rb +++ b/lib/pact/mock_service/request_handlers/interaction_replay.rb @@ -13,7 +13,11 @@ module RequestHandlers module PrettyGenerate #Doesn't seem to reliably pretty generate unless we go to JSON and back again :( def pretty_generate object - JSON.pretty_generate(JSON.parse(object.to_json)) + begin + JSON.pretty_generate(JSON.parse(object.to_json)) + rescue + object.to_s + end end end @@ -178,7 +182,7 @@ def self.response_from response end def self.render_body body - return '' unless body + return '' if body.nil? body.kind_of?(String) ? body.force_encoding('utf-8') : body.to_json end end diff --git a/lib/pact/mock_service/request_handlers/verification_get.rb b/lib/pact/mock_service/request_handlers/verification_get.rb index ff6d6a3..dd19925 100644 --- a/lib/pact/mock_service/request_handlers/verification_get.rb +++ b/lib/pact/mock_service/request_handlers/verification_get.rb @@ -52,7 +52,7 @@ def initialize verification def to_s titles_and_summaries.collect do | title, summaries | "#{title}:\n\t#{summaries.join("\n\t")}\n\n" if summaries.any? - end.compact.join + end.compact.join + verification.interaction_mismatches.collect(&:to_s).join("\n\n") + "\n" end diff --git a/lib/pact/mock_service/run.rb b/lib/pact/mock_service/run.rb index 1dd9bb5..6146084 100644 --- a/lib/pact/mock_service/run.rb +++ b/lib/pact/mock_service/run.rb @@ -25,7 +25,7 @@ def call require_monkeypatch - Rack::Handler::WEBrick.run(mock_service, webbrick_opts) + Rack::Handler::WEBrick.run(mock_service, **webbrick_opts) end private @@ -58,6 +58,9 @@ def service_options unique_pact_file_names: options[:unique_pact_file_names], consumer: options[:consumer], provider: options[:provider], + broker_token: options[:broker_token], + broker_username: options[:broker_username], + broker_password: options[:broker_password], cors_enabled: options[:cors], pact_specification_version: options[:pact_specification_version] || Pact::SpecificationVersion::NIL_VERSION.to_s, pactfile_write_mode: options[:pact_file_write_mode], @@ -98,7 +101,7 @@ def webbrick_opts def ssl_opts { :SSLEnable => true, - :SSLCertName => [ %w[CN localhost] ] + :SSLCertName => [ ["CN", host] ] } end diff --git a/lib/pact/mock_service/server/spawn.rb b/lib/pact/mock_service/server/spawn.rb index f1a6273..6056e05 100644 --- a/lib/pact/mock_service/server/spawn.rb +++ b/lib/pact/mock_service/server/spawn.rb @@ -7,15 +7,15 @@ class Spawn class PortUnavailableError < StandardError; end - def self.call pidfile, port, ssl = false + def self.call pidfile, host, port, ssl = false if pidfile.can_start? - if port_available? port + if port_available? host, port pid = fork do yield end pidfile.pid = pid Process.detach(pid) - Server::WaitForServerUp.(port, {ssl: ssl}) + Server::WaitForServerUp.(host, port, {ssl: ssl}) pidfile.write else raise PortUnavailableError.new("ERROR: Port #{port} already in use.") @@ -23,8 +23,8 @@ def self.call pidfile, port, ssl = false end end - def self.port_available? port - server = TCPServer.new('127.0.0.1', port) + def self.port_available? host, port + server = TCPServer.new(host, port) true rescue false diff --git a/lib/pact/mock_service/server/wait_for_server_up.rb b/lib/pact/mock_service/server/wait_for_server_up.rb index 3778823..974023d 100644 --- a/lib/pact/mock_service/server/wait_for_server_up.rb +++ b/lib/pact/mock_service/server/wait_for_server_up.rb @@ -7,18 +7,18 @@ module MockService module Server class WaitForServerUp - def self.call(port, options = {ssl: false}) + def self.call(host, port, options = {ssl: false}) tries = 0 responsive = false - while !(responsive = responsive?(port, options)) && tries < 100 + while !(responsive = responsive?(host, port, options)) && tries < 100 tries += 1 sleep 1 end raise "Timed out waiting for server to start up on port #{port}" if !responsive end - def self.responsive? port, options - http = Net::HTTP.new('localhost', port) + def self.responsive? host, port, options + http = Net::HTTP.new(host, port) if options[:ssl] http.use_ssl = true http.verify_mode = OpenSSL::SSL::VERIFY_NONE @@ -27,7 +27,7 @@ def self.responsive? port, options scheme = 'http' end http.start { - request = Net::HTTP::Get.new "#{scheme}://localhost:#{port}/" + request = Net::HTTP::Get.new "#{scheme}://#{host}:#{port}/" request['X-Pact-Mock-Service'] = true response = http.request request response.code == '200' diff --git a/lib/pact/mock_service/session.rb b/lib/pact/mock_service/session.rb index b193ae0..54923bc 100644 --- a/lib/pact/mock_service/session.rb +++ b/lib/pact/mock_service/session.rb @@ -17,6 +17,8 @@ def initialize options @expected_interactions = Interactions::ExpectedInteractions.new @actual_interactions = Interactions::ActualInteractions.new @verified_interactions = Interactions::VerifiedInteractions.new + @warn_on_too_many_interactions = options[:warn_on_too_many_interactions] || false + @max_concurrent_interactions_before_warning = get_max_concurrent_interactions_before_warning @consumer_contract_details = { pact_dir: options[:pact_dir], consumer: {name: options[:consumer]}, @@ -64,10 +66,15 @@ def add_expected_interaction interaction private + attr_reader :warn_on_too_many_interactions, :max_concurrent_interactions_before_warning + def really_add_expected_interaction interaction expected_interactions << interaction logger.info "Registered expected interaction #{interaction.request.method_and_path}" logger.debug JSON.pretty_generate InteractionDecorator.new(interaction) + if warn_on_too_many_interactions && expected_interactions.size > max_concurrent_interactions_before_warning + logger.warn "You currently have #{expected_interactions.size} interactions mocked at the same time. This suggests the scope of your consumer tests is larger than recommended, and you may find them hard to debug and maintain. See https://pact.io/too-many-interactions for more information." + end end def handle_almost_duplicate_interaction previous_interaction, interaction @@ -81,6 +88,9 @@ def interaction_already_verified_with_same_description_and_provider_state_but_no other && other != interaction ? other : nil end + def get_max_concurrent_interactions_before_warning + ENV['PACT_MAX_CONCURRENT_INTERACTIONS_BEFORE_WARNING'] ? ENV['PACT_MAX_CONCURRENT_INTERACTIONS_BEFORE_WARNING'].to_i : 3 + end end end end diff --git a/lib/pact/mock_service/spawn.rb b/lib/pact/mock_service/spawn.rb index ce1aa40..369841c 100644 --- a/lib/pact/mock_service/spawn.rb +++ b/lib/pact/mock_service/spawn.rb @@ -8,15 +8,16 @@ module Pact module MockService class Spawn - def self.call consumer, provider, port, options - new(consumer, provider, port, options).call + def self.call consumer, provider, host, port, options + new(consumer, provider, host, port, options).call end - attr_reader :consumer, :provider, :port, :options + attr_reader :consumer, :provider, :host, :port, :options - def initialize consumer, provider, port, options + def initialize consumer, provider, host, port, options @consumer = consumer @provider = provider + @host = host @port = port @options = options end @@ -49,7 +50,7 @@ def mock_service end def start_mock_service app, port - Pact::Server.new(app, port, ssl: options[:ssl]).boot + Pact::Server.new(app, host, port, ssl: options[:ssl]).boot end def create_log_file @@ -73,7 +74,7 @@ def log_file_path end def base_url - options[:ssl] ? "https://localhost:#{port}" : "http://localhost:#{port}" + options[:ssl] ? "https://#{host}:#{port}" : "http://#{host}:#{port}" end def name diff --git a/lib/pact/mock_service/version.rb b/lib/pact/mock_service/version.rb index d17e309..c3076b9 100644 --- a/lib/pact/mock_service/version.rb +++ b/lib/pact/mock_service/version.rb @@ -1,5 +1,5 @@ module Pact module MockService - VERSION = "2.12.0" + VERSION = "3.9.1" end end diff --git a/lib/pact/stub_service/cli.rb b/lib/pact/stub_service/cli.rb index 75e63ef..79ae35c 100755 --- a/lib/pact/stub_service/cli.rb +++ b/lib/pact/stub_service/cli.rb @@ -27,6 +27,10 @@ class CLI < Pact::MockService::CLI::CustomThor method_option :port, aliases: "-p", desc: "Port on which to run the service" method_option :host, aliases: "-h", desc: "Host on which to bind the service", default: 'localhost' method_option :log, aliases: "-l", desc: "File to which to log output" + method_option :broker_username, aliases: "-n", desc: "Pact Broker basic auth username", :required => false + method_option :broker_password, aliases: "-p", desc: "Pact Broker basic auth password", :required => false + method_option :broker_token, aliases: "-k", desc: "Pact Broker bearer token (can also be set using the PACT_BROKER_TOKEN environment variable)", :required => false + method_option :log_level, desc: "Log level. Options are DEBUG INFO WARN ERROR", default: "DEBUG" method_option :cors, aliases: "-o", desc: "Support browser security in tests by responding to OPTIONS requests and adding CORS headers to mocked responses" method_option :ssl, desc: "Use a self-signed SSL cert to run the service over HTTPS", type: :boolean, default: false method_option :sslcert, desc: "Specify the path to the SSL cert to use when running the service over HTTPS" diff --git a/pact-mock_service.gemspec b/pact-mock_service.gemspec index d2a4fe8..51da6d6 100644 --- a/pact-mock_service.gemspec +++ b/pact-mock_service.gemspec @@ -23,15 +23,15 @@ Gem::Specification.new do |gem| gem.add_runtime_dependency 'rack', '>= 1.6' gem.add_runtime_dependency 'rspec', '>=2.14' gem.add_runtime_dependency 'find_a_port', '~> 1.0.1' - gem.add_runtime_dependency 'thor', '~> 0.19' + gem.add_runtime_dependency 'thor', '>= 0.19', '< 2.0' gem.add_runtime_dependency 'json' gem.add_runtime_dependency 'webrick', '~> 1.3' gem.add_runtime_dependency 'term-ansicolor', '~> 1.0' - gem.add_runtime_dependency 'pact-support', '~> 1.2', '>= 1.2.1' + gem.add_runtime_dependency 'pact-support', '~> 1.16', '>= 1.16.4' gem.add_runtime_dependency 'filelock', '~> 1.1' gem.add_development_dependency 'rack-test', '~> 0.7' - gem.add_development_dependency 'rake', '~> 10.0.3' + gem.add_development_dependency 'rake', '~> 13.0', '>= 13.0.1' gem.add_development_dependency 'webmock', '~> 3.4' gem.add_development_dependency 'pry' gem.add_development_dependency 'fakefs', '~> 0.4' diff --git a/script/trigger-release.sh b/script/trigger-release.sh new file mode 100755 index 0000000..6c6f057 --- /dev/null +++ b/script/trigger-release.sh @@ -0,0 +1,30 @@ +#!/bin/sh + +# 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 -X POST 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/administration_endpoints_cors_spec.rb b/spec/features/administration_endpoints_cors_spec.rb index 1625ced..da23c4a 100644 --- a/spec/features/administration_endpoints_cors_spec.rb +++ b/spec/features/administration_endpoints_cors_spec.rb @@ -85,21 +85,25 @@ it "includes the CORS headers in the response to DELETE /interactions" do | example | delete "/interactions", nil, admin_headers expect(last_response.headers['Access-Control-Allow-Origin']).to eq '*' + expect(last_response.headers['Access-Control-Allow-Credentials']).to eq 'true' end it "includes the CORS headers in the response to POST /interactions" do | example | post "/interactions", expected_interaction, admin_headers expect(last_response.headers['Access-Control-Allow-Origin']).to eq '*' + expect(last_response.headers['Access-Control-Allow-Credentials']).to eq 'true' end it "includes the CORS headers in the response to POST /pact" do | example | post "/pact", pact_details, admin_headers expect(last_response.headers['Access-Control-Allow-Origin']).to eq '*' + expect(last_response.headers['Access-Control-Allow-Credentials']).to eq 'true' end it "includes the CORS headers in the response to GET /interactions/verification" do | example | get "/interactions/verification", nil, admin_headers expect(last_response.headers['Access-Control-Allow-Origin']).to eq '*' + expect(last_response.headers['Access-Control-Allow-Credentials']).to eq 'true' end context "when the Origin header is set" do diff --git a/spec/features/log/mock_multiple_responses_spec.log b/spec/features/log/mock_multiple_responses_spec.log index d11b5dc..a930e5c 100644 --- a/spec/features/log/mock_multiple_responses_spec.log +++ b/spec/features/log/mock_multiple_responses_spec.log @@ -1,4 +1,4 @@ -INFO -- : Cleared interactions for example "Pact::Consumer::MockService when more than one response has been mocked when the actual request matches more than one expected request returns an error response" +INFO -- : Cleared interactions for example "Pact::Consumer::MockService when more than one response has been mocked when the actual request matches one expected request returns the expected response" INFO -- : Registered expected interaction GET /alligators DEBUG -- : { "description": "a request for alligators", @@ -20,15 +20,16 @@ DEBUG -- : { "name": "Mary" } ] - } + }, + "metadata": null } -INFO -- : Registered expected interaction GET /alligators +INFO -- : Registered expected interaction GET /zebras DEBUG -- : { - "description": "a request for alligators", - "providerState": "there are no alligators", + "description": "a request for zebras", + "providerState": "there are zebras", "request": { "method": "get", - "path": "/alligators", + "path": "/zebras", "headers": { "Accept": "application/json" } @@ -39,9 +40,12 @@ DEBUG -- : { "Content-Type": "application/json" }, "body": [ - + { + "name": "Xena Zebra" + } ] - } + }, + "metadata": null } INFO -- : Received request GET /alligators DEBUG -- : { @@ -56,7 +60,46 @@ DEBUG -- : { "Cookie": "" } } -ERROR -- : Multiple interactions found for GET /alligators: +INFO -- : Found matching response for GET /alligators +DEBUG -- : { + "status": 200, + "headers": { + "Content-Type": "application/json" + }, + "body": [ + { + "name": "Mary" + } + ] +} +INFO -- : Received request GET /zebras +DEBUG -- : { + "method": "get", + "query": "", + "path": "/zebras", + "headers": { + "Https": "off", + "Content-Length": "0", + "Accept": "application/json", + "Host": "example.org", + "Cookie": "" + } +} +INFO -- : Found matching response for GET /zebras +DEBUG -- : { + "status": 200, + "headers": { + "Content-Type": "application/json" + }, + "body": [ + { + "name": "Xena Zebra" + } + ] +} +INFO -- : Verifying - interactions matched for example "Pact::Consumer::MockService when more than one response has been mocked when the actual request matches one expected request returns the expected response" +INFO -- : Cleared interactions for example "Pact::Consumer::MockService when more than one response has been mocked when the actual request matches more than one expected request returns an error response" +INFO -- : Registered expected interaction GET /alligators DEBUG -- : { "description": "a request for alligators", "providerState": "alligators exist", @@ -77,8 +120,10 @@ DEBUG -- : { "name": "Mary" } ] - } + }, + "metadata": null } +INFO -- : Registered expected interaction GET /alligators DEBUG -- : { "description": "a request for alligators", "providerState": "there are no alligators", @@ -97,21 +142,23 @@ DEBUG -- : { "body": [ ] + }, + "metadata": null +} +INFO -- : Received request GET /alligators +DEBUG -- : { + "method": "get", + "query": "", + "path": "/alligators", + "headers": { + "Https": "off", + "Content-Length": "0", + "Accept": "application/json", + "Host": "example.org", + "Cookie": "" } } -WARN -- : Verifying - actual interactions do not match expected interactions for example "Pact::Consumer::MockService when more than one response has been mocked when the actual request matches more than one expected request returns an error response". -Missing requests: - GET /alligators - GET /alligators - - -WARN -- : Missing requests: - GET /alligators - GET /alligators - - -INFO -- : Cleared interactions for example "Pact::Consumer::MockService when more than one response has been mocked when the actual request matches one expected request returns the expected response" -INFO -- : Registered expected interaction GET /alligators +ERROR -- : Multiple interactions found for GET /alligators: DEBUG -- : { "description": "a request for alligators", "providerState": "alligators exist", @@ -132,15 +179,15 @@ DEBUG -- : { "name": "Mary" } ] - } + }, + "metadata": null } -INFO -- : Registered expected interaction GET /zebras DEBUG -- : { - "description": "a request for zebras", - "providerState": "there are zebras", + "description": "a request for alligators", + "providerState": "there are no alligators", "request": { "method": "get", - "path": "/zebras", + "path": "/alligators", "headers": { "Accept": "application/json" } @@ -151,60 +198,19 @@ DEBUG -- : { "Content-Type": "application/json" }, "body": [ - { - "name": "Xena Zebra" - } + ] - } -} -INFO -- : Received request GET /alligators -DEBUG -- : { - "method": "get", - "query": "", - "path": "/alligators", - "headers": { - "Https": "off", - "Content-Length": "0", - "Accept": "application/json", - "Host": "example.org", - "Cookie": "" - } -} -INFO -- : Found matching response for GET /alligators -DEBUG -- : { - "status": 200, - "headers": { - "Content-Type": "application/json" - }, - "body": [ - { - "name": "Mary" - } - ] -} -INFO -- : Received request GET /zebras -DEBUG -- : { - "method": "get", - "query": "", - "path": "/zebras", - "headers": { - "Https": "off", - "Content-Length": "0", - "Accept": "application/json", - "Host": "example.org", - "Cookie": "" - } -} -INFO -- : Found matching response for GET /zebras -DEBUG -- : { - "status": 200, - "headers": { - "Content-Type": "application/json" }, - "body": [ - { - "name": "Xena Zebra" - } - ] + "metadata": null } -INFO -- : Verifying - interactions matched for example "Pact::Consumer::MockService when more than one response has been mocked when the actual request matches one expected request returns the expected response" +WARN -- : Verifying - actual interactions do not match expected interactions for example "Pact::Consumer::MockService when more than one response has been mocked when the actual request matches more than one expected request returns an error response". +Missing requests: + GET /alligators + GET /alligators + + +WARN -- : Missing requests: + GET /alligators + GET /alligators + + diff --git a/spec/features/log/mock_one_response_spec.log b/spec/features/log/mock_one_response_spec.log index 9b6f24c..31bcfa7 100644 --- a/spec/features/log/mock_one_response_spec.log +++ b/spec/features/log/mock_one_response_spec.log @@ -1,4 +1,4 @@ -INFO -- : Cleared interactions for example "Pact::Consumer::MockService when a response has been mocked when the actual request does not match the expected request returns an error response" +INFO -- : Cleared interactions for example "Pact::Consumer::MockService when a response has been mocked when the actual request matches the expected request returns the expected response" INFO -- : Registered expected interaction GET /alligators DEBUG -- : { "description": "a request for alligators", @@ -20,7 +20,8 @@ DEBUG -- : { "name": "Mary" } ] - } + }, + "metadata": null } INFO -- : Received request GET /alligators DEBUG -- : { @@ -30,41 +31,25 @@ DEBUG -- : { "headers": { "Https": "off", "Content-Length": "0", - "Accept": "application/xml", + "Accept": "application/json", "Host": "example.org", "Cookie": "" } } -ERROR -- : No matching interaction found for GET /alligators -ERROR -- : Interaction diffs for that route: -ERROR -- : Diff with interaction: "a request for alligators" given "alligators exist" -Diff --------------------------------------- -Key: - is expected - + is actual -Matching keys and values are not shown - - { - "headers": { -- "Accept": "application/json" -+ "Accept": "application/xml" - } - } - -Description of differences --------------------------------------- -* Expected "application/json" but got "application/xml" at $.headers.Accept - -WARN -- : Verifying - actual interactions do not match expected interactions for example "Pact::Consumer::MockService when a response has been mocked when the actual request does not match the expected request returns an error response". -Incorrect requests: - GET /alligators (request headers did not match) - - -WARN -- : Incorrect requests: - GET /alligators (request headers did not match) - - -INFO -- : Cleared interactions for example "Pact::Consumer::MockService when a response has been mocked when the actual request matches the expected request returns the expected response" +INFO -- : Found matching response for GET /alligators +DEBUG -- : { + "status": 200, + "headers": { + "Content-Type": "application/json" + }, + "body": [ + { + "name": "Mary" + } + ] +} +INFO -- : Verifying - interactions matched for example "Pact::Consumer::MockService when a response has been mocked when the actual request matches the expected request returns the expected response" +INFO -- : Cleared interactions for example "Pact::Consumer::MockService when a response has been mocked when the actual request does not match the expected request returns an error response" INFO -- : Registered expected interaction GET /alligators DEBUG -- : { "description": "a request for alligators", @@ -86,7 +71,8 @@ DEBUG -- : { "name": "Mary" } ] - } + }, + "metadata": null } INFO -- : Received request GET /alligators DEBUG -- : { @@ -96,21 +82,37 @@ DEBUG -- : { "headers": { "Https": "off", "Content-Length": "0", - "Accept": "application/json", + "Accept": "application/xml", "Host": "example.org", "Cookie": "" } } -INFO -- : Found matching response for GET /alligators -DEBUG -- : { - "status": 200, - "headers": { - "Content-Type": "application/json" - }, - "body": [ - { - "name": "Mary" - } - ] -} -INFO -- : Verifying - interactions matched for example "Pact::Consumer::MockService when a response has been mocked when the actual request matches the expected request returns the expected response" +ERROR -- : No matching interaction found for GET /alligators +ERROR -- : Interaction diffs for that route: +ERROR -- : Diff with interaction: "a request for alligators" given "alligators exist" +Diff +-------------------------------------- +Key: - is expected + + is actual +Matching keys and values are not shown + + { + "headers": { +- "Accept": "application/json" ++ "Accept": "application/xml" + } + } + +Description of differences +-------------------------------------- +* Expected "application/json" but got "application/xml" at $.headers.Accept + +WARN -- : Verifying - actual interactions do not match expected interactions for example "Pact::Consumer::MockService when a response has been mocked when the actual request does not match the expected request returns an error response". +Incorrect requests: + GET /alligators (request headers did not match) + + +WARN -- : Incorrect requests: + GET /alligators (request headers did not match) + + diff --git a/spec/features/mock_interactions_with_cors_spec.rb b/spec/features/mock_interactions_with_cors_spec.rb index 9dac90f..3bbdd81 100644 --- a/spec/features/mock_interactions_with_cors_spec.rb +++ b/spec/features/mock_interactions_with_cors_spec.rb @@ -61,6 +61,7 @@ # Ensure it allows the browser to actually make the request expect(last_response.status).to eq 200 expect(last_response.headers['Access-Control-Allow-Origin']).to eq 'http://localhost:1234' + expect(last_response.headers['Access-Control-Allow-Credentials']).to eq 'true' expect(last_response.headers['Access-Control-Allow-Headers']).to include 'accept' expect(last_response.headers['Access-Control-Allow-Methods']).to include "DELETE, POST, GET, HEAD, PUT, TRACE, CONNECT" diff --git a/spec/features/write_pact_file_spec.rb b/spec/features/write_pact_file_spec.rb index 07aaa4d..f2fe405 100644 --- a/spec/features/write_pact_file_spec.rb +++ b/spec/features/write_pact_file_spec.rb @@ -69,6 +69,9 @@ expect(pact_json['interactions']).to_not include( include("description" => "a request for alligators") ) + expect(pact_json['interactions']).to_not include( + include("metadata" => nil) + ) end end @@ -79,6 +82,53 @@ expect(pact_json['interactions']).to include( include("description" => "a request for alligators") ) + expect(pact_json['interactions']).to_not include( + include("metadata" => nil) + ) + end + end + + context "when the expected interaction is executed but is marked to not be written to the pact file" do + let(:zebra_interaction) do + { + description: "a request for zebras", + provider_state: "zebras exist", + request: { + method: :get, + path: '/zebras', + headers: {'Accept' => 'application/zebra'} + }, + response: { + status: 200 + }, + metadata: { + write_to_pact: false + } + }.to_json + end + + before do + post "/interactions", zebra_interaction, admin_headers + end + + it "does not include the interaction in the pact file" do + get "/alligators", nil, {'HTTP_ACCEPT' => 'application/alligator'} + get "/giraffes", nil, {'HTTP_ACCEPT' => 'application/giraffe'} + get "/zebras", nil, {'HTTP_ACCEPT' => 'application/zebra'} + post "/pact", pact_details, admin_headers + + expect(pact_json['interactions']).to include( + include("description" => "a request for alligators") + ) + expect(pact_json['interactions']).to include( + include("description" => "a request for giraffes") + ) + expect(pact_json['interactions']).to_not include( + include("description" => "a request for zebras") + ) + expect(pact_json['interactions']).to_not include( + include("metadata" => nil) + ) end end diff --git a/spec/lib/pact/consumer/mock_service/verification_get_spec.rb b/spec/lib/pact/consumer/mock_service/verification_get_spec.rb index 4268db4..58d1e22 100644 --- a/spec/lib/pact/consumer/mock_service/verification_get_spec.rb +++ b/spec/lib/pact/consumer/mock_service/verification_get_spec.rb @@ -105,6 +105,7 @@ module RequestHandlers describe "FailureMessage" do let(:missing_interactions_summaries) { ["Blah", "Thing"]} let(:interaction_mismatches_summaries) { []} + let(:interaction_mismatches) { []} let(:unexpected_requests_summaries) { []} let(:verification) { instance_double("Pact::Consumer::Verification") } subject { VerificationGet::FailureMessage.new(verification).to_s } @@ -112,6 +113,7 @@ module RequestHandlers before do allow(verification).to receive(:missing_interactions_summaries).and_return(missing_interactions_summaries) allow(verification).to receive(:interaction_mismatches_summaries).and_return(interaction_mismatches_summaries) + allow(verification).to receive(:interaction_mismatches).and_return(interaction_mismatches) allow(verification).to receive(:unexpected_requests_summaries).and_return(unexpected_requests_summaries) end @@ -122,6 +124,7 @@ module RequestHandlers \tBlah \tThing + EOS } it "only includes missing interactions" do @@ -132,6 +135,7 @@ module RequestHandlers context "with missing, mismatches and unexpected interactions" do let(:interaction_mismatches_summaries) { ["wiffle"]} + let(:interaction_mismatches) { ["diffs"]} let(:unexpected_requests_summaries) { ["moose"]} let(:expected_string) { <<-EOS @@ -145,6 +149,7 @@ module RequestHandlers Unexpected requests: \tmoose +diffs EOS } it "includes all the things" do diff --git a/spec/lib/pact/consumer/server_spec.rb b/spec/lib/pact/consumer/server_spec.rb index 19f6685..d2c3226 100644 --- a/spec/lib/pact/consumer/server_spec.rb +++ b/spec/lib/pact/consumer/server_spec.rb @@ -4,7 +4,7 @@ describe 'booting' do context 'with `nil` port' do let(:app) { -> (env) { [200, {}, ['OK']] } } - let(:server) { described_class.new(app, nil) } + let(:server) { described_class.new(app, 'localhost', nil) } it 'boots server with port 0 trick' do expect(server.port).to be_nil diff --git a/spec/lib/pact/consumer_contract/consumer_contract_decorator_spec.rb b/spec/lib/pact/consumer_contract/consumer_contract_decorator_spec.rb index 09926af..b8567d3 100644 --- a/spec/lib/pact/consumer_contract/consumer_contract_decorator_spec.rb +++ b/spec/lib/pact/consumer_contract/consumer_contract_decorator_spec.rb @@ -34,6 +34,7 @@ module Pact end end + describe "as_json" do context "with multiple interactions" do let(:desc_2) { 'Desc 1' } @@ -127,7 +128,51 @@ module Pact end end end - end + context "when an interaction is marked to not be written" do + before do + Pact.configuration.pactfile_write_order = :chronological + end + + let(:interaction_1) do + InteractionFactory.create( + provider_state: 'State 1', + description: 'Desc 1', + response: { + status: 201 + }) + end + let(:interaction_2) do + InteractionFactory.create( + provider_state: 'State 2', + description: 'Desc 2', + response: { + status: 201 + }, + metadata: { + write_to_pact: true + }) + end + let(:interaction_3) do + InteractionFactory.create( + provider_state: 'State 3', + description: 'Desc 3', + response: { + status: 201 + }, + metadata: { + write_to_pact: false + }) + end + let(:interactions) { [interaction_1, interaction_2, interaction_3] } + + it "only renders writable interactions" do + expect(subject.as_json[:interactions]).to eq([ + InteractionDecorator.new(interaction_1, pact_specification_version: pact_specification_version).as_json, + InteractionDecorator.new(interaction_2, pact_specification_version: pact_specification_version).as_json + ]) + end + end + end end end diff --git a/spec/lib/pact/mock_service/app_manager_spec.rb b/spec/lib/pact/mock_service/app_manager_spec.rb index e8f70f2..5435b34 100644 --- a/spec/lib/pact/mock_service/app_manager_spec.rb +++ b/spec/lib/pact/mock_service/app_manager_spec.rb @@ -50,14 +50,6 @@ module Pact::MockService end end - context "for a host other than localhost" do - let(:url) { 'http://aserver:1234'} - - it "should throw an unsupported error" do - expect { AppManager.instance.register_mock_service_for name, url, options }.to raise_error "Currently only services on localhost are supported" - end - end - describe "find_a_port option" do let(:url) { 'http://localhost' } diff --git a/spec/lib/pact/mock_service/client_spec.rb b/spec/lib/pact/mock_service/client_spec.rb index 744231c..d75508f 100644 --- a/spec/lib/pact/mock_service/client_spec.rb +++ b/spec/lib/pact/mock_service/client_spec.rb @@ -53,7 +53,7 @@ module MockService end it "deletes the interactions" do - Pact::MockService::Client.clear_interactions 4444, "some example" + Pact::MockService::Client.clear_interactions "http://localhost:4444", "some example" expect(delete_verifications).to have_been_made end end diff --git a/spec/lib/pact/mock_service/request_handlers/interaction_replay_spec.rb b/spec/lib/pact/mock_service/request_handlers/interaction_replay_spec.rb index 3bb5677..dbd116e 100644 --- a/spec/lib/pact/mock_service/request_handlers/interaction_replay_spec.rb +++ b/spec/lib/pact/mock_service/request_handlers/interaction_replay_spec.rb @@ -143,6 +143,18 @@ module RequestHandlers end end end + + context "when the body contains special charachters" do + let(:actual_body) { '\xEB' } + + let(:expected_response_body) do + {"message"=>"No interaction found for GET /path", "interaction_diffs"=>[{"body"=>{"ACTUAL"=>"\\xEB", "EXPECTED"=>{"a"=>"body"}}, "description"=>"a request"}]} + end + + it "returns the specified response status" do + expect(response_status).to eq 500 + end + end end context "when no request is found with a matching method and path" do diff --git a/spec/lib/pact/mock_service/session_spec.rb b/spec/lib/pact/mock_service/session_spec.rb index 1a190ae..1139aa9 100644 --- a/spec/lib/pact/mock_service/session_spec.rb +++ b/spec/lib/pact/mock_service/session_spec.rb @@ -7,7 +7,6 @@ module Pact::MockService let(:logger) { double('Logger').as_null_object } describe "set_expected_interactions" do - let(:interaction_1) { InteractionFactory.create } let(:interaction_2) { InteractionFactory.create } let(:interactions) { [interaction_1, interaction_2] } @@ -84,6 +83,7 @@ module Pact::MockService let(:expected_interactions) { instance_double('Interactions::ExpectedInteractions', :<< => nil) } let(:actual_interactions) { instance_double('Interactions::ActualInteractions') } let(:verified_interactions) { instance_double('Interactions::VerifiedInteractions') } + let(:matching_interaction) { nil } before do allow(Interactions::ExpectedInteractions).to receive(:new).and_return(expected_interactions) @@ -95,8 +95,6 @@ module Pact::MockService subject { Session.new(logger: logger) } context "when there is no already verified interaction with the same description and provider state" do - let(:matching_interaction) { nil } - it "adds the new interaction to the interaction list" do expect(expected_interactions).to receive(:<<).with(interaction_1) subject.add_expected_interaction interaction_1 @@ -138,6 +136,31 @@ module Pact::MockService expect { subject.add_expected_interaction interaction_1 }.to raise_error SameSameButDifferentError, diff_message end end + + context "when there are more than 3 interactions mocked at the same time" do + subject { Session.new(logger: logger, warn_on_too_many_interactions: true) } + + it "logs a warning" do + allow(expected_interactions).to receive(:size).and_return(3, 4) + expect(logger).to receive(:warn).with(/You currently have 4 interactions/).once + subject.add_expected_interaction(InteractionFactory.create('description' => 'third interaction')) # no warning + subject.add_expected_interaction(InteractionFactory.create('description' => 'forth interaction')) # warning + end + + context "when PACT_MAX_CONCURRENT_INTERACTIONS_BEFORE_WARNING is set" do + before do + allow(ENV).to receive(:[]).and_call_original + allow(ENV).to receive(:[]).with("PACT_MAX_CONCURRENT_INTERACTIONS_BEFORE_WARNING").and_return("5") + end + + it "logs a warning when over the configured limit" do + allow(expected_interactions).to receive(:size).and_return(5, 6) + expect(logger).to receive(:warn).with(/You currently have 6 interactions/).once + subject.add_expected_interaction(InteractionFactory.create('description' => 'fifth interaction')) # no warning + subject.add_expected_interaction(InteractionFactory.create('description' => 'sixth interaction')) # warning + end + end + end end end end diff --git a/spec/support/integration_spec_support.rb b/spec/support/integration_spec_support.rb index 64ae089..9ddd2f9 100644 --- a/spec/support/integration_spec_support.rb +++ b/spec/support/integration_spec_support.rb @@ -35,7 +35,7 @@ def start_control port, options = '' end def wait_until_server_started port, ssl = false - Pact::MockService::Server::WaitForServerUp.(port, {ssl: ssl}) + Pact::MockService::Server::WaitForServerUp.("localhost", port, {ssl: ssl}) end def kill_server pid diff --git a/tasks/package.rake b/tasks/package.rake index 766c6c0..c3dddf8 100644 --- a/tasks/package.rake +++ b/tasks/package.rake @@ -84,7 +84,7 @@ def create_package(version, target, os_type = :unix) end sh "cp -pR build/vendor #{package_dir}/lib/" - sh "cp pact-mock-service.gemspec Gemfile Gemfile.lock #{package_dir}/lib/vendor/" + sh "cp pact-mock_service.gemspec Gemfile Gemfile.lock #{package_dir}/lib/vendor/" sh "mkdir #{package_dir}/lib/vendor/.bundle" sh "cp packaging/bundler-config #{package_dir}/lib/vendor/.bundle/config" if !ENV['DIR_ONLY']