Skip to content

Commit 56bd683

Browse files
committed
feat(stub): add pact-stub-service CLI
Allow existing pact files to be used to create a stub service
1 parent 80dccec commit 56bd683

File tree

7 files changed

+265
-1
lines changed

7 files changed

+265
-1
lines changed

bin/pact-stub-service

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
#!/usr/bin/env ruby
2+
require 'pact/stub_service/cli'
3+
Pact::StubService::CLI.start

lib/pact/mock_service/app.rb

+10
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ def initialize options = {}
2020
logger = Logger.from_options(options)
2121
@name = options.fetch(:name, "MockService")
2222
@session = Session.new(options.merge(logger: logger))
23+
setup_stub(options[:stub_pactfile_paths]) if options[:stub_pactfile_paths] && options[:stub_pactfile_paths].any?
2324
request_handlers = RequestHandlers.new(@name, logger, @session, options)
2425
@app = Rack::Builder.app do
2526
use Pact::Consumer::MockService::ErrorHandler, logger
@@ -36,6 +37,15 @@ def shutdown
3637
write_pact_if_configured
3738
end
3839

40+
def setup_stub stub_pactfile_paths
41+
stub_pactfile_paths.each do | pactfile_path |
42+
$stdout.puts "Loading interactions from #{pactfile_path}"
43+
hash_interactions = JSON.parse(File.read(pactfile_path))['interactions']
44+
interactions = hash_interactions.collect { | hash | Interaction.from_hash(hash) }
45+
@session.set_expected_interactions interactions
46+
end
47+
end
48+
3949
def write_pact_if_configured
4050
consumer_contract_writer = ConsumerContractWriter.new(@session.consumer_contract_details, StdoutLogger.new)
4151
if consumer_contract_writer.can_write? && !@session.pact_written?
+71
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
require 'thor'
2+
3+
module Pact
4+
module MockService
5+
class CLI < Thor
6+
##
7+
# Custom Thor task allows the following:
8+
#
9+
# `script arg1 arg2` to be interpreted as `script <default_task> arg1 arg2`
10+
# `--option 1 --option 2` to be interpreted as `--option 1 2` (the standard Thor format for multiple value options)
11+
# `script --help` to display the help for the default task instead of the command list
12+
#
13+
class CustomThor < ::Thor
14+
15+
no_commands do
16+
def self.start given_args = ARGV, config = {}
17+
super(massage_args(given_args))
18+
end
19+
20+
def help *args
21+
if args.empty?
22+
super(self.class.default_task)
23+
else
24+
super
25+
end
26+
end
27+
28+
def self.massage_args argv
29+
prepend_default_task_name(turn_muliple_tag_options_into_array(argv))
30+
end
31+
32+
def self.prepend_default_task_name argv
33+
if known_first_arguments.include?(argv[0])
34+
argv
35+
else
36+
[default_command] + argv
37+
end
38+
end
39+
40+
# other task names, help, and the help shortcuts
41+
def self.known_first_arguments
42+
@known_first_arguments ||= tasks.keys + ::Thor::HELP_MAPPINGS + ['help']
43+
end
44+
45+
def self.turn_muliple_tag_options_into_array argv
46+
new_argv = []
47+
opt_name = nil
48+
argv.each_with_index do | arg, i |
49+
if arg.start_with?('-')
50+
opt_name = arg
51+
existing = new_argv.find { | a | a.first == opt_name }
52+
if !existing
53+
new_argv << [arg]
54+
end
55+
else
56+
if opt_name
57+
existing = new_argv.find { | a | a.first == opt_name }
58+
existing << arg
59+
opt_name = nil
60+
else
61+
new_argv << [arg]
62+
end
63+
end
64+
end
65+
new_argv.flatten
66+
end
67+
end
68+
end
69+
end
70+
end
71+
end

lib/pact/mock_service/run.rb

+3-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
require 'find_a_port'
22
require 'pact/mock_service/app'
33
require 'pact/consumer/mock_service/set_location'
4+
require 'pact/mock_service/run'
45

