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,64 @@
|
|
1
|
+
require 'pact/mock_service/request_handlers/base_request_handler'
|
2
|
+
|
3
|
+
module Pact
|
4
|
+
module MockService
|
5
|
+
module RequestHandlers
|
6
|
+
class Options < BaseRequestHandler
|
7
|
+
|
8
|
+
attr_reader :name, :logger, :cors_enabled
|
9
|
+
|
10
|
+
HTTP_ACCESS_CONTROL_REQUEST_METHOD = "HTTP_ACCESS_CONTROL_REQUEST_METHOD".freeze
|
11
|
+
HTTP_ACCESS_CONTROL_REQUEST_HEADERS = "HTTP_ACCESS_CONTROL_REQUEST_HEADERS".freeze
|
12
|
+
ACCESS_CONTROL_ALLOW_CREDENTIALS = "Access-Control-Allow-Credentials".freeze
|
13
|
+
ACCESS_CONTROL_ALLOW_ORIGIN = "Access-Control-Allow-Origin".freeze
|
14
|
+
ACCESS_CONTROL_ALLOW_METHODS = "Access-Control-Allow-Methods".freeze
|
15
|
+
ACCESS_CONTROL_ALLOW_HEADERS = "Access-Control-Allow-Headers".freeze
|
16
|
+
AUTHORIZATION = "authorization".freeze
|
17
|
+
COOKIE = "cookie".freeze
|
18
|
+
HTTP_ORIGIN = "HTTP_ORIGIN".freeze
|
19
|
+
ALL_METHODS = "DELETE, POST, GET, HEAD, PUT, TRACE, CONNECT, PATCH".freeze
|
20
|
+
REQUEST_METHOD = "REQUEST_METHOD".freeze
|
21
|
+
OPTIONS = "OPTIONS".freeze
|
22
|
+
X_PACT_MOCK_SERVICE_REGEXP = /x-pact-mock-service/i
|
23
|
+
|
24
|
+
def initialize name, logger, cors_enabled
|
25
|
+
@name = name
|
26
|
+
@logger = logger
|
27
|
+
@cors_enabled = cors_enabled
|
28
|
+
end
|
29
|
+
|
30
|
+
def match? env
|
31
|
+
is_options_request?(env) && (cors_enabled || is_administration_request?(env))
|
32
|
+
end
|
33
|
+
|
34
|
+
def respond env
|
35
|
+
cors_headers = {
|
36
|
+
ACCESS_CONTROL_ALLOW_ORIGIN => env.fetch(HTTP_ORIGIN,'*'),
|
37
|
+
ACCESS_CONTROL_ALLOW_HEADERS => env.fetch(HTTP_ACCESS_CONTROL_REQUEST_HEADERS, '*'),
|
38
|
+
ACCESS_CONTROL_ALLOW_METHODS => ALL_METHODS
|
39
|
+
}
|
40
|
+
|
41
|
+
if is_request_with_credentials?(env)
|
42
|
+
cors_headers[ACCESS_CONTROL_ALLOW_CREDENTIALS] = "true"
|
43
|
+
end
|
44
|
+
|
45
|
+
logger.info "Received OPTIONS request for mock service administration endpoint #{env[HTTP_ACCESS_CONTROL_REQUEST_METHOD]} #{env['PATH_INFO']}. Returning CORS headers: #{cors_headers}."
|
46
|
+
[200, cors_headers, []]
|
47
|
+
end
|
48
|
+
|
49
|
+
def is_options_request? env
|
50
|
+
env[REQUEST_METHOD] == OPTIONS
|
51
|
+
end
|
52
|
+
|
53
|
+
def is_administration_request? env
|
54
|
+
(env[HTTP_ACCESS_CONTROL_REQUEST_HEADERS] || '').match(X_PACT_MOCK_SERVICE_REGEXP)
|
55
|
+
end
|
56
|
+
|
57
|
+
def is_request_with_credentials? env
|
58
|
+
headers = (env[HTTP_ACCESS_CONTROL_REQUEST_HEADERS] || '').split(",").map { |header| header.strip.downcase }
|
59
|
+
headers.include?(AUTHORIZATION) || headers.include?(COOKIE)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
require 'pact/mock_service/request_handlers/base_administration_request_handler'
|
2
|
+
require 'pact/consumer_contract/consumer_contract_writer'
|
3
|
+
|
4
|
+
module Pact
|
5
|
+
module MockService
|
6
|
+
module RequestHandlers
|
7
|
+
class PactPost < BaseAdministrationRequestHandler
|
8
|
+
|
9
|
+
attr_accessor :consumer_contract, :verified_interactions, :default_options, :session
|
10
|
+
|
11
|
+
def initialize name, logger, session
|
12
|
+
super name, logger
|
13
|
+
@verified_interactions = session.verified_interactions
|
14
|
+
@default_options = {}
|
15
|
+
@default_options.merge!(session.consumer_contract_details)
|
16
|
+
@session = session
|
17
|
+
end
|
18
|
+
|
19
|
+
def request_path
|
20
|
+
'/pact'
|
21
|
+
end
|
22
|
+
|
23
|
+
def request_method
|
24
|
+
'POST'
|
25
|
+
end
|
26
|
+
|
27
|
+
def respond env
|
28
|
+
body = env['rack.input'].string
|
29
|
+
consumer_contract_details = body.size > 0 ? JSON.parse(body, symbolize_names: true) : {}
|
30
|
+
consumer_contract_params = default_options.merge(consumer_contract_details.merge(interactions: verified_interactions))
|
31
|
+
consumer_contract_writer = ConsumerContractWriter.new(consumer_contract_params, logger)
|
32
|
+
session.record_pact_written
|
33
|
+
json_response(consumer_contract_writer.write)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
require 'pact/mock_service/request_handlers/base_administration_request_handler'
|
2
|
+
|
3
|
+
module Pact
|
4
|
+
module MockService
|
5
|
+
module RequestHandlers
|
6
|
+
|
7
|
+
class SessionDelete < BaseAdministrationRequestHandler
|
8
|
+
|
9
|
+
attr_accessor :session
|
10
|
+
|
11
|
+
def initialize name, logger, session
|
12
|
+
super name, logger
|
13
|
+
@session = session
|
14
|
+
end
|
15
|
+
|
16
|
+
def request_path
|
17
|
+
'/session'
|
18
|
+
end
|
19
|
+
|
20
|
+
def request_method
|
21
|
+
'DELETE'
|
22
|
+
end
|
23
|
+
|
24
|
+
def respond env
|
25
|
+
session.clear_all
|
26
|
+
logger.info "Cleared session"
|
27
|
+
text_response 'Cleared session'
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
require 'pact/mock_service/request_handlers/base_administration_request_handler'
|
2
|
+
|
3
|
+
module Pact
|
4
|
+
module MockService
|
5
|
+
module RequestHandlers
|
6
|
+
class VerificationGet < BaseAdministrationRequestHandler
|
7
|
+
|
8
|
+
def initialize name, logger, session
|
9
|
+
super name, logger
|
10
|
+
@expected_interactions = session.expected_interactions
|
11
|
+
@actual_interactions = session.actual_interactions
|
12
|
+
end
|
13
|
+
|
14
|
+
def request_path
|
15
|
+
'/interactions/verification'
|
16
|
+
end
|
17
|
+
|
18
|
+
def request_method
|
19
|
+
'GET'
|
20
|
+
end
|
21
|
+
|
22
|
+
def respond env
|
23
|
+
verification = Pact::MockService::Interactions::Verification.new(expected_interactions, actual_interactions)
|
24
|
+
example_desc = example_description(env)
|
25
|
+
example_desc = example_desc ? " for example #{example_desc.inspect}" : ''
|
26
|
+
if verification.all_matched?
|
27
|
+
logger.info "Verifying - interactions matched#{example_desc}"
|
28
|
+
text_response('Interactions matched')
|
29
|
+
else
|
30
|
+
error_message = FailureMessage.new(verification).to_s
|
31
|
+
logger.warn "Verifying - actual interactions do not match expected interactions#{example_desc}. \n#{error_message}"
|
32
|
+
logger.warn error_message
|
33
|
+
response_message = "Actual interactions do not match expected interactions for mock #{name}.\n\n#{error_message}See #{logger.description} for details."
|
34
|
+
text_response(response_message, 500)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
attr_accessor :expected_interactions, :actual_interactions
|
41
|
+
|
42
|
+
def example_description env
|
43
|
+
params_hash(env).fetch("example_description", [])[0]
|
44
|
+
end
|
45
|
+
|
46
|
+
class FailureMessage
|
47
|
+
|
48
|
+
def initialize verification
|
49
|
+
@verification = verification
|
50
|
+
end
|
51
|
+
|
52
|
+
def to_s
|
53
|
+
titles_and_summaries.collect do | title, summaries |
|
54
|
+
"#{title}:\n\t#{summaries.join("\n\t")}\n\n" if summaries.any?
|
55
|
+
end.compact.join + verification.interaction_mismatches.collect(&:to_s).join("\n\n") + "\n"
|
56
|
+
|
57
|
+
end
|
58
|
+
|
59
|
+
private
|
60
|
+
|
61
|
+
attr_reader :verification
|
62
|
+
|
63
|
+
def titles_and_summaries
|
64
|
+
{
|
65
|
+
"Incorrect requests" => verification.interaction_mismatches_summaries,
|
66
|
+
"Missing requests" => verification.missing_interactions_summaries,
|
67
|
+
"Unexpected requests" => verification.unexpected_requests_summaries,
|
68
|
+
}
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
require 'pact/mock_service/request_handlers/interaction_post'
|
2
|
+
require 'pact/mock_service/request_handlers/interactions_put'
|
3
|
+
require 'pact/mock_service/request_handlers/index_get'
|
4
|
+
require 'pact/mock_service/request_handlers/interaction_delete'
|
5
|
+
require 'pact/mock_service/request_handlers/interaction_replay'
|
6
|
+
require 'pact/mock_service/request_handlers/log_get'
|
7
|
+
require 'pact/mock_service/request_handlers/options'
|
8
|
+
require 'pact/mock_service/request_handlers/missing_interactions_get'
|
9
|
+
require 'pact/mock_service/request_handlers/pact_post'
|
10
|
+
require 'pact/mock_service/request_handlers/session_delete'
|
11
|
+
require 'pact/mock_service/request_handlers/verification_get'
|
12
|
+
require 'pact/consumer/request'
|
13
|
+
require 'pact/support'
|
14
|
+
|
15
|
+
module Pact
|
16
|
+
module MockService
|
17
|
+
module RequestHandlers
|
18
|
+
|
19
|
+
def self.new *args
|
20
|
+
App.new(*args)
|
21
|
+
end
|
22
|
+
|
23
|
+
class App < ::Rack::Cascade
|
24
|
+
def initialize name, logger, session, options
|
25
|
+
super [
|
26
|
+
Options.new(name, logger, options[:cors_enabled]),
|
27
|
+
SessionDelete.new(name, logger, session),
|
28
|
+
MissingInteractionsGet.new(name, logger, session),
|
29
|
+
VerificationGet.new(name, logger, session),
|
30
|
+
InteractionPost.new(name, logger, session, Pact::SpecificationVersion.new(options.fetch(:pact_specification_version))),
|
31
|
+
InteractionsPut.new(name, logger, session, Pact::SpecificationVersion.new(options.fetch(:pact_specification_version))),
|
32
|
+
InteractionDelete.new(name, logger, session),
|
33
|
+
LogGet.new(name, logger),
|
34
|
+
PactPost.new(name, logger, session),
|
35
|
+
IndexGet.new(name, logger),
|
36
|
+
InteractionReplay.new(name, logger, session, options[:cors_enabled], options[:stub_pactfile_paths])
|
37
|
+
]
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module Pact
|
2
|
+
module MockService
|
3
|
+
class ResponseDecorator
|
4
|
+
|
5
|
+
def initialize response
|
6
|
+
@response = response
|
7
|
+
end
|
8
|
+
|
9
|
+
def to_json(options = {})
|
10
|
+
as_json.to_json(options)
|
11
|
+
end
|
12
|
+
|
13
|
+
def as_json options = {}
|
14
|
+
to_hash
|
15
|
+
end
|
16
|
+
|
17
|
+
def to_hash
|
18
|
+
hash = {}
|
19
|
+
hash[:status] = response.status if response.specified?(:status)
|
20
|
+
hash[:headers] = response.headers if response.specified?(:headers)
|
21
|
+
hash[:body] = response.body if response.specified?(:body)
|
22
|
+
hash
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
attr_reader :response
|
28
|
+
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,125 @@
|
|
1
|
+
require 'find_a_port'
|
2
|
+
require 'pact/mock_service/app'
|
3
|
+
require 'pact/consumer/mock_service/set_location'
|
4
|
+
require 'pact/mock_service/run'
|
5
|
+
require 'pact/mock_service/server/webrick_request_monkeypatch'
|
6
|
+
require 'pact/specification_version'
|
7
|
+
|
8
|
+
module Pact
|
9
|
+
module MockService
|
10
|
+
class Run
|
11
|
+
|
12
|
+
def self.call options
|
13
|
+
new(options).call
|
14
|
+
end
|
15
|
+
|
16
|
+
def initialize options
|
17
|
+
@options = options
|
18
|
+
end
|
19
|
+
|
20
|
+
def call
|
21
|
+
require 'pact/mock_service/app'
|
22
|
+
|
23
|
+
trap(:INT) { call_shutdown_hooks }
|
24
|
+
trap(:TERM) { call_shutdown_hooks }
|
25
|
+
|
26
|
+
require_monkeypatch
|
27
|
+
|
28
|
+
Rack::Handler::WEBrick.run(mock_service, **webbrick_opts)
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
attr_reader :options
|
34
|
+
|
35
|
+
def mock_service
|
36
|
+
@mock_service ||= begin
|
37
|
+
mock_service = Pact::MockService.new(service_options)
|
38
|
+
Pact::Consumer::SetLocation.new(mock_service, base_url)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def call_shutdown_hooks
|
43
|
+
unless @shutting_down
|
44
|
+
@shutting_down = true
|
45
|
+
begin
|
46
|
+
mock_service.shutdown
|
47
|
+
ensure
|
48
|
+
Rack::Handler::WEBrick.shutdown
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def service_options
|
54
|
+
# dummy pact_specification_version is needed to stop RequestHandlers blowing up
|
55
|
+
service_options = {
|
56
|
+
pact_dir: options[:pact_dir],
|
57
|
+
log_level: options[:log_level],
|
58
|
+
unique_pact_file_names: options[:unique_pact_file_names],
|
59
|
+
consumer: options[:consumer],
|
60
|
+
provider: options[:provider],
|
61
|
+
broker_token: options[:broker_token],
|
62
|
+
broker_username: options[:broker_username],
|
63
|
+
broker_password: options[:broker_password],
|
64
|
+
cors_enabled: options[:cors],
|
65
|
+
pact_specification_version: options[:pact_specification_version] || Pact::SpecificationVersion::NIL_VERSION.to_s,
|
66
|
+
pactfile_write_mode: options[:pact_file_write_mode],
|
67
|
+
stub_pactfile_paths: options[:stub_pactfile_paths]
|
68
|
+
}
|
69
|
+
service_options[:log_file] = open_log_file if options[:log]
|
70
|
+
service_options
|
71
|
+
end
|
72
|
+
|
73
|
+
def open_log_file
|
74
|
+
FileUtils.mkdir_p File.dirname(options[:log])
|
75
|
+
log = File.open(options[:log], 'w')
|
76
|
+
log.sync = true
|
77
|
+
log
|
78
|
+
end
|
79
|
+
|
80
|
+
def webbrick_opts
|
81
|
+
# By default, the webrick logs go to $stderr, which then show up as an ERROR
|
82
|
+
# log in pact-go, so it was changed to $stdout.
|
83
|
+
# $stdout needs sync = true for pact-js to determine the port dynamically from
|
84
|
+
# the output (otherwise it does not flush in time for the port to be read)
|
85
|
+
$stdout.sync = true
|
86
|
+
opts = {
|
87
|
+
:Port => port,
|
88
|
+
:Host => host,
|
89
|
+
:AccessLog => [],
|
90
|
+
:Logger => WEBrick::BasicLog.new($stdout)
|
91
|
+
}
|
92
|
+
opts.merge!({
|
93
|
+
:SSLCertificate => OpenSSL::X509::Certificate.new(File.open(options[:sslcert]).read) }) if options[:sslcert]
|
94
|
+
opts.merge!({
|
95
|
+
:SSLPrivateKey => OpenSSL::PKey::RSA.new(File.open(options[:sslkey]).read) }) if options[:sslkey]
|
96
|
+
opts.merge!(ssl_opts) if options[:ssl]
|
97
|
+
opts.merge!(options[:webbrick_options]) if options[:webbrick_options]
|
98
|
+
opts
|
99
|
+
end
|
100
|
+
|
101
|
+
def ssl_opts
|
102
|
+
{
|
103
|
+
:SSLEnable => true,
|
104
|
+
:SSLCertName => [ ["CN", host] ]
|
105
|
+
}
|
106
|
+
end
|
107
|
+
|
108
|
+
def port
|
109
|
+
@port ||= (options[:port] || FindAPort.available_port).to_i
|
110
|
+
end
|
111
|
+
|
112
|
+
def host
|
113
|
+
@host ||= options[:host] || "localhost"
|
114
|
+
end
|
115
|
+
|
116
|
+
def base_url
|
117
|
+
options[:ssl] ? "https://#{host}:#{port}" : "http://#{host}:#{port}"
|
118
|
+
end
|
119
|
+
|
120
|
+
def require_monkeypatch
|
121
|
+
require options[:monkeypatch] if options[:monkeypatch]
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'pact/mock_service/server/spawn'
|
2
|
+
|
3
|
+
module Pact
|
4
|
+
module MockService
|
5
|
+
module Server
|
6
|
+
class Respawn
|
7
|
+
|
8
|
+
def self.call pidfile, port, ssl = false
|
9
|
+
if pidfile.file_exists_and_process_running?
|
10
|
+
pidfile.kill_process
|
11
|
+
end
|
12
|
+
|
13
|
+
Spawn.(pidfile, port, ssl) do
|
14
|
+
yield
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
require 'pact/mock_service/server/wait_for_server_up'
|
2
|
+
|
3
|
+
module Pact
|
4
|
+
module MockService
|
5
|
+
module Server
|
6
|
+
class Spawn
|
7
|
+
|
8
|
+
class PortUnavailableError < StandardError; end
|
9
|
+
|
10
|
+
def self.call pidfile, host, port, ssl = false
|
11
|
+
if pidfile.can_start?
|
12
|
+
if port_available? host, port
|
13
|
+
pid = fork do
|
14
|
+
yield
|
15
|
+
end
|
16
|
+
pidfile.pid = pid
|
17
|
+
Process.detach(pid)
|
18
|
+
Server::WaitForServerUp.(host, port, {ssl: ssl})
|
19
|
+
pidfile.write
|
20
|
+
else
|
21
|
+
raise PortUnavailableError.new("ERROR: Port #{port} already in use.")
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.port_available? host, port
|
27
|
+
server = TCPServer.new(host, port)
|
28
|
+
true
|
29
|
+
rescue
|
30
|
+
false
|
31
|
+
ensure
|
32
|
+
server.close if server
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
require 'timeout'
|
2
|
+
require 'net/http'
|
3
|
+
require 'openssl'
|
4
|
+
|
5
|
+
module Pact
|
6
|
+
module MockService
|
7
|
+
module Server
|
8
|
+
class WaitForServerUp
|
9
|
+
|
10
|
+
def self.call(host, port, options = {ssl: false})
|
11
|
+
tries = 0
|
12
|
+
responsive = false
|
13
|
+
while !(responsive = responsive?(host, port, options)) && tries < 100
|
14
|
+
tries += 1
|
15
|
+
sleep 1
|
16
|
+
end
|
17
|
+
raise "Timed out waiting for server to start up on port #{port}" if !responsive
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.responsive? host, port, options
|
21
|
+
http = Net::HTTP.new(host, port)
|
22
|
+
if options[:ssl]
|
23
|
+
http.use_ssl = true
|
24
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
25
|
+
scheme = 'https'
|
26
|
+
else
|
27
|
+
scheme = 'http'
|
28
|
+
end
|
29
|
+
http.start {
|
30
|
+
request = Net::HTTP::Get.new "#{scheme}://#{host}:#{port}/"
|
31
|
+
request['X-Pact-Mock-Service'] = true
|
32
|
+
response = http.request request
|
33
|
+
response.code == '200'
|
34
|
+
}
|
35
|
+
rescue SystemCallError => e
|
36
|
+
return false
|
37
|
+
rescue EOFError
|
38
|
+
return false
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module WEBrick
|
2
|
+
class HTTPRequest
|
3
|
+
alias_method :pact_original_meta_vars, :meta_vars
|
4
|
+
|
5
|
+
def meta_vars
|
6
|
+
original_underscored_headers = []
|
7
|
+
self.each{|key, val| original_underscored_headers << key if key.include?("_") }
|
8
|
+
# This header allows us to restore the original format (eg. underscored) of the headers
|
9
|
+
# when parsing the incoming Rack env back to a response object.
|
10
|
+
vars = pact_original_meta_vars
|
11
|
+
vars["X_PACT_UNDERSCORED_HEADER_NAMES"] = original_underscored_headers.join(",")
|
12
|
+
vars
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,96 @@
|
|
1
|
+
require 'pact/mock_service/interactions/expected_interactions'
|
2
|
+
require 'pact/mock_service/interactions/actual_interactions'
|
3
|
+
require 'pact/mock_service/interactions/verified_interactions'
|
4
|
+
require 'pact/mock_service/interaction_decorator'
|
5
|
+
require 'pact/mock_service/interactions/interaction_diff_message'
|
6
|
+
require 'pact/mock_service/errors'
|
7
|
+
|
8
|
+
module Pact
|
9
|
+
module MockService
|
10
|
+
class Session
|
11
|
+
|
12
|
+
attr_reader :expected_interactions, :actual_interactions, :verified_interactions, :consumer_contract_details, :logger
|
13
|
+
|
14
|
+
def initialize options
|
15
|
+
@pact_written = false
|
16
|
+
@logger = options[:logger]
|
17
|
+
@expected_interactions = Interactions::ExpectedInteractions.new
|
18
|
+
@actual_interactions = Interactions::ActualInteractions.new
|
19
|
+
@verified_interactions = Interactions::VerifiedInteractions.new
|
20
|
+
@warn_on_too_many_interactions = options[:warn_on_too_many_interactions] || false
|
21
|
+
@max_concurrent_interactions_before_warning = get_max_concurrent_interactions_before_warning
|
22
|
+
@consumer_contract_details = {
|
23
|
+
pact_dir: options[:pact_dir],
|
24
|
+
consumer: {name: options[:consumer]},
|
25
|
+
provider: {name: options[:provider]},
|
26
|
+
interactions: verified_interactions,
|
27
|
+
pact_specification_version: options[:pact_specification_version],
|
28
|
+
pactfile_write_mode: options[:pactfile_write_mode]
|
29
|
+
}
|
30
|
+
end
|
31
|
+
|
32
|
+
def pact_written?
|
33
|
+
@pact_written
|
34
|
+
end
|
35
|
+
|
36
|
+
def record_pact_written
|
37
|
+
@pact_written = true
|
38
|
+
end
|
39
|
+
|
40
|
+
def set_expected_interactions interactions
|
41
|
+
clear_expected_and_actual_interactions
|
42
|
+
interactions.each do | interaction |
|
43
|
+
add_expected_interaction interaction
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def clear_expected_and_actual_interactions
|
48
|
+
expected_interactions.clear
|
49
|
+
actual_interactions.clear
|
50
|
+
end
|
51
|
+
|
52
|
+
def clear_all
|
53
|
+
expected_interactions.clear
|
54
|
+
actual_interactions.clear
|
55
|
+
verified_interactions.clear
|
56
|
+
@pact_written = false
|
57
|
+
end
|
58
|
+
|
59
|
+
def add_expected_interaction interaction
|
60
|
+
if (previous_interaction = interaction_already_verified_with_same_description_and_provider_state_but_not_equal(interaction))
|
61
|
+
handle_almost_duplicate_interaction previous_interaction, interaction
|
62
|
+
else
|
63
|
+
really_add_expected_interaction interaction
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
private
|
68
|
+
|
69
|
+
attr_reader :warn_on_too_many_interactions, :max_concurrent_interactions_before_warning
|
70
|
+
|
71
|
+
def really_add_expected_interaction interaction
|
72
|
+
expected_interactions << interaction
|
73
|
+
logger.info "Registered expected interaction #{interaction.request.method_and_path}"
|
74
|
+
logger.debug JSON.pretty_generate InteractionDecorator.new(interaction)
|
75
|
+
if warn_on_too_many_interactions && expected_interactions.size > max_concurrent_interactions_before_warning
|
76
|
+
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."
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def handle_almost_duplicate_interaction previous_interaction, interaction
|
81
|
+
message = Interactions::InteractionDiffMessage.new(previous_interaction, interaction).to_s
|
82
|
+
logger.error message
|
83
|
+
raise SameSameButDifferentError, message
|
84
|
+
end
|
85
|
+
|
86
|
+
def interaction_already_verified_with_same_description_and_provider_state_but_not_equal interaction
|
87
|
+
other = verified_interactions.find_matching_description_and_provider_state interaction
|
88
|
+
other && other != interaction ? other : nil
|
89
|
+
end
|
90
|
+
|
91
|
+
def get_max_concurrent_interactions_before_warning
|
92
|
+
ENV['PACT_MAX_CONCURRENT_INTERACTIONS_BEFORE_WARNING'] ? ENV['PACT_MAX_CONCURRENT_INTERACTIONS_BEFORE_WARNING'].to_i : 3
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|