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,79 @@
|
|
|
1
|
+
require 'net/http'
|
|
2
|
+
require 'pact/mock_service/interaction_decorator'
|
|
3
|
+
require 'pact/errors'
|
|
4
|
+
|
|
5
|
+
module Pact
|
|
6
|
+
module MockService
|
|
7
|
+
|
|
8
|
+
class VerificationFailedError < Pact::Error; end
|
|
9
|
+
class AddInteractionError < Pact::Error; end
|
|
10
|
+
class WritePactError < Pact::Error; end
|
|
11
|
+
|
|
12
|
+
class Client
|
|
13
|
+
|
|
14
|
+
MOCK_SERVICE_ADMINISTRATON_HEADERS = {'X-Pact-Mock-Service' => 'true'}
|
|
15
|
+
|
|
16
|
+
def initialize port, host = 'localhost'
|
|
17
|
+
@http = Net::HTTP.new(host, port)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def verify example_description
|
|
21
|
+
response = http.request_get("/interactions/verification?example_description=#{CGI.escape(example_description)}", MOCK_SERVICE_ADMINISTRATON_HEADERS)
|
|
22
|
+
raise VerificationFailedError.new("\e[31m#{response.body}\e[m") unless response.is_a? Net::HTTPSuccess
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def log msg
|
|
26
|
+
http.request_get("/log?msg=#{CGI.escape(msg)}", MOCK_SERVICE_ADMINISTRATON_HEADERS)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def wait_for_interactions wait_max_seconds, poll_interval
|
|
30
|
+
wait_until_true(wait_max_seconds, poll_interval) do
|
|
31
|
+
response = http.request_get("/interactions/missing", MOCK_SERVICE_ADMINISTRATON_HEADERS)
|
|
32
|
+
JSON.parse(response.body)['size'] == 0
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def clear_interactions example_description
|
|
37
|
+
http.delete("/interactions?example_description=#{CGI.escape(example_description)}", MOCK_SERVICE_ADMINISTRATON_HEADERS)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def add_expected_interaction interaction
|
|
41
|
+
response = http.request_post(
|
|
42
|
+
'/interactions',
|
|
43
|
+
interaction_json(interaction),
|
|
44
|
+
MOCK_SERVICE_ADMINISTRATON_HEADERS.merge("Content-Type" => "application/json")
|
|
45
|
+
)
|
|
46
|
+
raise AddInteractionError.new("\e[31m#{response.body}\e[m") unless response.is_a? Net::HTTPSuccess
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def self.clear_interactions mock_service_base_url, example_description
|
|
50
|
+
uri = URI(mock_service_base_url)
|
|
51
|
+
Net::HTTP.new(uri.host, uri.port).delete("/interactions?example_description=#{CGI.escape(example_description)}", MOCK_SERVICE_ADMINISTRATON_HEADERS)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def write_pact pacticipant_details
|
|
55
|
+
response = http.request_post("/pact", pacticipant_details.to_json, MOCK_SERVICE_ADMINISTRATON_HEADERS.merge("Content-Type" => "application/json"))
|
|
56
|
+
raise WritePactError.new("\e[31m#{response.body}\e[m") unless response.is_a? Net::HTTPSuccess
|
|
57
|
+
response.body
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
private
|
|
61
|
+
|
|
62
|
+
attr_reader :http
|
|
63
|
+
|
|
64
|
+
#todo: in need a better home (where can we move it?)
|
|
65
|
+
def wait_until_true timeout=3, interval=0.1
|
|
66
|
+
time_limit = Time.now + timeout
|
|
67
|
+
loop do
|
|
68
|
+
result = yield
|
|
69
|
+
return if result || Time.now >= time_limit
|
|
70
|
+
sleep interval
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def interaction_json interaction
|
|
75
|
+
Pact::MockService::InteractionDecorator.new(interaction).to_json
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
require 'pact/consumer/mock_service/rack_request_helper'
|
|
2
|
+
require 'pact/mock_service/control_server/require_pacticipant_headers'
|
|
3
|
+
require 'pact/mock_service/control_server/index'
|
|
4
|
+
require 'pact/mock_service/control_server/mock_services'
|
|
5
|
+
require 'pact/mock_service/control_server/mock_service_creator'
|
|
6
|
+
require 'rack'
|
|
7
|
+
require 'rack/cascade'
|
|
8
|
+
|
|
9
|
+
module Pact
|
|
10
|
+
module MockService
|
|
11
|
+
module ControlServer
|
|
12
|
+
class App
|
|
13
|
+
|
|
14
|
+
include Pact::Consumer::RackRequestHelper
|
|
15
|
+
|
|
16
|
+
def initialize options = {}
|
|
17
|
+
@mock_services = mock_services = MockServices.new([])
|
|
18
|
+
@app = Rack::Builder.new {
|
|
19
|
+
run Rack::Cascade.new([
|
|
20
|
+
Index.new,
|
|
21
|
+
Rack::Builder.new {
|
|
22
|
+
use RequirePacticipantHeaders
|
|
23
|
+
run Rack::Cascade.new([
|
|
24
|
+
mock_services,
|
|
25
|
+
MockServiceCreator.new(mock_services, options)
|
|
26
|
+
])
|
|
27
|
+
}
|
|
28
|
+
])
|
|
29
|
+
}
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def call env
|
|
33
|
+
@app.call(env)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def shutdown
|
|
37
|
+
@mock_services.shutdown
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# Delegates the incoming request that was sent to the control server
|
|
2
|
+
# to the underlying MockService
|
|
3
|
+
# if the X-Pact-Consumer and X-Pact-Provider headers match
|
|
4
|
+
# the consumer and provider for this MockService.
|
|
5
|
+
|
|
6
|
+
module Pact
|
|
7
|
+
module MockService
|
|
8
|
+
module ControlServer
|
|
9
|
+
|
|
10
|
+
class Delegator
|
|
11
|
+
|
|
12
|
+
HTTP_X_PACT_CONSUMER = 'HTTP_X_PACT_CONSUMER'.freeze
|
|
13
|
+
HTTP_X_PACT_PROVIDER = 'HTTP_X_PACT_PROVIDER'.freeze
|
|
14
|
+
PACT_MOCK_SERVICE_HEADER = {'HTTP_X_PACT_MOCK_SERVICE' => 'true'}.freeze
|
|
15
|
+
NOT_FOUND_RESPONSE = [404, {}, []].freeze
|
|
16
|
+
|
|
17
|
+
def initialize app, consumer_name, provider_name
|
|
18
|
+
@app = app
|
|
19
|
+
@consumer_name = consumer_name
|
|
20
|
+
@provider_name = provider_name
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def call env
|
|
24
|
+
return NOT_FOUND_RESPONSE unless consumer_and_provider_headers_match?(env)
|
|
25
|
+
delegate env
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def shutdown
|
|
29
|
+
@app.shutdown
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def consumer_and_provider_headers_match? env
|
|
35
|
+
env[HTTP_X_PACT_CONSUMER] == @consumer_name && env[HTTP_X_PACT_PROVIDER] == @provider_name
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def delegate env
|
|
39
|
+
@app.call(env.merge(PACT_MOCK_SERVICE_HEADER))
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
module Pact
|
|
2
|
+
module MockService
|
|
3
|
+
module ControlServer
|
|
4
|
+
class Index
|
|
5
|
+
|
|
6
|
+
HTTP_X_PACT_MOCK_SERVICE = 'HTTP_X_PACT_MOCK_SERVICE'
|
|
7
|
+
PATH_INFO = 'PATH_INFO'
|
|
8
|
+
INDEX_RESPONSE = [200, {'Content-Type' => 'text/plain'}, ['Control server running']].freeze
|
|
9
|
+
NOT_FOUND_RESPONSE = [404, {}, []].freeze
|
|
10
|
+
|
|
11
|
+
def call env
|
|
12
|
+
if is_index_request_with_mock_service_header? env
|
|
13
|
+
INDEX_RESPONSE
|
|
14
|
+
else
|
|
15
|
+
NOT_FOUND_RESPONSE
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def is_index_request_with_mock_service_header? env
|
|
20
|
+
env[HTTP_X_PACT_MOCK_SERVICE] && env[PATH_INFO].chomp("/").size == 0
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
require 'pact/mock_service/spawn'
|
|
2
|
+
require 'pact/mock_service/control_server/delegator'
|
|
3
|
+
require 'find_a_port'
|
|
4
|
+
require 'pact/mock_service/server/wait_for_server_up'
|
|
5
|
+
|
|
6
|
+
# Create a new MockService on a random port and delegate the incoming request to it
|
|
7
|
+
|
|
8
|
+
module Pact
|
|
9
|
+
module MockService
|
|
10
|
+
module ControlServer
|
|
11
|
+
class MockServiceCreator
|
|
12
|
+
|
|
13
|
+
attr_reader :options
|
|
14
|
+
|
|
15
|
+
def initialize mock_services, options
|
|
16
|
+
@mock_services = mock_services
|
|
17
|
+
@options = options
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def call env
|
|
21
|
+
consumer_name = env['HTTP_X_PACT_CONSUMER']
|
|
22
|
+
provider_name = env['HTTP_X_PACT_PROVIDER']
|
|
23
|
+
port = FindAPort.available_port
|
|
24
|
+
mock_service = Pact::MockService::Spawn.(consumer_name, provider_name, options[:host] || 'localhost', port, options)
|
|
25
|
+
delegator = Delegator.new(mock_service, consumer_name, provider_name)
|
|
26
|
+
@mock_services.add(delegator)
|
|
27
|
+
delegator.call(env)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
require 'rack'
|
|
2
|
+
require 'rack/cascade'
|
|
3
|
+
|
|
4
|
+
module Pact
|
|
5
|
+
module MockService
|
|
6
|
+
module ControlServer
|
|
7
|
+
class MockServices < Rack::Cascade
|
|
8
|
+
|
|
9
|
+
def add app
|
|
10
|
+
mock_services << app
|
|
11
|
+
super
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def shutdown
|
|
15
|
+
mock_services.each(&:shutdown)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
def mock_services
|
|
21
|
+
@mock_services ||= []
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
module Pact
|
|
2
|
+
module MockService
|
|
3
|
+
module ControlServer
|
|
4
|
+
class RequirePacticipantHeaders
|
|
5
|
+
|
|
6
|
+
def initialize app
|
|
7
|
+
@app = app
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def call env
|
|
11
|
+
if env['HTTP_X_PACT_CONSUMER'] && env['HTTP_X_PACT_PROVIDER']
|
|
12
|
+
@app.call(env)
|
|
13
|
+
else
|
|
14
|
+
[500, {}, ["Please specify the consumer name and the provider name by setting the X-Pact-Consumer and X-Pact-Provider headers"]]
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
require 'pact/mock_service/control_server/app'
|
|
2
|
+
require 'pact/mock_service/server/webrick_request_monkeypatch'
|
|
3
|
+
|
|
4
|
+
module Pact
|
|
5
|
+
module MockService
|
|
6
|
+
module ControlServer
|
|
7
|
+
class Run
|
|
8
|
+
|
|
9
|
+
def self.call options
|
|
10
|
+
new(options).call
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def initialize options
|
|
14
|
+
@options = options
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def call
|
|
18
|
+
trap(:INT) { shutdown }
|
|
19
|
+
trap(:TERM) { shutdown }
|
|
20
|
+
|
|
21
|
+
# https://github.com/rack/rack/blob/ae78184e5c1fcf4ac133171ed4b47b0548cd9b44/lib/rack/handler/webrick.rb#L32
|
|
22
|
+
# Rack adapter for webrick uses class variable for the server which contains the port,
|
|
23
|
+
# so if we use it more than once in the same process, we lose the reference to the first
|
|
24
|
+
# server, and can't shut it down. So, keep a manual reference to the Webrick server, and
|
|
25
|
+
# shut it down directly rather than use Rack::Handler::WEBrick.shutdown
|
|
26
|
+
# Ruby!
|
|
27
|
+
Rack::Handler::WEBrick.run(control_server, **webbrick_opts) do | server |
|
|
28
|
+
@webrick_server = server
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
attr_reader :options
|
|
35
|
+
|
|
36
|
+
def control_server
|
|
37
|
+
@control_server ||= Pact::MockService::ControlServer::App.new control_server_options
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def shutdown
|
|
41
|
+
unless @shutting_down
|
|
42
|
+
@shutting_down = true
|
|
43
|
+
begin
|
|
44
|
+
@control_server.shutdown
|
|
45
|
+
ensure
|
|
46
|
+
@webrick_server.shutdown
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def control_server_options
|
|
52
|
+
{
|
|
53
|
+
log_dir: options[:log_dir] || "log",
|
|
54
|
+
pact_dir: options[:pact_dir] || ".",
|
|
55
|
+
unique_pact_file_names: options[:unique_pact_file_names],
|
|
56
|
+
cors_enabled: options[:cors] || false,
|
|
57
|
+
ssl: options[:ssl],
|
|
58
|
+
host: options[:host],
|
|
59
|
+
pact_specification_version: options[:pact_specification_version]
|
|
60
|
+
}
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def webbrick_opts
|
|
64
|
+
{
|
|
65
|
+
:Port => options[:port],
|
|
66
|
+
:Host => options[:host],
|
|
67
|
+
:AccessLog => []
|
|
68
|
+
}
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
require 'pact/shared/active_support_support'
|
|
2
|
+
require 'pact/mock_service/request_decorator'
|
|
3
|
+
require 'pact/mock_service/response_decorator'
|
|
4
|
+
|
|
5
|
+
# Represents the Interaction in the form required by the MockService
|
|
6
|
+
# The json generated will be posted to the MockService to register the expectation
|
|
7
|
+
|
|
8
|
+
module Pact
|
|
9
|
+
module MockService
|
|
10
|
+
class InteractionDecorator
|
|
11
|
+
|
|
12
|
+
include ActiveSupportSupport
|
|
13
|
+
|
|
14
|
+
def initialize interaction
|
|
15
|
+
@interaction = interaction
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def as_json options = {}
|
|
19
|
+
fix_all_the_things to_hash
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def to_json(options = {})
|
|
23
|
+
as_json.to_json(options)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def to_hash
|
|
27
|
+
hash = { :description => interaction.description }
|
|
28
|
+
hash[:providerState] = interaction.provider_state if interaction.provider_state
|
|
29
|
+
hash[:request] = decorate_request.as_json
|
|
30
|
+
hash[:response] = decorate_response.as_json
|
|
31
|
+
hash[:metadata] = interaction.metadata
|
|
32
|
+
hash
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
attr_reader :interaction
|
|
38
|
+
|
|
39
|
+
def decorate_request
|
|
40
|
+
RequestDecorator.new(interaction.request)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def decorate_response
|
|
44
|
+
ResponseDecorator.new(interaction.response)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
require 'pact/mock_service/interactions/candidate_interactions'
|
|
2
|
+
|
|
3
|
+
module Pact
|
|
4
|
+
module MockService
|
|
5
|
+
module Interactions
|
|
6
|
+
class ActualInteractions
|
|
7
|
+
|
|
8
|
+
attr_reader :matched_interactions, :interaction_mismatches, :unexpected_requests
|
|
9
|
+
|
|
10
|
+
def initialize
|
|
11
|
+
clear
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# For testing, sigh
|
|
15
|
+
def clear
|
|
16
|
+
@matched_interactions = []
|
|
17
|
+
@interaction_mismatches = []
|
|
18
|
+
@unexpected_requests = []
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def register_matched interaction
|
|
22
|
+
@matched_interactions << interaction
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def register_unexpected_request request
|
|
26
|
+
@unexpected_requests << request
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def register_interaction_mismatch interaction_mismatch
|
|
30
|
+
@interaction_mismatches << interaction_mismatch
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
module Pact
|
|
2
|
+
module MockService
|
|
3
|
+
module Interactions
|
|
4
|
+
class CandidateInteractions < Array
|
|
5
|
+
|
|
6
|
+
def matching_interactions actual_request
|
|
7
|
+
select do | candidate_interaction |
|
|
8
|
+
candidate_interaction.request.matches? actual_request
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
require 'pact/mock_service/interactions/candidate_interactions'
|
|
2
|
+
|
|
3
|
+
module Pact
|
|
4
|
+
module MockService
|
|
5
|
+
module Interactions
|
|
6
|
+
class ExpectedInteractions < Array
|
|
7
|
+
|
|
8
|
+
def find_candidate_interactions actual_request
|
|
9
|
+
Pact::MockService::Interactions::CandidateInteractions.new(
|
|
10
|
+
select do | interaction |
|
|
11
|
+
interaction.request.matches_route? actual_request
|
|
12
|
+
end
|
|
13
|
+
)
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
require 'pact/shared/json_differ'
|
|
2
|
+
require 'pact/mock_service/interaction_decorator'
|
|
3
|
+
|
|
4
|
+
module Pact
|
|
5
|
+
module MockService
|
|
6
|
+
module Interactions
|
|
7
|
+
class InteractionDiffMessage
|
|
8
|
+
|
|
9
|
+
def initialize previous_interaction, new_interaction
|
|
10
|
+
@previous_interaction = previous_interaction
|
|
11
|
+
@new_interaction = new_interaction
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def to_s
|
|
15
|
+
"An interaction with same description (#{new_interaction.description.inspect}) and provider state (#{new_interaction.provider_state.inspect}) but a different #{differences} has already been used. Please use a different description or provider state, or remove any random data in the interaction."
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
attr_reader :previous_interaction, :new_interaction
|
|
21
|
+
|
|
22
|
+
def differences
|
|
23
|
+
diff = Pact::JsonDiffer.call(previous_interaction_hash, new_interaction_hash, allow_unexpected_keys: false)
|
|
24
|
+
diff.keys.collect do | parent_key |
|
|
25
|
+
diff[parent_key].keys.collect do | child_key |
|
|
26
|
+
"#{parent_key} #{child_key}"
|
|
27
|
+
end
|
|
28
|
+
end.flatten.join(", ").reverse.sub(",", "dna ").reverse
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def previous_interaction_hash
|
|
32
|
+
raw_hash previous_interaction
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def new_interaction_hash
|
|
36
|
+
raw_hash new_interaction
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def raw_hash interaction
|
|
40
|
+
JSON.parse(Pact::MockService::InteractionDecorator.new(interaction).to_json)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
module Pact
|
|
2
|
+
module MockService
|
|
3
|
+
module Interactions
|
|
4
|
+
|
|
5
|
+
# expected interactions where the methods and paths match the actual request.
|
|
6
|
+
# This is used to display a helpful message to the user when a request
|
|
7
|
+
# comes in that doesn't match any of the expected interactions.
|
|
8
|
+
class InteractionMismatch
|
|
9
|
+
|
|
10
|
+
attr_accessor :candidate_interactions, :actual_request
|
|
11
|
+
|
|
12
|
+
# Assumes the method and path matches...
|
|
13
|
+
|
|
14
|
+
def initialize candidate_interactions, actual_request
|
|
15
|
+
@candidate_interactions = candidate_interactions
|
|
16
|
+
@actual_request = actual_request
|
|
17
|
+
@candiate_diffs = candidate_interactions.collect{ | candidate_interaction| CandidateDiff.new(candidate_interaction, actual_request)}
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def to_hash
|
|
21
|
+
candiate_diffs.collect(&:to_hash)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def to_s
|
|
25
|
+
candiate_diffs.collect(&:to_s).join("\n")
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def short_summary
|
|
29
|
+
mismatched_attributes = candiate_diffs.collect(&:mismatched_attributes).flatten.uniq.join(", ").reverse.sub(",", "dna ").reverse #OMG what a hack!
|
|
30
|
+
actual_request.method_and_path + " (request #{mismatched_attributes} did not match)"
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
attr_accessor :candiate_diffs
|
|
36
|
+
|
|
37
|
+
class CandidateDiff
|
|
38
|
+
|
|
39
|
+
attr_accessor :candidate_interaction, :actual_request
|
|
40
|
+
|
|
41
|
+
def initialize candidate_interaction, actual_request
|
|
42
|
+
@candidate_interaction = candidate_interaction
|
|
43
|
+
@actual_request = actual_request
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def mismatched_attributes
|
|
47
|
+
diff.keys
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def to_hash
|
|
51
|
+
summary = {:description => candidate_interaction.description}
|
|
52
|
+
summary[:provider_state] = candidate_interaction.provider_state if candidate_interaction.provider_state
|
|
53
|
+
summary.merge(diff)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def to_s
|
|
57
|
+
[
|
|
58
|
+
"Diff with interaction: #{candidate_interaction.description_with_provider_state_quoted}",
|
|
59
|
+
diff_formatter.call(diff, {colour: false})
|
|
60
|
+
].join("\n")
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def diff_formatter
|
|
64
|
+
Pact.configuration.diff_formatter_for_content_type(candidate_interaction.request.content_type)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def diff
|
|
68
|
+
@diff ||= candidate_interaction.request.difference(actual_request)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
require 'pact/mock_service/errors'
|
|
2
|
+
require 'pact/mock_service/interactions/interaction_diff_message'
|
|
3
|
+
|
|
4
|
+
#
|
|
5
|
+
# When running in pactfile_write_mode :overwrite, all interactions are cleared from the
|
|
6
|
+
# pact file, and all new interactions should be distinct (unique description and provider state).
|
|
7
|
+
# When running in pactfile_write_mode :update, an interaction with the same description
|
|
8
|
+
# and provider state as an existing one will just overwrite that one interaction.
|
|
9
|
+
# When running in pactfile_write_mode :merge, an interaction with the same description and provider
|
|
10
|
+
# state must be identical to the existing one, otherwise an exception will be raised.
|
|
11
|
+
|
|
12
|
+
module Pact
|
|
13
|
+
module MockService
|
|
14
|
+
module Interactions
|
|
15
|
+
|
|
16
|
+
def self.filter existing_interactions, pactfile_write_mode
|
|
17
|
+
if pactfile_write_mode == :update
|
|
18
|
+
UpdatableInteractionsFilter.new(existing_interactions)
|
|
19
|
+
elsif pactfile_write_mode == :merge
|
|
20
|
+
MergingInteractionsFilter.new(existing_interactions)
|
|
21
|
+
else
|
|
22
|
+
existing_interactions
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
#TODO: think of a better word than filter
|
|
27
|
+
class InteractionsFilter
|
|
28
|
+
def initialize interactions = []
|
|
29
|
+
@interactions = interactions
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def index_of interaction
|
|
33
|
+
@interactions.find_index{ |i| i.matches_criteria?(description: interaction.description, provider_state: interaction.provider_state)}
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
class UpdatableInteractionsFilter < InteractionsFilter
|
|
38
|
+
def << interaction
|
|
39
|
+
if (ndx = index_of(interaction))
|
|
40
|
+
@interactions[ndx] = interaction
|
|
41
|
+
else
|
|
42
|
+
@interactions << interaction
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
class MergingInteractionsFilter < InteractionsFilter
|
|
48
|
+
def << interaction
|
|
49
|
+
if (ndx = index_of(interaction))
|
|
50
|
+
if same_same_but_different?(@interactions[ndx], interaction)
|
|
51
|
+
message = Interactions::InteractionDiffMessage.new(@interactions[ndx], interaction).to_s
|
|
52
|
+
raise SameSameButDifferentError, message
|
|
53
|
+
end
|
|
54
|
+
@interactions[ndx] = interaction
|
|
55
|
+
else
|
|
56
|
+
@interactions << interaction
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def same_same_but_different?(existing_interaction, new_interaction)
|
|
61
|
+
existing_interaction != new_interaction
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|