Skip to content

Commit

Permalink
feat(stub): add pact-stub-service CLI
Browse files Browse the repository at this point in the history
Allow existing pact files to be used to create a stub service
  • Loading branch information
bethesque committed Oct 12, 2017
1 parent 80dccec commit 56bd683
Show file tree
Hide file tree
Showing 7 changed files with 265 additions and 1 deletion.
3 changes: 3 additions & 0 deletions bin/pact-stub-service
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
#!/usr/bin/env ruby
require 'pact/stub_service/cli'
Pact::StubService::CLI.start
10 changes: 10 additions & 0 deletions lib/pact/mock_service/app.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ def initialize options = {}
logger = Logger.from_options(options)
@name = options.fetch(:name, "MockService")
@session = Session.new(options.merge(logger: logger))
setup_stub(options[:stub_pactfile_paths]) if options[:stub_pactfile_paths] && options[:stub_pactfile_paths].any?
request_handlers = RequestHandlers.new(@name, logger, @session, options)
@app = Rack::Builder.app do
use Pact::Consumer::MockService::ErrorHandler, logger
Expand All @@ -36,6 +37,15 @@ def shutdown
write_pact_if_configured
end

def setup_stub stub_pactfile_paths
stub_pactfile_paths.each do | pactfile_path |
$stdout.puts "Loading interactions from #{pactfile_path}"
hash_interactions = JSON.parse(File.read(pactfile_path))['interactions']
interactions = hash_interactions.collect { | hash | Interaction.from_hash(hash) }
@session.set_expected_interactions interactions
end
end

def write_pact_if_configured
consumer_contract_writer = ConsumerContractWriter.new(@session.consumer_contract_details, StdoutLogger.new)
if consumer_contract_writer.can_write? && !@session.pact_written?
Expand Down
71 changes: 71 additions & 0 deletions lib/pact/mock_service/cli/custom_thor.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
require 'thor'

module Pact
module MockService
class CLI < Thor
##
# Custom Thor task allows the following:
#
# `script arg1 arg2` to be interpreted as `script <default_task> arg1 arg2`
# `--option 1 --option 2` to be interpreted as `--option 1 2` (the standard Thor format for multiple value options)
# `script --help` to display the help for the default task instead of the command list
#
class CustomThor < ::Thor

no_commands do
def self.start given_args = ARGV, config = {}
super(massage_args(given_args))
end

def help *args
if args.empty?
super(self.class.default_task)
else
super
end
end

def self.massage_args argv
prepend_default_task_name(turn_muliple_tag_options_into_array(argv))
end

def self.prepend_default_task_name argv
if known_first_arguments.include?(argv[0])
argv
else
[default_command] + argv
end
end

# other task names, help, and the help shortcuts
def self.known_first_arguments
@known_first_arguments ||= tasks.keys + ::Thor::HELP_MAPPINGS + ['help']
end

def self.turn_muliple_tag_options_into_array argv
new_argv = []
opt_name = nil
argv.each_with_index do | arg, i |
if arg.start_with?('-')
opt_name = arg
existing = new_argv.find { | a | a.first == opt_name }
if !existing
new_argv << [arg]
end
else
if opt_name
existing = new_argv.find { | a | a.first == opt_name }
existing << arg
opt_name = nil
else
new_argv << [arg]
end
end
end
new_argv.flatten
end
end
end
end
end
end
4 changes: 3 additions & 1 deletion lib/pact/mock_service/run.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
require 'find_a_port'
require 'pact/mock_service/app'
require 'pact/consumer/mock_service/set_location'
require 'pact/mock_service/run'

module Pact
module MockService
Expand Down Expand Up @@ -53,7 +54,8 @@ def service_options
provider: options[:provider],
cors_enabled: options[:cors],
pact_specification_version: options[:pact_specification_version],
pactfile_write_mode: options[:pact_file_write_mode]
pactfile_write_mode: options[:pact_file_write_mode],
stub_pactfile_paths: options[:stub_pactfile_paths]
}
service_options[:log_file] = open_log_file if options[:log]
service_options
Expand Down
45 changes: 45 additions & 0 deletions lib/pact/stub_service/cli.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
require 'pact/mock_service/cli/custom_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 StubService
class CLI < Pact::MockService::CLI::CustomThor

desc 'PACT ...', "Start a stub service with the given pact file(s). Note that this is in beta release, and no logic has been added to handle the situation where more than one matching interaction is found for a request. At the moment, an error response will be returned."

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 :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"
method_option :sslkey, desc: "Specify the path to the SSL key to use when running the service over HTTPS"
method_option :stub_pactfile_paths, hide: true

def service(*pactfiles)
raise Thor::Error.new("Please provide an existing pact file to load") if pactfiles.empty?
require 'pact/mock_service/run'
options.stub_pactfile_paths = pactfiles
opts = Thor::CoreExt::HashWithIndifferentAccess.new
opts.merge!(options)
opts[:stub_pactfile_paths] = pactfiles
opts[:pactfile_write_mode] = 'none'
MockService::Run.(opts)
end