56
module Pact
67
module MockService
@@ -53,7 +54,8 @@ def service_options
5354
provider: options[:provider],
5455
cors_enabled: options[:cors],
5556
pact_specification_version: options[:pact_specification_version],
56-
pactfile_write_mode: options[:pact_file_write_mode]
57+
pactfile_write_mode: options[:pact_file_write_mode],
58+
stub_pactfile_paths: options[:stub_pactfile_paths]
5759
}
5860
service_options[:log_file] = open_log_file if options[:log]
5961
service_options

lib/pact/stub_service/cli.rb

+45
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
require 'pact/mock_service/cli/custom_thor'
2+
require 'webrick/https'
3+
require 'rack/handler/webrick'
4+
require 'fileutils'
5+
require 'pact/mock_service/server/wait_for_server_up'
6+
require 'pact/mock_service/cli/pidfile'
7+
require 'socket'
8+
9+
module Pact
10+
module StubService
11+
class CLI < Pact::MockService::CLI::CustomThor
12+
13+
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."
14+
15+
method_option :port, aliases: "-p", desc: "Port on which to run the service"
16+
method_option :host, aliases: "-h", desc: "Host on which to bind the service", default: 'localhost'
17+
method_option :log, aliases: "-l", desc: "File to which to log output"
18+
method_option :cors, aliases: "-o", desc: "Support browser security in tests by responding to OPTIONS requests and adding CORS headers to mocked responses"
19+
method_option :ssl, desc: "Use a self-signed SSL cert to run the service over HTTPS", type: :boolean, default: false
20+
method_option :sslcert, desc: "Specify the path to the SSL cert to use when running the service over HTTPS"
21+
method_option :sslkey, desc: "Specify the path to the SSL key to use when running the service over HTTPS"
22+
method_option :stub_pactfile_paths, hide: true
23+
24+
def service(*pactfiles)
25+
raise Thor::Error.new("Please provide an existing pact file to load") if pactfiles.empty?
26+
require 'pact/mock_service/run'
27+
options.stub_pactfile_paths = pactfiles
28+
opts = Thor::CoreExt::HashWithIndifferentAccess.new
29+
opts.merge!(options)
30+
opts[:stub_pactfile_paths] = pactfiles
31+
opts[:pactfile_write_mode] = 'none'
32+
MockService::Run.(opts)
33+
end
34+
35+
desc 'version', "Show the pact-stub-service gem version"
36+
37+
def version
38+
require 'pact/mock_service/version.rb'
39+
puts Pact::MockService::VERSION
40+
end
41+
42+
default_task :service
43+
end
44+
end
45+
end

