hs-pact-mock_service 3.9.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/CHANGELOG.md +494 -0
- data/Gemfile +8 -0
- data/LICENSE.txt +22 -0
- data/README.md +137 -0
- data/bin/pact-mock-service +3 -0
- data/bin/pact-stub-service +3 -0
- data/lib/pact/consumer/mock_service/cors_origin_header_middleware.rb +31 -0
- data/lib/pact/consumer/mock_service/error_handler.rb +31 -0
- data/lib/pact/consumer/mock_service/rack_request_helper.rb +65 -0
- data/lib/pact/consumer/mock_service/set_location.rb +28 -0
- data/lib/pact/consumer/server.rb +111 -0
- data/lib/pact/consumer_contract/consumer_contract_decorator.rb +53 -0
- data/lib/pact/consumer_contract/consumer_contract_writer.rb +181 -0
- data/lib/pact/consumer_contract/interaction_decorator.rb +40 -0
- data/lib/pact/consumer_contract/request_decorator.rb +88 -0
- data/lib/pact/consumer_contract/response_decorator.rb +47 -0
- data/lib/pact/mock_service/app.rb +85 -0
- data/lib/pact/mock_service/app_manager.rb +156 -0
- data/lib/pact/mock_service/cli/custom_thor.rb +74 -0
- data/lib/pact/mock_service/cli/pidfile.rb +99 -0
- data/lib/pact/mock_service/cli.rb +213 -0
- data/lib/pact/mock_service/client.rb +79 -0
- data/lib/pact/mock_service/control_server/app.rb +42 -0
- data/lib/pact/mock_service/control_server/delegator.rb +44 -0
- data/lib/pact/mock_service/control_server/index.rb +25 -0
- data/lib/pact/mock_service/control_server/mock_service_creator.rb +32 -0
- data/lib/pact/mock_service/control_server/mock_services.rb +26 -0
- data/lib/pact/mock_service/control_server/require_pacticipant_headers.rb +20 -0
- data/lib/pact/mock_service/control_server/run.rb +73 -0
- data/lib/pact/mock_service/errors.rb +9 -0
- data/lib/pact/mock_service/interaction_decorator.rb +49 -0
- data/lib/pact/mock_service/interactions/actual_interactions.rb +36 -0
- data/lib/pact/mock_service/interactions/candidate_interactions.rb +15 -0
- data/lib/pact/mock_service/interactions/expected_interactions.rb +18 -0
- data/lib/pact/mock_service/interactions/interaction_diff_message.rb +45 -0
- data/lib/pact/mock_service/interactions/interaction_mismatch.rb +74 -0
- data/lib/pact/mock_service/interactions/interactions_filter.rb +66 -0
- data/lib/pact/mock_service/interactions/verification.rb +52 -0
- data/lib/pact/mock_service/interactions/verified_interactions.rb +20 -0
- data/lib/pact/mock_service/logger.rb +40 -0
- data/lib/pact/mock_service/request_decorator.rb +36 -0
- data/lib/pact/mock_service/request_handlers/base_administration_request_handler.rb +42 -0
- data/lib/pact/mock_service/request_handlers/base_request_handler.rb +30 -0
- data/lib/pact/mock_service/request_handlers/index_get.rb +23 -0
- data/lib/pact/mock_service/request_handlers/interaction_delete.rb +38 -0
- data/lib/pact/mock_service/request_handlers/interaction_post.rb +43 -0
- data/lib/pact/mock_service/request_handlers/interaction_replay.rb +191 -0
- data/lib/pact/mock_service/request_handlers/interactions_put.rb +44 -0
- data/lib/pact/mock_service/request_handlers/log_get.rb +27 -0
- data/lib/pact/mock_service/request_handlers/missing_interactions_get.rb +33 -0
- data/lib/pact/mock_service/request_handlers/options.rb +64 -0
- data/lib/pact/mock_service/request_handlers/pact_post.rb +38 -0
- data/lib/pact/mock_service/request_handlers/session_delete.rb +32 -0
- data/lib/pact/mock_service/request_handlers/verification_get.rb +74 -0
- data/lib/pact/mock_service/request_handlers.rb +42 -0
- data/lib/pact/mock_service/response_decorator.rb +31 -0
- data/lib/pact/mock_service/run.rb +125 -0
- data/lib/pact/mock_service/server/respawn.rb +20 -0
- data/lib/pact/mock_service/server/spawn.rb +37 -0
- data/lib/pact/mock_service/server/wait_for_server_up.rb +44 -0
- data/lib/pact/mock_service/server/webrick_request_monkeypatch.rb +15 -0
- data/lib/pact/mock_service/session.rb +96 -0
- data/lib/pact/mock_service/spawn.rb +86 -0
- data/lib/pact/mock_service/version.rb +5 -0
- data/lib/pact/mock_service.rb +2 -0
- data/lib/pact/stub_service/cli.rb +71 -0
- data/lib/pact/support/expand_file_list.rb +26 -0
- metadata +399 -0
@@ -0,0 +1,31 @@
|
|
1
|
+
module Pact
|
2
|
+
module Consumer
|
3
|
+
class CorsOriginHeaderMiddleware
|
4
|
+
|
5
|
+
def initialize app, cors_enabled
|
6
|
+
@app = app
|
7
|
+
@cors_enabled = cors_enabled
|
8
|
+
end
|
9
|
+
|
10
|
+
def call env
|
11
|
+
response = @app.call env
|
12
|
+
if env['HTTP_X_PACT_MOCK_SERVICE'] || @cors_enabled
|
13
|
+
add_cors_header env, response
|
14
|
+
else
|
15
|
+
response
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def shutdown
|
20
|
+
@app.shutdown
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def add_cors_header env, response
|
26
|
+
cors_headers = { 'Access-Control-Allow-Origin' => env.fetch('HTTP_ORIGIN','*'), 'Access-Control-Allow-Credentials' => 'true'}
|
27
|
+
[response[0], response[1].merge(cors_headers), response[2]]
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module Pact
|
2
|
+
module Consumer
|
3
|
+
class MockService
|
4
|
+
class ErrorHandler
|
5
|
+
|
6
|
+
def initialize app, logger
|
7
|
+
@app = app
|
8
|
+
@logger = logger
|
9
|
+
end
|
10
|
+
|
11
|
+
def call env
|
12
|
+
begin
|
13
|
+
@app.call(env)
|
14
|
+
rescue Pact::Error => e
|
15
|
+
@logger.error e.message
|
16
|
+
[500, {'Content-Type' => 'application/json'}, [{message: e.message}.to_json]]
|
17
|
+
rescue StandardError => e
|
18
|
+
message = "Error ocurred in mock service: #{e.class} - #{e.message}"
|
19
|
+
@logger.error message
|
20
|
+
@logger.error e.backtrace.join("\n")
|
21
|
+
[500, {'Content-Type' => 'application/json'}, [{message: message, backtrace: e.backtrace}.to_json]]
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def shutdown
|
26
|
+
@app.shutdown
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
require 'cgi/core'
|
2
|
+
require 'pact/consumer_contract/query'
|
3
|
+
|
4
|
+
module Pact
|
5
|
+
module Consumer
|
6
|
+
|
7
|
+
module RackRequestHelper
|
8
|
+
REQUEST_KEYS = {
|
9
|
+
'REQUEST_METHOD' => :method,
|
10
|
+
'PATH_INFO' => :path,
|
11
|
+
'QUERY_STRING' => :query,
|
12
|
+
'rack.input' => :body
|
13
|
+
}
|
14
|
+
|
15
|
+
def params_hash env
|
16
|
+
Pact::Query.parse_string(env["QUERY_STRING"])
|
17
|
+
end
|
18
|
+
|
19
|
+
def request_as_hash_from env
|
20
|
+
request = env.inject({}) do |memo, (k, v)|
|
21
|
+
request_key = REQUEST_KEYS[k]
|
22
|
+
memo[request_key] = v if request_key
|
23
|
+
memo
|
24
|
+
end
|
25
|
+
|
26
|
+
request[:headers] = headers_from env
|
27
|
+
body_string = request[:body].read
|
28
|
+
|
29
|
+
if body_string.empty?
|
30
|
+
request.delete :body
|
31
|
+
else
|
32
|
+
body_is_json = request[:headers]['Content-Type'] =~ /json/
|
33
|
+
request[:body] = body_is_json ? JSON.parse(body_string) : body_string
|
34
|
+
end
|
35
|
+
request[:method] = request[:method].downcase
|
36
|
+
request
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def headers_from env
|
42
|
+
headers = env.reject{ |key, value| !(key.start_with?("HTTP") || key == 'CONTENT_TYPE' || key == 'CONTENT_LENGTH')}
|
43
|
+
dasherized_headers = headers.inject({}) do | hash, header |
|
44
|
+
hash[standardise_header(header.first)] = header.last
|
45
|
+
hash
|
46
|
+
end
|
47
|
+
# This header is set in lib/pact/mock_service/server/webrick_request_monkeypatch.rb to allow use to
|
48
|
+
# restore the original header names with underscores.
|
49
|
+
restore_underscored_header_names(dasherized_headers, (env['X_PACT_UNDERSCORED_HEADER_NAMES'] || '').split(","))
|
50
|
+
end
|
51
|
+
|
52
|
+
def standardise_header header
|
53
|
+
header.gsub(/^HTTP_/, '').split(/[_-]/).collect{|word| word[0].upcase + word[1..-1].downcase}.join("-")
|
54
|
+
end
|
55
|
+
|
56
|
+
def restore_underscored_header_names dasherized_headers, original_header_names
|
57
|
+
original_header_names.each_with_object(dasherized_headers) do | original_header_name, headers |
|
58
|
+
if headers.key?(standardise_header(original_header_name))
|
59
|
+
headers[original_header_name] = headers.delete(standardise_header(original_header_name))
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module Pact
|
2
|
+
module Consumer
|
3
|
+
class SetLocation
|
4
|
+
|
5
|
+
LOCATION = 'X-Pact-Mock-Service-Location'.freeze
|
6
|
+
HTTP_X_PACT_MOCK_SERVICE = 'HTTP_X_PACT_MOCK_SERVICE'
|
7
|
+
|
8
|
+
def initialize app, base_url
|
9
|
+
@app = app
|
10
|
+
@location_header = {LOCATION => base_url}.freeze
|
11
|
+
end
|
12
|
+
|
13
|
+
def call env
|
14
|
+
response = @app.call(env)
|
15
|
+
env[HTTP_X_PACT_MOCK_SERVICE] ? add_location_header_to_response(response) : response
|
16
|
+
end
|
17
|
+
|
18
|
+
def add_location_header_to_response response
|
19
|
+
[response.first, response[1].merge(@location_header), response.last]
|
20
|
+
end
|
21
|
+
|
22
|
+
def shutdown
|
23
|
+
@app.shutdown
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,111 @@
|
|
1
|
+
require 'uri'
|
2
|
+
require 'net/http'
|
3
|
+
require 'rack'
|
4
|
+
|
5
|
+
# Copied shamelessly from Capybara
|
6
|
+
# Used to run a mock service in a new thread when started by the AppManager or the ControlServer
|
7
|
+
|
8
|
+
module Pact
|
9
|
+
class Server
|
10
|
+
class Middleware
|
11
|
+
attr_accessor :error
|
12
|
+
|
13
|
+
def initialize(app)
|
14
|
+
@app = app
|
15
|
+
end
|
16
|
+
|
17
|
+
def call(env)
|
18
|
+
if env["PATH_INFO"] == "/__identify__"
|
19
|
+
[200, {}, [@app.object_id.to_s]]
|
20
|
+
else
|
21
|
+
begin
|
22
|
+
@app.call(env)
|
23
|
+
rescue StandardError => e
|
24
|
+
@error = e unless @error
|
25
|
+
raise e
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
class << self
|
32
|
+
def ports
|
33
|
+
@ports ||= {}
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
attr_reader :app, :host, :port, :options
|
38
|
+
|
39
|
+
def initialize(app, host, port, options = {})
|
40
|
+
@app = app
|
41
|
+
@middleware = Middleware.new(@app)
|
42
|
+
@server_thread = nil
|
43
|
+
@host = host
|
44
|
+
@port = port
|
45
|
+
@options = options
|
46
|
+
end
|
47
|
+
|
48
|
+
def reset_error!
|
49
|
+
@middleware.error = nil
|
50
|
+
end
|
51
|
+
|
52
|
+
def error
|
53
|
+
@middleware.error
|
54
|
+
end
|
55
|
+
|
56
|
+
def responsive?
|
57
|
+
return false if @server_thread && @server_thread.join(0)
|
58
|
+
res = get_identity
|
59
|
+
if res.is_a?(Net::HTTPSuccess) or res.is_a?(Net::HTTPRedirection)
|
60
|
+
return res.body == @app.object_id.to_s
|
61
|
+
end
|
62
|
+
rescue SystemCallError
|
63
|
+
return false
|
64
|
+
rescue EOFError
|
65
|
+
return false
|
66
|
+
end
|
67
|
+
|
68
|
+
def run_default_server(app, port)
|
69
|
+
require 'rack/handler/webrick'
|
70
|
+
Rack::Handler::WEBrick.run(app, **webrick_opts) do |server|
|
71
|
+
@port = server[:Port]
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def get_identity
|
76
|
+
return false unless @port
|
77
|
+
http = Net::HTTP.new host, @port
|
78
|
+
if options[:ssl]
|
79
|
+
http.use_ssl = true
|
80
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
81
|
+
end
|
82
|
+
http.get('/__identify__')
|
83
|
+
end
|
84
|
+
|
85
|
+
def webrick_opts
|
86
|
+
opts = { Host: host.nil? ? 'localhost' : host, Port: port.nil? ? 0 : port, AccessLog: [], Logger: WEBrick::Log::new(nil, 0) }
|
87
|
+
opts.merge!({
|
88
|
+
:SSLCertificate => OpenSSL::X509::Certificate.new(File.open(options[:sslcert]).read) }) if options[:sslcert]
|
89
|
+
opts.merge!({
|
90
|
+
:SSLPrivateKey => OpenSSL::PKey::RSA.new(File.open(options[:sslkey]).read) }) if options[:sslkey]
|
91
|
+
opts.merge!(ssl_opts) if options[:ssl]
|
92
|
+
opts
|
93
|
+
end
|
94
|
+
|
95
|
+
def ssl_opts
|
96
|
+
{ SSLEnable: true, SSLCertName: [ ["CN", host] ] }
|
97
|
+
end
|
98
|
+
|
99
|
+
def boot
|
100
|
+
unless responsive?
|
101
|
+
@server_thread = Thread.new { run_default_server(@middleware, @port) }
|
102
|
+
Timeout.timeout(60) { @server_thread.join(0.1) until responsive? }
|
103
|
+
end
|
104
|
+
rescue Timeout::Error
|
105
|
+
raise "Rack application timed out during boot"
|
106
|
+
else
|
107
|
+
Pact::Server.ports[@app.object_id] = @port
|
108
|
+
self
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
require 'pact/shared/active_support_support'
|
2
|
+
require 'pact/consumer_contract/interaction_decorator'
|
3
|
+
|
4
|
+
module Pact
|
5
|
+
class ConsumerContractDecorator
|
6
|
+
|
7
|
+
include ActiveSupportSupport
|
8
|
+
|
9
|
+
def initialize consumer_contract, decorator_options = {}
|
10
|
+
@consumer_contract = consumer_contract
|
11
|
+
@decorator_options = decorator_options
|
12
|
+
end
|
13
|
+
|
14
|
+
def as_json(options = {})
|
15
|
+
fix_all_the_things(
|
16
|
+
consumer: consumer_contract.consumer.as_json,
|
17
|
+
provider: consumer_contract.provider.as_json,
|
18
|
+
interactions: sorted_interactions.collect{ |i| InteractionDecorator.new(i, @decorator_options).as_json},
|
19
|
+
metadata: {
|
20
|
+
pactSpecification: {
|
21
|
+
version: pact_specification_version
|
22
|
+
}
|
23
|
+
}
|
24
|
+
)
|
25
|
+
end
|
26
|
+
|
27
|
+
def to_json(options = {})
|
28
|
+
as_json.to_json(options)
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def sorted_interactions
|
34
|
+
# Default order: chronological
|
35
|
+
return consumer_contract.writable_interactions if Pact.configuration.pactfile_write_order == :chronological
|
36
|
+
# We are supporting only chronological or alphabetical order
|
37
|
+
raise NotImplementedError if Pact.configuration.pactfile_write_order != :alphabetical
|
38
|
+
|
39
|
+
consumer_contract.writable_interactions.sort{|a, b| sortable_id(a) <=> sortable_id(b)}
|
40
|
+
end
|
41
|
+
|
42
|
+
def sortable_id interaction
|
43
|
+
"#{interaction.description.downcase} #{interaction.response.status} #{(interaction.provider_state || '').downcase}"
|
44
|
+
end
|
45
|
+
|
46
|
+
attr_reader :consumer_contract
|
47
|
+
|
48
|
+
def pact_specification_version
|
49
|
+
version = @decorator_options.fetch(:pact_specification_version)
|
50
|
+
"#{version[0]}.0.0" # Only care about the first digit
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,181 @@
|
|
1
|
+
require 'pact/consumer_contract'
|
2
|
+
require 'pact/mock_service/interactions/interactions_filter'
|
3
|
+
require 'pact/consumer_contract/file_name'
|
4
|
+
require 'pact/consumer_contract/pact_file'
|
5
|
+
require 'pact/consumer_contract/consumer_contract_decorator'
|
6
|
+
require 'pact/shared/active_support_support'
|
7
|
+
require 'fileutils'
|
8
|
+
require 'filelock'
|
9
|
+
|
10
|
+
module Pact
|
11
|
+
|
12
|
+
class ConsumerContractWriterError < StandardError; end
|
13
|
+
|
14
|
+
class ConsumerContractWriter
|
15
|
+
|
16
|
+
DEFAULT_PACT_SPECIFICATION_VERSION = '2.0.0'
|
17
|
+
|
18
|
+
include Pact::FileName
|
19
|
+
include Pact::PactFile
|
20
|
+
include ActiveSupportSupport
|
21
|
+
|
22
|
+
def initialize consumer_contract_details, logger
|
23
|
+
@logger = logger
|
24
|
+
@consumer_contract_details = consumer_contract_details
|
25
|
+
@pactfile_write_mode = (consumer_contract_details[:pactfile_write_mode] || :overwrite).to_sym
|
26
|
+
@interactions = consumer_contract_details.fetch(:interactions)
|
27
|
+
@pact_specification_version = (consumer_contract_details[:pact_specification_version] || DEFAULT_PACT_SPECIFICATION_VERSION).to_s
|
28
|
+
@consumer_contract_decorator_class = consumer_contract_details[:consumer_contract_decorator_class] || Pact::ConsumerContractDecorator
|
29
|
+
@error_stream = consumer_contract_details[:error_stream] || Pact.configuration.error_stream
|
30
|
+
@output_stream = consumer_contract_details[:output_stream] || Pact.configuration.output_stream
|
31
|
+
end
|
32
|
+
|
33
|
+
def consumer_contract
|
34
|
+
@consumer_contract ||= Pact::ConsumerContract.new(
|
35
|
+
consumer: ServiceConsumer.new(name: consumer_contract_details[:consumer][:name]),
|
36
|
+
provider: ServiceProvider.new(name: consumer_contract_details[:provider][:name]),
|
37
|
+
interactions: interactions_for_new_consumer_contract)
|
38
|
+
end
|
39
|
+
|
40
|
+
def write
|
41
|
+
update_pactfile_if_needed
|
42
|
+
pact_json
|
43
|
+
end
|
44
|
+
|
45
|
+
def can_write?
|
46
|
+
consumer_name && provider_name && consumer_contract_details[:pact_dir]
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
|
51
|
+
attr_reader :consumer_contract_details, :pactfile_write_mode, :interactions, :logger, :pact_specification_version, :consumer_contract_decorator_class
|
52
|
+
attr_reader :error_stream, :output_stream
|
53
|
+
|
54
|
+
def update_pactfile_if_needed
|
55
|
+
return if pactfile_write_mode == :none
|
56
|
+
return if interactions.count == 0
|
57
|
+
update_pactfile
|
58
|
+
end
|
59
|
+
|
60
|
+
def update_pactfile
|
61
|
+
logger.info log_message
|
62
|
+
FileUtils.mkdir_p File.dirname(pactfile_path)
|
63
|
+
Filelock(pactfile_path) do | pact_file |
|
64
|
+
# must be read after obtaining the lock, and must be read from the yielded file object, otherwise windows freaks out
|
65
|
+
@existing_contents = pact_file.read
|
66
|
+
new_contents = pact_json
|
67
|
+
pact_file.rewind
|
68
|
+
pact_file.truncate 0
|
69
|
+
pact_file.write new_contents
|
70
|
+
pact_file.flush
|
71
|
+
pact_file.truncate(pact_file.pos)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def pact_json
|
76
|
+
@pact_json ||= fix_json_formatting(JSON.pretty_generate(decorated_pact))
|
77
|
+
end
|
78
|
+
|
79
|
+
def decorated_pact
|
80
|
+
@decorated_pact ||= consumer_contract_decorator_class.new(consumer_contract, pact_specification_version: pact_specification_version)
|
81
|
+
end
|
82
|
+
|
83
|
+
def interactions_for_new_consumer_contract
|
84
|
+
if pactfile_exists? && (updating? || merging?)
|
85
|
+
merged_interactions = existing_interactions.dup
|
86
|
+
filter = Pact::MockService::Interactions.filter(merged_interactions, pactfile_write_mode)
|
87
|
+
interactions.each {|i| filter << i }
|
88
|
+
merged_interactions
|
89
|
+
else
|
90
|
+
interactions
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
def existing_interactions
|
95
|
+
interactions = []
|
96
|
+
if pactfile_exists? && pactfile_has_contents?
|
97
|
+
begin
|
98
|
+
interactions = existing_consumer_contract.interactions
|
99
|
+
print_updating_warning if updating?
|
100
|
+
rescue StandardError => e
|
101
|
+
warn_and_stderr "Could not load existing consumer contract from #{pactfile_path} due to #{e}. Creating a new file."
|
102
|
+
logger.error e
|
103
|
+
logger.error e.backtrace
|
104
|
+
end
|
105
|
+
end
|
106
|
+
interactions
|
107
|
+
end
|
108
|
+
|
109
|
+
def pactfile_exists?
|
110
|
+
File.exist?(pactfile_path)
|
111
|
+
end
|
112
|
+
|
113
|
+
def pactfile_has_contents?
|
114
|
+
File.size(pactfile_path) != 0
|
115
|
+
end
|
116
|
+
|
117
|
+
def existing_contents
|
118
|
+
@existing_contents
|
119
|
+
end
|
120
|
+
|
121
|
+
def existing_consumer_contract
|
122
|
+
@existing_consumer_contract ||= Pact::ConsumerContract.from_json(existing_contents)
|
123
|
+
end
|
124
|
+
|
125
|
+
def warn_and_stderr msg
|
126
|
+
error_stream.puts msg
|
127
|
+
logger.warn msg
|
128
|
+
end
|
129
|
+
|
130
|
+
def info_and_puts msg
|
131
|
+
output_stream.puts msg
|
132
|
+
logger.info msg
|
133
|
+
end
|
134
|
+
|
135
|
+
def consumer_name
|
136
|
+
consumer_contract_details[:consumer][:name]
|
137
|
+
end
|
138
|
+
|
139
|
+
def provider_name
|
140
|
+
consumer_contract_details[:provider][:name]
|
141
|
+
end
|
142
|
+
|
143
|
+
def pactfile_path
|
144
|
+
raise 'You must specify a consumer and provider name' unless (consumer_name && provider_name)
|
145
|
+
file_path consumer_name, provider_name, pact_dir, unique: consumer_contract_details[:unique_pact_file_names]
|
146
|
+
end
|
147
|
+
|
148
|
+
def pact_dir
|
149
|
+
unless consumer_contract_details[:pact_dir]
|
150
|
+
raise ConsumerContractWriterError.new("Please indicate the directory to write the pact to by specifying the pact_dir field")
|
151
|
+
end
|
152
|
+
consumer_contract_details[:pact_dir]
|
153
|
+
end
|
154
|
+
|
155
|
+
def updating?
|
156
|
+
pactfile_write_mode == :update
|
157
|
+
end
|
158
|
+
|
159
|
+
def merging?
|
160
|
+
pactfile_write_mode == :merge
|
161
|
+
end
|
162
|
+
|
163
|
+
def log_message
|
164
|
+
if updating?
|
165
|
+
"Updating pact for #{provider_name} at #{pactfile_path}"
|
166
|
+
elsif merging?
|
167
|
+
"Merging interactions into pact for #{provider_name} at #{pactfile_path}"
|
168
|
+
else
|
169
|
+
"Writing pact for #{provider_name} to #{pactfile_path}"
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
def print_updating_warning
|
174
|
+
info_and_puts "*****************************************************************************"
|
175
|
+
info_and_puts "Updating existing file .#{pactfile_path.gsub(Dir.pwd, '')} as pactfile_write_mode is :update"
|
176
|
+
info_and_puts "Only interactions defined in this test run will be updated."
|
177
|
+
info_and_puts "As interactions are identified by description and provider state, pleased note that if either of these have changed, the old interactions won't be removed from the pact file until the specs are next run with :pactfile_write_mode => :overwrite."
|
178
|
+
info_and_puts "*****************************************************************************"
|
179
|
+
end
|
180
|
+
end
|
181
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
require 'pact/shared/active_support_support'
|
2
|
+
require 'pact/consumer_contract/request_decorator'
|
3
|
+
require 'pact/consumer_contract/response_decorator'
|
4
|
+
|
5
|
+
module Pact
|
6
|
+
class InteractionDecorator
|
7
|
+
|
8
|
+
include ActiveSupportSupport
|
9
|
+
|
10
|
+
def initialize interaction, decorator_options = {}
|
11
|
+
@interaction = interaction
|
12
|
+
@decorator_options = decorator_options
|
13
|
+
end
|
14
|
+
|
15
|
+
def as_json options = {}
|
16
|
+
hash = { :description => interaction.description }
|
17
|
+
hash[:providerState] = interaction.provider_state if interaction.provider_state
|
18
|
+
hash[:request] = decorate_request.as_json(options)
|
19
|
+
hash[:response] = decorate_response.as_json(options)
|
20
|
+
fix_all_the_things hash
|
21
|
+
end
|
22
|
+
|
23
|
+
def to_json(options = {})
|
24
|
+
as_json(options).to_json(options)
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
attr_reader :interaction
|
30
|
+
|
31
|
+
def decorate_request
|
32
|
+
RequestDecorator.new(interaction.request, @decorator_options)
|
33
|
+
end
|
34
|
+
|
35
|
+
def decorate_response
|
36
|
+
ResponseDecorator.new(interaction.response, @decorator_options)
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
require 'pact/reification'
|
2
|
+
|
3
|
+
module Pact
|
4
|
+
class RequestDecorator
|
5
|
+
|
6
|
+
def initialize request, decorator_options = {}
|
7
|
+
@request = request
|
8
|
+
@decorator_options = decorator_options
|
9
|
+
end
|
10
|
+
|
11
|
+
def to_json(options = {})
|
12
|
+
as_json.to_json(options)
|
13
|
+
end
|
14
|
+
|
15
|
+
def as_json options = {}
|
16
|
+
hash = to_hash_without_rules
|
17
|
+
include_matching_rules? ? with_matching_rules(hash) : hash
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
attr_reader :request
|
23
|
+
|
24
|
+
def to_hash_without_rules
|
25
|
+
hash = {
|
26
|
+
method: request.method,
|
27
|
+
path: path
|
28
|
+
}
|
29
|
+
hash[:query] = query if request.specified?(:query)
|
30
|
+
hash[:headers] = headers if request.specified?(:headers)
|
31
|
+
hash[:body] = body if request.specified?(:body)
|
32
|
+
hash
|
33
|
+
end
|
34
|
+
|
35
|
+
def path
|
36
|
+
Pact::Reification.from_term(request.path)
|
37
|
+
end
|
38
|
+
|
39
|
+
def query
|
40
|
+
Pact::Reification.from_term(request.query)
|
41
|
+
end
|
42
|
+
|
43
|
+
def headers
|
44
|
+
Pact::Reification.from_term(request.headers)
|
45
|
+
end
|
46
|
+
|
47
|
+
# This feels wrong to be checking the class type of the body
|
48
|
+
# Do this better somehow.
|
49
|
+
def body
|
50
|
+
if content_type_is_form && request.body.is_a?(Hash)
|
51
|
+
URI.encode_www_form convert_hash_body_to_array_of_arrays
|
52
|
+
else
|
53
|
+
Pact::Reification.from_term(request.body)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def content_type_is_form
|
58
|
+
request.content_type? 'application/x-www-form-urlencoded'
|
59
|
+
end
|
60
|
+
|
61
|
+
#This probably belongs somewhere else.
|
62
|
+
def convert_hash_body_to_array_of_arrays
|
63
|
+
arrays = []
|
64
|
+
request.body.keys.each do | key |
|
65
|
+
[*request.body[key]].each do | value |
|
66
|
+
arrays << [key, value]
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
Pact::Reification.from_term(arrays)
|
71
|
+
end
|
72
|
+
|
73
|
+
def include_matching_rules?
|
74
|
+
pact_specification_version && !pact_specification_version.start_with?('1')
|
75
|
+
end
|
76
|
+
|
77
|
+
def with_matching_rules hash
|
78
|
+
matching_rules = Pact::MatchingRules.extract request.to_hash
|
79
|
+
return hash if matching_rules.empty?
|
80
|
+
hash.merge(matchingRules: matching_rules)
|
81
|
+
end
|
82
|
+
|
83
|
+
def pact_specification_version
|
84
|
+
@decorator_options[:pact_specification_version]
|
85
|
+
end
|
86
|
+
|
87
|
+
end
|
88
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
require 'pact/matching_rules'
|
2
|
+
require 'pact/term'
|
3
|
+
|
4
|
+
module Pact
|
5
|
+
class ResponseDecorator
|
6
|
+
|
7
|
+
def initialize response, decorator_options = {}
|
8
|
+
@response = response
|
9
|
+
@decorator_options = decorator_options
|
10
|
+
end
|
11
|
+
|
12
|
+
def to_json(options = {})
|
13
|
+
as_json.to_json(options)
|
14
|
+
end
|
15
|
+
|
16
|
+
def as_json options = {}
|
17
|
+
include_matching_rules? ? with_matching_rules(attributes_hash) : attributes_hash
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
attr_reader :response
|
23
|
+
|
24
|
+
def attributes_hash
|
25
|
+
hash = {}
|
26
|
+
hash[:status] = response.status if response.specified?(:status)
|
27
|
+
hash[:headers] = response.headers if response.specified?(:headers)
|
28
|
+
hash[:body] = response.body if response.specified?(:body)
|
29
|
+
hash
|
30
|
+
end
|
31
|
+
|
32
|
+
def include_matching_rules?
|
33
|
+
pact_specification_version && !pact_specification_version.start_with?('1')
|
34
|
+
end
|
35
|
+
|
36
|
+
def with_matching_rules hash
|
37
|
+
matching_rules = Pact::MatchingRules.extract hash
|
38
|
+
example = Pact::Reification.from_term hash
|
39
|
+
return example if matching_rules.empty?
|
40
|
+
example.merge(matchingRules: matching_rules)
|
41
|
+
end
|
42
|
+
|
43
|
+
def pact_specification_version
|
44
|
+
@decorator_options[:pact_specification_version]
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|