desc 'version', "Show the pact-stub-service gem version"

def version
require 'pact/mock_service/version.rb'
puts Pact::MockService::VERSION
end

default_task :service
end
end
end
28 changes: 28 additions & 0 deletions script/stub_example.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
#!/usr/bin/env bash

# BEFORE SUITE start mock service
# invoked by the pact framework
bundle exec bin/pact-stub-service tmp/pacts/foo-bar.json \
--port 1234 \
--log ./tmp/bar_stub_service.log &
pid=$!

# BEFORE SUITE wait for mock service to start up
# invoked by the pact framework
while [ "200" -ne "$(curl -H "X-Pact-Mock-Service: true" -s -o /dev/null -w "%{http_code}" localhost:1234)" ]; do sleep 0.5; done

# IN A TEST execute interaction(s)
# this would be done by the consumer code under test
curl localhost:1234/foo
echo ''


# AFTER SUITE stop mock service
# this would be invoked by the test framework
kill -2 $pid

while [ kill -0 $pid 2> /dev/null ]; do sleep 0.5; done

echo ''
echo 'FYI the stub service logs are:'
cat ./tmp/bar_stub_service.log
105 changes: 105 additions & 0 deletions spec/lib/pact/mock_service/cli/custom_thor_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
require 'pact/mock_service/cli/custom_thor'

class Pact::MockService::CLI

class Delegate
def self.call options; end
end

class TestThor < CustomThor
desc 'ARGUMENT', 'This is the description'
def test_default(argument)
Delegate.call(argument: argument)
end

desc '', ''
method_option :multi, type: :array
def test_multiple_options
Delegate.call(options)
end

default_command :test_default
end

describe CustomThor do
subject { TestThor.new }

it "invokes the default task when aguments are given without specifying a task" do
expect(Delegate).to receive(:call).with(argument: 'foo')
TestThor.start(%w{foo})
end

it "converts options that are specified multiple times into a single array" do
expect(Delegate).to receive(:call).with({'multi' => ['one', 'two']})
TestThor.start(%w{test_multiple_options --multi one --multi two})
end

describe ".prepend_default_task_name" do
let(:argv_with) { [TestThor.default_command, 'foo'] }

context "when the default task name is given" do
it "does not prepend the default task name" do
expect(TestThor.prepend_default_task_name(argv_with)).to eq(argv_with)
end
end

context "when the first argument is --help" do
let(:argv) { ['--help', 'foo'] }

it "does not prepend the default task name" do
expect(TestThor.prepend_default_task_name(argv)).to eq(argv)
end
end

context "when the default task name is not given" do
let(:argv) { ['foo'] }

it "prepends the default task name" do
expect(TestThor.prepend_default_task_name(argv)).to eq(argv_with)
end
end
end

describe ".turn_muliple_tag_options_into_array" do
it "turns '--tag foo --tag bar' into '--tag foo bar'" do
input = %w{--ignore this --tag foo --tag bar --wiffle --that}
output = %w{--ignore this --tag foo bar --wiffle --that }
expect(TestThor.turn_muliple_tag_options_into_array(input)).to eq output
end

it "turns '--tag foo bar --tag meep' into '--tag foo meep bar'" do
input = %w{--ignore this --tag foo bar --tag meep --wiffle --that}
output = %w{--ignore this --tag foo meep bar --wiffle --that}
expect(TestThor.turn_muliple_tag_options_into_array(input)).to eq output
end

it "turns '--tag foo --tag bar wiffle' into '--tag foo bar wiffle' which is silly" do
input = %w{--ignore this --tag foo --tag bar wiffle}
output = %w{--ignore this --tag foo bar wiffle}
expect(TestThor.turn_muliple_tag_options_into_array(input)).to eq output
end

it "maintains '--tag foo bar wiffle'" do
input = %w{--ignore this --tag foo bar wiffle --meep}
output = %w{--ignore this --tag foo bar wiffle --meep}
expect(TestThor.turn_muliple_tag_options_into_array(input)).to eq output
end

it "turns '-t foo -t bar' into '-t foo bar'" do
input = %w{--ignore this -t foo -t bar --meep --that 1 2 3}
output = %w{--ignore this -t foo bar --meep --that 1 2 3}
expect(TestThor.turn_muliple_tag_options_into_array(input)).to eq output
end

it "doesn't change anything when there are no duplicate options" do
input = %w{--ignore this --taggy foo --blah bar --wiffle --that}
expect(TestThor.turn_muliple_tag_options_into_array(input)).to eq input
end

it "return an empty array when given an empty array" do
input = []
expect(TestThor.turn_muliple_tag_options_into_array(input)).to eq input
end
end
end
end

0 comments on commit 56bd683

Please sign in to comment.