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']