hs-pact-mock_service 3.9.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|