pact-mock_service 0.0.1
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 +15 -0
- data/.gitignore +29 -0
- data/.rspec +2 -0
- data/.travis.yml +8 -0
- data/CHANGELOG.md +4 -0
- data/Gemfile +8 -0
- data/Gemfile.lock +100 -0
- data/LICENSE.txt +22 -0
- data/README.md +314 -0
- data/Rakefile +6 -0
- data/bin/pact-mock-service +3 -0
- data/lib/pact/consumer/app_manager.rb +159 -0
- data/lib/pact/consumer/interactions_filter.rb +48 -0
- data/lib/pact/consumer/mock_service/app.rb +84 -0
- data/lib/pact/consumer/mock_service/interaction_delete.rb +33 -0
- data/lib/pact/consumer/mock_service/interaction_list.rb +76 -0
- data/lib/pact/consumer/mock_service/interaction_mismatch.rb +73 -0
- data/lib/pact/consumer/mock_service/interaction_post.rb +31 -0
- data/lib/pact/consumer/mock_service/interaction_replay.rb +139 -0
- data/lib/pact/consumer/mock_service/log_get.rb +28 -0
- data/lib/pact/consumer/mock_service/missing_interactions_get.rb +30 -0
- data/lib/pact/consumer/mock_service/mock_service_administration_endpoint.rb +31 -0
- data/lib/pact/consumer/mock_service/pact_post.rb +33 -0
- data/lib/pact/consumer/mock_service/rack_request_helper.rb +51 -0
- data/lib/pact/consumer/mock_service/verification_get.rb +68 -0
- data/lib/pact/consumer/mock_service.rb +2 -0
- data/lib/pact/consumer/mock_service_client.rb +65 -0
- data/lib/pact/consumer/mock_service_interaction_expectation.rb +37 -0
- data/lib/pact/consumer/request.rb +27 -0
- data/lib/pact/consumer/server.rb +90 -0
- data/lib/pact/consumer_contract/consumer_contract_writer.rb +84 -0
- data/lib/pact/mock_service/cli.rb +49 -0
- data/lib/pact/mock_service/version.rb +5 -0
- data/lib/pact/mock_service.rb +1 -0
- data/pact-mock-service.gemspec +41 -0
- data/spec/lib/pact/consumer/app_manager_spec.rb +42 -0
- data/spec/lib/pact/consumer/mock_service/app_spec.rb +52 -0
- data/spec/lib/pact/consumer/mock_service/interaction_list_spec.rb +78 -0
- data/spec/lib/pact/consumer/mock_service/interaction_mismatch_spec.rb +70 -0
- data/spec/lib/pact/consumer/mock_service/interaction_replay_spec.rb +12 -0
- data/spec/lib/pact/consumer/mock_service/rack_request_helper_spec.rb +88 -0
- data/spec/lib/pact/consumer/mock_service/verification_get_spec.rb +142 -0
- data/spec/lib/pact/consumer/mock_service_client_spec.rb +88 -0
- data/spec/lib/pact/consumer/mock_service_interaction_expectation_spec.rb +54 -0
- data/spec/lib/pact/consumer/service_consumer_spec.rb +11 -0
- data/spec/lib/pact/consumer_contract/consumer_contract_writer_spec.rb +111 -0
- data/spec/spec_helper.rb +16 -0
- data/spec/support/a_consumer-a_producer.json +32 -0
- data/spec/support/a_consumer-a_provider.json +32 -0
- data/spec/support/active_support_if_configured.rb +6 -0
- data/spec/support/app_for_config_ru.rb +4 -0
- data/spec/support/case-insensitive-response-header-matching.json +21 -0
- data/spec/support/case-insensitive-response-header-matching.rb +15 -0
- data/spec/support/consumer_contract_template.json +24 -0
- data/spec/support/dsl_spec_support.rb +7 -0
- data/spec/support/factories.rb +82 -0
- data/spec/support/generated_index.md +4 -0
- data/spec/support/generated_markdown.md +55 -0
- data/spec/support/interaction_view_model.json +63 -0
- data/spec/support/interaction_view_model_with_terms.json +50 -0
- data/spec/support/markdown_pact.json +48 -0
- data/spec/support/missing_provider_states_output.txt +25 -0
- data/spec/support/options.json +21 -0
- data/spec/support/options_app.rb +15 -0
- data/spec/support/pact_helper.rb +57 -0
- data/spec/support/shared_examples_for_request.rb +94 -0
- data/spec/support/spec_support.rb +20 -0
- data/spec/support/stubbing.json +22 -0
- data/spec/support/stubbing_using_allow.rb +29 -0
- data/spec/support/term.json +48 -0
- data/spec/support/test_app_fail.json +61 -0
- data/spec/support/test_app_pass.json +38 -0
- data/spec/support/test_app_with_right_content_type_differ.json +23 -0
- data/tasks/spec.rake +6 -0
- metadata +388 -0
@@ -0,0 +1,48 @@
|
|
1
|
+
#
|
2
|
+
# When running in pactfile_write_mode :overwrite, all interactions are cleared from the
|
3
|
+
# pact file, and all new interactions should be distinct (unique description and provider state).
|
4
|
+
# When running in pactfile_write_mode :update, an interaction with the same description
|
5
|
+
# and provider state as an existing one will just overwrite that one interaction.
|
6
|
+
#
|
7
|
+
|
8
|
+
module Pact
|
9
|
+
module Consumer
|
10
|
+
|
11
|
+
#TODO: think of a better word than filter
|
12
|
+
class InteractionsFilter
|
13
|
+
def initialize interactions = []
|
14
|
+
@interactions = interactions
|
15
|
+
end
|
16
|
+
|
17
|
+
def index_of interaction
|
18
|
+
@interactions.find_index{ |i| i.matches_criteria?(description: interaction.description, provider_state: interaction.provider_state)}
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
class UpdatableInteractionsFilter < InteractionsFilter
|
23
|
+
|
24
|
+
def << interaction
|
25
|
+
if (ndx = index_of(interaction))
|
26
|
+
@interactions[ndx] = interaction
|
27
|
+
else
|
28
|
+
@interactions << interaction
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
33
|
+
|
34
|
+
class DistinctInteractionsFilter < InteractionsFilter
|
35
|
+
|
36
|
+
def << interaction
|
37
|
+
if (ndx = index_of(interaction))
|
38
|
+
if @interactions[ndx] != interaction
|
39
|
+
raise "Interaction with same description (#{interaction.description}) and provider state (#{interaction.provider_state}) already exists"
|
40
|
+
end
|
41
|
+
else
|
42
|
+
@interactions << interaction
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
require 'rack'
|
2
|
+
require 'uri'
|
3
|
+
require 'json'
|
4
|
+
require 'logger'
|
5
|
+
require 'awesome_print'
|
6
|
+
require 'awesome_print/core_ext/logger' #For some reason we get an error indicating that the method 'ap' is private unless we load this specifically
|
7
|
+
require 'pact/consumer/request'
|
8
|
+
require 'pact/consumer/mock_service/interaction_list'
|
9
|
+
require 'pact/consumer/mock_service/interaction_delete'
|
10
|
+
require 'pact/consumer/mock_service/interaction_post'
|
11
|
+
require 'pact/consumer/mock_service/interaction_replay'
|
12
|
+
require 'pact/consumer/mock_service/missing_interactions_get'
|
13
|
+
require 'pact/consumer/mock_service/verification_get'
|
14
|
+
require 'pact/consumer/mock_service/log_get'
|
15
|
+
require 'pact/consumer/mock_service/pact_post'
|
16
|
+
require 'pact/support'
|
17
|
+
|
18
|
+
AwesomePrint.defaults = {
|
19
|
+
indent: -2,
|
20
|
+
plain: true,
|
21
|
+
index: false
|
22
|
+
}
|
23
|
+
|
24
|
+
module Pact
|
25
|
+
module Consumer
|
26
|
+
|
27
|
+
class MockService
|
28
|
+
|
29
|
+
def initialize options = {}
|
30
|
+
log_description = configure_logger options
|
31
|
+
interaction_list = InteractionList.new
|
32
|
+
|
33
|
+
@name = options.fetch(:name, "MockService")
|
34
|
+
interactions = []
|
35
|
+
@handlers = [
|
36
|
+
MissingInteractionsGet.new(@name, @logger, interaction_list),
|
37
|
+
VerificationGet.new(@name, @logger, interaction_list, log_description),
|
38
|
+
InteractionPost.new(@name, @logger, interaction_list),
|
39
|
+
InteractionDelete.new(@name, @logger, interaction_list),
|
40
|
+
LogGet.new(@name, @logger),
|
41
|
+
PactPost.new(@name, @logger, interactions),
|
42
|
+
InteractionReplay.new(@name, @logger, interaction_list, interactions)
|
43
|
+
]
|
44
|
+
end
|
45
|
+
|
46
|
+
def configure_logger options
|
47
|
+
options = {log_file: $stdout}.merge options
|
48
|
+
log_stream = options[:log_file]
|
49
|
+
@logger = Logger.new log_stream
|
50
|
+
@logger.level = Pact.configuration.logger.level
|
51
|
+
|
52
|
+
if log_stream.is_a? File
|
53
|
+
File.absolute_path(log_stream).gsub(Dir.pwd + "/", '')
|
54
|
+
else
|
55
|
+
"standard out/err"
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def to_s
|
60
|
+
"#{@name} #{super.to_s}"
|
61
|
+
end
|
62
|
+
|
63
|
+
def call env
|
64
|
+
response = []
|
65
|
+
begin
|
66
|
+
relevant_handler = @handlers.detect { |handler| handler.match? env }
|
67
|
+
response = relevant_handler.respond env
|
68
|
+
rescue StandardError => e
|
69
|
+
@logger.error 'Error ocurred in mock service:'
|
70
|
+
@logger.ap e, :error
|
71
|
+
@logger.ap e.backtrace
|
72
|
+
response = [500, {'Content-Type' => 'application/json'}, [{message: e.message, backtrace: e.backtrace}.to_json]]
|
73
|
+
rescue Exception => e
|
74
|
+
@logger.error 'Exception ocurred in mock service:'
|
75
|
+
@logger.ap e, :error
|
76
|
+
@logger.ap e.backtrace
|
77
|
+
raise e
|
78
|
+
end
|
79
|
+
response
|
80
|
+
end
|
81
|
+
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require 'pact/consumer/mock_service/rack_request_helper'
|
2
|
+
require 'pact/consumer/mock_service/mock_service_administration_endpoint'
|
3
|
+
|
4
|
+
module Pact
|
5
|
+
module Consumer
|
6
|
+
|
7
|
+
class InteractionDelete < MockServiceAdministrationEndpoint
|
8
|
+
|
9
|
+
include RackRequestHelper
|
10
|
+
|
11
|
+
attr_accessor :interaction_list
|
12
|
+
|
13
|
+
def initialize name, logger, interaction_list
|
14
|
+
super name, logger
|
15
|
+
@interaction_list = interaction_list
|
16
|
+
end
|
17
|
+
|
18
|
+
def request_path
|
19
|
+
'/interactions'
|
20
|
+
end
|
21
|
+
|
22
|
+
def request_method
|
23
|
+
'DELETE'
|
24
|
+
end
|
25
|
+
|
26
|
+
def respond env
|
27
|
+
interaction_list.clear
|
28
|
+
logger.info "Cleared interactions before example \"#{params_hash(env)['example_description']}\""
|
29
|
+
[200, {}, ['Deleted interactions']]
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
module Pact
|
2
|
+
module Consumer
|
3
|
+
class InteractionList
|
4
|
+
|
5
|
+
attr_reader :interactions
|
6
|
+
attr_reader :unexpected_requests
|
7
|
+
attr_reader :interaction_mismatches
|
8
|
+
|
9
|
+
def initialize
|
10
|
+
clear
|
11
|
+
end
|
12
|
+
|
13
|
+
# For testing, sigh
|
14
|
+
def clear
|
15
|
+
@interactions = []
|
16
|
+
@matched_interactions = []
|
17
|
+
@interaction_mismatches = []
|
18
|
+
@unexpected_requests = []
|
19
|
+
end
|
20
|
+
|
21
|
+
def add interactions
|
22
|
+
@interactions << interactions
|
23
|
+
end
|
24
|
+
|
25
|
+
def register_matched interaction
|
26
|
+
@matched_interactions << interaction
|
27
|
+
end
|
28
|
+
|
29
|
+
def register_unexpected_request request
|
30
|
+
@unexpected_requests << request
|
31
|
+
end
|
32
|
+
|
33
|
+
def register_interaction_mismatch interaction_mismatch
|
34
|
+
@interaction_mismatches << interaction_mismatch
|
35
|
+
end
|
36
|
+
|
37
|
+
def all_matched?
|
38
|
+
interaction_diffs.empty?
|
39
|
+
end
|
40
|
+
|
41
|
+
def missing_interactions
|
42
|
+
@interactions - @matched_interactions - @interaction_mismatches.collect(&:candidate_interactions).flatten
|
43
|
+
end
|
44
|
+
|
45
|
+
def missing_interactions_summaries
|
46
|
+
missing_interactions.collect(&:request).collect(&:method_and_path)
|
47
|
+
end
|
48
|
+
|
49
|
+
def interaction_mismatches_summaries
|
50
|
+
interaction_mismatches.collect(&:short_summary)
|
51
|
+
end
|
52
|
+
|
53
|
+
def unexpected_requests_summaries
|
54
|
+
unexpected_requests.collect(&:method_and_path)
|
55
|
+
end
|
56
|
+
|
57
|
+
def interaction_diffs
|
58
|
+
{
|
59
|
+
:missing_interactions => missing_interactions_summaries,
|
60
|
+
:interaction_mismatches => interaction_mismatches_summaries,
|
61
|
+
:unexpected_requests => unexpected_requests_summaries
|
62
|
+
}.inject({}) do | hash, pair |
|
63
|
+
hash[pair.first] = pair.last if pair.last.any?
|
64
|
+
hash
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def find_candidate_interactions actual_request
|
69
|
+
interactions.select do | interaction |
|
70
|
+
interaction.request.matches_route? actual_request
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
module Pact
|
2
|
+
module Consumer
|
3
|
+
|
4
|
+
# Presents the differences between an actual request, and a list of
|
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
|
@@ -0,0 +1,31 @@
|
|
1
|
+
require 'pact/consumer/mock_service/mock_service_administration_endpoint'
|
2
|
+
|
3
|
+
module Pact
|
4
|
+
module Consumer
|
5
|
+
class InteractionPost < MockServiceAdministrationEndpoint
|
6
|
+
|
7
|
+
attr_accessor :interaction_list
|
8
|
+
|
9
|
+
def initialize name, logger, interaction_list
|
10
|
+
super name, logger
|
11
|
+
@interaction_list = interaction_list
|
12
|
+
end
|
13
|
+
|
14
|
+
def request_path
|
15
|
+
'/interactions'
|
16
|
+
end
|
17
|
+
|
18
|
+
def request_method
|
19
|
+
'POST'
|
20
|
+
end
|
21
|
+
|
22
|
+
def respond env
|
23
|
+
interaction = Interaction.from_hash(JSON.load(env['rack.input'].string))
|
24
|
+
interaction_list.add interaction
|
25
|
+
logger.info "Registered expected interaction #{interaction.request.method_and_path}"
|
26
|
+
logger.debug JSON.pretty_generate JSON.parse(interaction.to_json)
|
27
|
+
[200, {}, ['Added interaction']]
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,139 @@
|
|
1
|
+
require 'pact/matchers'
|
2
|
+
require 'pact/consumer/mock_service/rack_request_helper'
|
3
|
+
require 'pact/consumer/mock_service/interaction_mismatch'
|
4
|
+
require 'pact/consumer_contract'
|
5
|
+
require 'pact/consumer/interactions_filter'
|
6
|
+
|
7
|
+
module Pact
|
8
|
+
module Consumer
|
9
|
+
|
10
|
+
class InteractionReplay
|
11
|
+
include Pact::Matchers
|
12
|
+
include RackRequestHelper
|
13
|
+
|
14
|
+
attr_accessor :name, :logger, :interaction_list, :interactions
|
15
|
+
|
16
|
+
def initialize name, logger, interaction_list, interactions
|
17
|
+
@name = name
|
18
|
+
@logger = logger
|
19
|
+
@interaction_list = interaction_list
|
20
|
+
@interactions = DistinctInteractionsFilter.new(interactions)
|
21
|
+
end
|
22
|
+
|
23
|
+
def match? env
|
24
|
+
true # default handler
|
25
|
+
end
|
26
|
+
|
27
|
+
def respond env
|
28
|
+
find_response request_as_hash_from(env)
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def add_verified_interaction interaction
|
34
|
+
interactions << interaction
|
35
|
+
end
|
36
|
+
|
37
|
+
def find_response request_hash
|
38
|
+
actual_request = Request::Actual.from_hash(request_hash)
|
39
|
+
logger.info "Received request #{actual_request.method_and_path}"
|
40
|
+
logger.debug pretty_generate actual_request
|
41
|
+
candidate_interactions = interaction_list.find_candidate_interactions actual_request
|
42
|
+
matching_interactions = find_matching_interactions actual_request, from: candidate_interactions
|
43
|
+
|
44
|
+
case matching_interactions.size
|
45
|
+
when 0 then handle_unrecognised_request actual_request, candidate_interactions
|
46
|
+
when 1 then handle_matched_interaction matching_interactions.first
|
47
|
+
else
|
48
|
+
handle_more_than_one_matching_interaction actual_request, matching_interactions
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def find_matching_interactions actual_request, opts
|
53
|
+
candidate_interactions = opts.fetch(:from)
|
54
|
+
candidate_interactions.select do | candidate_interaction |
|
55
|
+
candidate_interaction.request.matches? actual_request
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def handle_matched_interaction interaction
|
60
|
+
interaction_list.register_matched interaction
|
61
|
+
add_verified_interaction interaction
|
62
|
+
response = response_from(interaction.response)
|
63
|
+
logger.info "Found matching response for #{interaction.request.method_and_path}"
|
64
|
+
logger.debug pretty_generate(interaction.response)
|
65
|
+
response
|
66
|
+
end
|
67
|
+
|
68
|
+
def multiple_interactions_found_response actual_request, matching_interactions
|
69
|
+
response = {
|
70
|
+
message: "Multiple interaction found for #{actual_request.method_and_path}",
|
71
|
+
matching_interactions: matching_interactions.collect{ | interaction | request_summary_for(interaction) }
|
72
|
+
}
|
73
|
+
[500, {'Content-Type' => 'application/json'}, [response.to_json]]
|
74
|
+
end
|
75
|
+
|
76
|
+
def handle_more_than_one_matching_interaction actual_request, matching_interactions
|
77
|
+
logger.error "Multiple interactions found for #{actual_request.method_and_path}:"
|
78
|
+
matching_interactions.each do | interaction |
|
79
|
+
logger.debug pretty_generate(interaction)
|
80
|
+
end
|
81
|
+
multiple_interactions_found_response actual_request, matching_interactions
|
82
|
+
end
|
83
|
+
|
84
|
+
def interaction_mismatch actual_request, candidate_interactions
|
85
|
+
InteractionMismatch.new(candidate_interactions, actual_request)
|
86
|
+
end
|
87
|
+
|
88
|
+
def request_summary_for interaction
|
89
|
+
summary = {:description => interaction.description}
|
90
|
+
summary[:provider_state] if interaction.provider_state
|
91
|
+
summary[:request] = interaction.request
|
92
|
+
summary
|
93
|
+
end
|
94
|
+
|
95
|
+
def unrecognised_request_response interaction_mismatch
|
96
|
+
response = {
|
97
|
+
message: "No interaction found for #{interaction_mismatch.actual_request.method_and_path}",
|
98
|
+
interaction_diffs: interaction_mismatch.to_hash
|
99
|
+
}
|
100
|
+
[500, {'Content-Type' => 'application/json'}, [response.to_json]]
|
101
|
+
end
|
102
|
+
|
103
|
+
def log_unrecognised_request_and_interaction_diff interaction_mismatch
|
104
|
+
logger.error "No matching interaction found on #{name} for #{interaction_mismatch.actual_request.method_and_path}"
|
105
|
+
logger.error 'Interaction diffs for that route:'
|
106
|
+
logger.error(interaction_mismatch.to_s)
|
107
|
+
end
|
108
|
+
|
109
|
+
def handle_unrecognised_request actual_request, candidate_interactions
|
110
|
+
interaction_mismatch = interaction_mismatch(actual_request, candidate_interactions)
|
111
|
+
if candidate_interactions.any?
|
112
|
+
interaction_list.register_interaction_mismatch interaction_mismatch
|
113
|
+
else
|
114
|
+
interaction_list.register_unexpected_request actual_request
|
115
|
+
end
|
116
|
+
log_unrecognised_request_and_interaction_diff interaction_mismatch
|
117
|
+
unrecognised_request_response interaction_mismatch
|
118
|
+
end
|
119
|
+
|
120
|
+
def response_from response
|
121
|
+
[response.status, (response.headers || {}).to_hash, [render_body(Pact::Reification.from_term(response.body))]]
|
122
|
+
end
|
123
|
+
|
124
|
+
def render_body body
|
125
|
+
return '' unless body
|
126
|
+
body.kind_of?(String) ? body.force_encoding('utf-8') : body.to_json
|
127
|
+
end
|
128
|
+
|
129
|
+
def logger_info_ap msg
|
130
|
+
logger.info msg
|
131
|
+
end
|
132
|
+
|
133
|
+
#Doesn't seem to reliably pretty generate unless we go to JSON and back again :(
|
134
|
+
def pretty_generate object
|
135
|
+
JSON.pretty_generate(JSON.parse(object.to_json))
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'pact/consumer/mock_service/mock_service_administration_endpoint'
|
2
|
+
|
3
|
+
module Pact
|
4
|
+
module Consumer
|
5
|
+
class LogGet < MockServiceAdministrationEndpoint
|
6
|
+
|
7
|
+
include RackRequestHelper
|
8
|
+
|
9
|
+
def request_path
|
10
|
+
'/log'
|
11
|
+
end
|
12
|
+
|
13
|
+
def request_method
|
14
|
+
'GET'
|
15
|
+
end
|
16
|
+
|
17
|
+
|
18
|
+
def respond env
|
19
|
+
logger.info "Debug message from client - #{message(env)}"
|
20
|
+
[200, {}, []]
|
21
|
+
end
|
22
|
+
|
23
|
+
def message env
|
24
|
+
params_hash(env)['msg']
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'pact/consumer/mock_service/mock_service_administration_endpoint'
|
2
|
+
|
3
|
+
module Pact
|
4
|
+
module Consumer
|
5
|
+
|
6
|
+
class MissingInteractionsGet < MockServiceAdministrationEndpoint
|
7
|
+
include RackRequestHelper
|
8
|
+
|
9
|
+
def initialize name, logger, interaction_list
|
10
|
+
super name, logger
|
11
|
+
@interaction_list = interaction_list
|
12
|
+
end
|
13
|
+
|
14
|
+
def request_path
|
15
|
+
'/interactions/missing'
|
16
|
+
end
|
17
|
+
|
18
|
+
def request_method
|
19
|
+
'GET'
|
20
|
+
end
|
21
|
+
|
22
|
+
def respond env
|
23
|
+
number_of_missing_interactions = @interaction_list.missing_interactions.size
|
24
|
+
logger.info "Number of missing interactions for mock \"#{name}\" = #{number_of_missing_interactions}"
|
25
|
+
[200, {}, [{size: number_of_missing_interactions}.to_json]]
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
require 'pact/consumer/mock_service/rack_request_helper'
|
2
|
+
module Pact
|
3
|
+
module Consumer
|
4
|
+
class MockServiceAdministrationEndpoint
|
5
|
+
|
6
|
+
attr_accessor :logger, :name
|
7
|
+
|
8
|
+
def initialize name, logger
|
9
|
+
@name = name
|
10
|
+
@logger = logger
|
11
|
+
end
|
12
|
+
|
13
|
+
include RackRequestHelper
|
14
|
+
|
15
|
+
def match? env
|
16
|
+
headers_from(env)['X-Pact-Mock-Service'] &&
|
17
|
+
env['REQUEST_PATH'] == request_path &&
|
18
|
+
env['REQUEST_METHOD'] == request_method
|
19
|
+
end
|
20
|
+
|
21
|
+
def request_path
|
22
|
+
raise NotImplementedError
|
23
|
+
end
|
24
|
+
|
25
|
+
def request_method
|
26
|
+
raise NotImplementedError
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require 'pact/consumer/mock_service/mock_service_administration_endpoint'
|
2
|
+
require 'pact/consumer_contract/consumer_contract_writer'
|
3
|
+
|
4
|
+
module Pact
|
5
|
+
module Consumer
|
6
|
+
class PactPost < MockServiceAdministrationEndpoint
|
7
|
+
|
8
|
+
attr_accessor :consumer_contract, :interactions
|
9
|
+
|
10
|
+
def initialize name, logger, interactions
|
11
|
+
super name, logger
|
12
|
+
@interactions = interactions
|
13
|
+
end
|
14
|
+
|
15
|
+
def request_path
|
16
|
+
'/pact'
|
17
|
+
end
|
18
|
+
|
19
|
+
def request_method
|
20
|
+
'POST'
|
21
|
+
end
|
22
|
+
|
23
|
+
def respond env
|
24
|
+
consumer_contract_details = JSON.parse(env['rack.input'].string, symbolize_names: true)
|
25
|
+
logger.info "Writing pact with details #{consumer_contract_details}"
|
26
|
+
consumer_contract_writer = ConsumerContractWriter.new(consumer_contract_details.merge(interactions: interactions), logger)
|
27
|
+
json = consumer_contract_writer.write
|
28
|
+
|
29
|
+
[200, {'Content-Type' =>'application/json'}, [json]]
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
module Pact
|
2
|
+
module Consumer
|
3
|
+
|
4
|
+
module RackRequestHelper
|
5
|
+
REQUEST_KEYS = {
|
6
|
+
'REQUEST_METHOD' => :method,
|
7
|
+
'PATH_INFO' => :path,
|
8
|
+
'QUERY_STRING' => :query,
|
9
|
+
'rack.input' => :body
|
10
|
+
}
|
11
|
+
|
12
|
+
def params_hash env
|
13
|
+
env["QUERY_STRING"].split("&").collect{| param| param.split("=")}.inject({}){|params, param| params[param.first] = URI.decode(param.last); params }
|
14
|
+
end
|
15
|
+
|
16
|
+
def request_as_hash_from env
|
17
|
+
request = env.inject({}) do |memo, (k, v)|
|
18
|
+
request_key = REQUEST_KEYS[k]
|
19
|
+
memo[request_key] = v if request_key
|
20
|
+
memo
|
21
|
+
end
|
22
|
+
|
23
|
+
request[:headers] = headers_from env
|
24
|
+
body_string = request[:body].read
|
25
|
+
|
26
|
+
if body_string.empty?
|
27
|
+
request.delete :body
|
28
|
+
else
|
29
|
+
body_is_json = request[:headers]['Content-Type'] =~ /json/
|
30
|
+
request[:body] = body_is_json ? JSON.parse(body_string) : body_string
|
31
|
+
end
|
32
|
+
request[:method] = request[:method].downcase
|
33
|
+
request
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def headers_from env
|
39
|
+
headers = env.reject{ |key, value| !(key.start_with?("HTTP") || key == 'CONTENT_TYPE' || key == 'CONTENT_LENGTH')}
|
40
|
+
headers.inject({}) do | hash, header |
|
41
|
+
hash[standardise_header(header.first)] = header.last
|
42
|
+
hash
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def standardise_header header
|
47
|
+
header.gsub(/^HTTP_/, '').split("_").collect{|word| word[0] + word[1..-1].downcase}.join("-")
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|