script/stub_example.sh

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
#!/usr/bin/env bash
2+
3+
# BEFORE SUITE start mock service
4+
# invoked by the pact framework
5+
bundle exec bin/pact-stub-service tmp/pacts/foo-bar.json \
6+
--port 1234 \
7+
--log ./tmp/bar_stub_service.log &
8+
pid=$!
9+
10+
# BEFORE SUITE wait for mock service to start up
11+
# invoked by the pact framework
12+
while [ "200" -ne "$(curl -H "X-Pact-Mock-Service: true" -s -o /dev/null -w "%{http_code}" localhost:1234)" ]; do sleep 0.5; done
13+
14+
# IN A TEST execute interaction(s)
15+
# this would be done by the consumer code under test
16+
curl localhost:1234/foo
17+
echo ''
18+
19+
20+
# AFTER SUITE stop mock service
21+
# this would be invoked by the test framework
22+
kill -2 $pid
23+
24+
while [ kill -0 $pid 2> /dev/null ]; do sleep 0.5; done
25+
26+
echo ''
27+
echo 'FYI the stub service logs are:'
28+
cat ./tmp/bar_stub_service.log
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
require 'pact/mock_service/cli/custom_thor'
2+
3+
class Pact::MockService::CLI
4+
5+
class Delegate
6+
def self.call options; end
7+
end
8+
9+
class TestThor < CustomThor
10+
desc 'ARGUMENT', 'This is the description'
11+
def test_default(argument)
12+
Delegate.call(argument: argument)
13+
end
14+
15+
desc '', ''
16+
method_option :multi, type: :array
17+
def test_multiple_options
18+
Delegate.call(options)
19+
end
20+
21+
default_command :test_default
22+
end
23+
24+
describe CustomThor do
25+
subject { TestThor.new }
26+
27+
it "invokes the default task when aguments are given without specifying a task" do
28+
expect(Delegate).to receive(:call).with(argument: 'foo')
29+
TestThor.start(%w{foo})
30+
end
31+
32+
it "converts options that are specified multiple times into a single array" do
33+
expect(Delegate).to receive(:call).with({'multi' => ['one', 'two']})
34+
TestThor.start(%w{test_multiple_options --multi one --multi two})
35+
end
36+
37+
describe ".prepend_default_task_name" do
38+
let(:argv_with) { [TestThor.default_command, 'foo'] }
39+
40+
context "when the default task name is given" do
41+
it "does not prepend the default task name" do
42+
expect(TestThor.prepend_default_task_name(argv_with)).to eq(argv_with)
43+
end
44+
end
45+
46+
context "when the first argument is --help" do
47+
let(:argv) { ['--help', 'foo'] }
48+
49+
it "does not prepend the default task name" do
50+
expect(TestThor.prepend_default_task_name(argv)).to eq(argv)
51+
end
52+
end
53+
54+
context "when the default task name is not given" do
55+
let(:argv) { ['foo'] }
56+
57+
it "prepends the default task name" do
58+
expect(TestThor.prepend_default_task_name(argv)).to eq(argv_with)
59+
end
60+
end
61+
end
62+
63+
describe ".turn_muliple_tag_options_into_array" do
64+
it "turns '--tag foo --tag bar' into '--tag foo bar'" do
65+
input = %w{--ignore this --tag foo --tag bar --wiffle --that}
66+
output = %w{--ignore this --tag foo bar --wiffle --that }
67+
expect(TestThor.turn_muliple_tag_options_into_array(input)).to eq output
68+
end
69+
70+
it "turns '--tag foo bar --tag meep' into '--tag foo meep bar'" do
71+
input = %w{--ignore this --tag foo bar --tag meep --wiffle --that}
72+
output = %w{--ignore this --tag foo meep bar --wiffle --that}
73+
expect(TestThor.turn_muliple_tag_options_into_array(input)).to eq output
74+
end
75+
76+
it "turns '--tag foo --tag bar wiffle' into '--tag foo bar wiffle' which is silly" do
77+
input = %w{--ignore this --tag foo --tag bar wiffle}
78+
output = %w{--ignore this --tag foo bar wiffle}
79+
expect(TestThor.turn_muliple_tag_options_into_array(input)).to eq output
80+
end
81+
82+
it "maintains '--tag foo bar wiffle'" do
83+
input = %w{--ignore this --tag foo bar wiffle --meep}
84+
output = %w{--ignore this --tag foo bar wiffle --meep}
85+
expect(TestThor.turn_muliple_tag_options_into_array(input)).to eq output
86+
end
87+
88+
it "turns '-t foo -t bar' into '-t foo bar'" do
89+
input = %w{--ignore this -t foo -t bar --meep --that 1 2 3}
90+
output = %w{--ignore this -t foo bar --meep --that 1 2 3}
91+
expect(TestThor.turn_muliple_tag_options_into_array(input)).to eq output
92+
end
93+
94+
it "doesn't change anything when there are no duplicate options" do
95+
input = %w{--ignore this --taggy foo --blah bar --wiffle --that}
96+
expect(TestThor.turn_muliple_tag_options_into_array(input)).to eq input
97+
end
98+
99+
it "return an empty array when given an empty array" do
100+
input = []
101+
expect(TestThor.turn_muliple_tag_options_into_array(input)).to eq input
102+
end
103+
end
104+
end
105+
end

0 commit comments

Comments
 (0)