hs-pact-support 1.17.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 +7 -0
- data/CHANGELOG.md +620 -0
- data/LICENSE.txt +22 -0
- data/README.md +5 -0
- data/lib/pact/array_like.rb +49 -0
- data/lib/pact/configuration.rb +193 -0
- data/lib/pact/consumer/request.rb +27 -0
- data/lib/pact/consumer_contract/consumer_contract.rb +97 -0
- data/lib/pact/consumer_contract/file_name.rb +22 -0
- data/lib/pact/consumer_contract/headers.rb +51 -0
- data/lib/pact/consumer_contract/http_consumer_contract_parser.rb +37 -0
- data/lib/pact/consumer_contract/interaction.rb +81 -0
- data/lib/pact/consumer_contract/interaction_parser.rb +23 -0
- data/lib/pact/consumer_contract/interaction_v2_parser.rb +57 -0
- data/lib/pact/consumer_contract/interaction_v3_parser.rb +92 -0
- data/lib/pact/consumer_contract/pact_file.rb +157 -0
- data/lib/pact/consumer_contract/provider_state.rb +34 -0
- data/lib/pact/consumer_contract/query.rb +138 -0
- data/lib/pact/consumer_contract/query_hash.rb +89 -0
- data/lib/pact/consumer_contract/query_string.rb +51 -0
- data/lib/pact/consumer_contract/request.rb +83 -0
- data/lib/pact/consumer_contract/response.rb +58 -0
- data/lib/pact/consumer_contract/service_consumer.rb +28 -0
- data/lib/pact/consumer_contract/service_provider.rb +28 -0
- data/lib/pact/consumer_contract/string_with_matching_rules.rb +17 -0
- data/lib/pact/consumer_contract.rb +1 -0
- data/lib/pact/errors.rb +21 -0
- data/lib/pact/helpers.rb +60 -0
- data/lib/pact/http/authorization_header_redactor.rb +32 -0
- data/lib/pact/logging.rb +14 -0
- data/lib/pact/matchers/actual_type.rb +16 -0
- data/lib/pact/matchers/base_difference.rb +39 -0
- data/lib/pact/matchers/differ.rb +153 -0
- data/lib/pact/matchers/difference.rb +13 -0
- data/lib/pact/matchers/difference_indicator.rb +26 -0
- data/lib/pact/matchers/embedded_diff_formatter.rb +60 -0
- data/lib/pact/matchers/expected_type.rb +35 -0
- data/lib/pact/matchers/extract_diff_messages.rb +76 -0
- data/lib/pact/matchers/index_not_found.rb +15 -0
- data/lib/pact/matchers/list_diff_formatter.rb +103 -0
- data/lib/pact/matchers/matchers.rb +285 -0
- data/lib/pact/matchers/multipart_form_diff_formatter.rb +41 -0
- data/lib/pact/matchers/no_diff_at_index.rb +18 -0
- data/lib/pact/matchers/regexp_difference.rb +13 -0
- data/lib/pact/matchers/type_difference.rb +16 -0
- data/lib/pact/matchers/unexpected_index.rb +11 -0
- data/lib/pact/matchers/unexpected_key.rb +11 -0
- data/lib/pact/matchers/unix_diff_formatter.rb +157 -0
- data/lib/pact/matchers.rb +1 -0
- data/lib/pact/matching_rules/extract.rb +91 -0
- data/lib/pact/matching_rules/jsonpath.rb +58 -0
- data/lib/pact/matching_rules/merge.rb +125 -0
- data/lib/pact/matching_rules/v3/extract.rb +94 -0
- data/lib/pact/matching_rules/v3/merge.rb +141 -0
- data/lib/pact/matching_rules.rb +30 -0
- data/lib/pact/reification.rb +56 -0
- data/lib/pact/rspec.rb +51 -0
- data/lib/pact/shared/active_support_support.rb +65 -0
- data/lib/pact/shared/dsl.rb +76 -0
- data/lib/pact/shared/form_differ.rb +32 -0
- data/lib/pact/shared/jruby_support.rb +18 -0
- data/lib/pact/shared/json_differ.rb +10 -0
- data/lib/pact/shared/key_not_found.rb +15 -0
- data/lib/pact/shared/multipart_form_differ.rb +16 -0
- data/lib/pact/shared/null_expectation.rb +31 -0
- data/lib/pact/shared/request.rb +106 -0
- data/lib/pact/shared/text_differ.rb +11 -0
- data/lib/pact/something_like.rb +49 -0
- data/lib/pact/specification_version.rb +18 -0
- data/lib/pact/support/version.rb +5 -0
- data/lib/pact/support.rb +12 -0
- data/lib/pact/symbolize_keys.rb +13 -0
- data/lib/pact/term.rb +85 -0
- data/lib/tasks/pact.rake +29 -0
- metadata +327 -0
@@ -0,0 +1,92 @@
|
|
1
|
+
require 'pact/consumer_contract/request'
|
2
|
+
require 'pact/consumer_contract/response'
|
3
|
+
require 'pact/consumer_contract/provider_state'
|
4
|
+
require 'pact/symbolize_keys'
|
5
|
+
require 'pact/matching_rules'
|
6
|
+
require 'pact/errors'
|
7
|
+
require 'pact/consumer_contract/string_with_matching_rules'
|
8
|
+
|
9
|
+
module Pact
|
10
|
+
class InteractionV3Parser
|
11
|
+
|
12
|
+
include SymbolizeKeys
|
13
|
+
|
14
|
+
def self.call hash, options
|
15
|
+
request = parse_request(hash['request'], options)
|
16
|
+
response = parse_response(hash['response'], options)
|
17
|
+
provider_states = parse_provider_states(hash['providerStates'])
|
18
|
+
provider_state = provider_states.any? ? provider_states.first.name : nil
|
19
|
+
if provider_states && provider_states.size > 1
|
20
|
+
Pact.configuration.error_stream.puts("WARN: Currently only 1 provider state is supported. Ignoring ")
|
21
|
+
end
|
22
|
+
metadata = parse_metadata(hash['metadata'])
|
23
|
+
Interaction.new(symbolize_keys(hash).merge(request: request,
|
24
|
+
response: response,
|
25
|
+
provider_states: provider_states,
|
26
|
+
provider_state: provider_state,
|
27
|
+
metadata: metadata))
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.parse_request request_hash, options
|
31
|
+
request_matching_rules = request_hash['matchingRules'] || {}
|
32
|
+
if request_hash['body'].is_a?(String)
|
33
|
+
parse_request_with_string_body(request_hash, request_matching_rules['body'] || {}, options)
|
34
|
+
else
|
35
|
+
parse_request_with_non_string_body(request_hash, request_matching_rules, options)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def self.parse_response response_hash, options
|
40
|
+
response_matching_rules = response_hash['matchingRules'] || {}
|
41
|
+
if response_hash['body'].is_a?(String)
|
42
|
+
parse_response_with_string_body(response_hash, response_matching_rules['body'] || {}, options)
|
43
|
+
else
|
44
|
+
parse_response_with_non_string_body(response_hash, response_matching_rules, options)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def self.parse_request_with_non_string_body request_hash, request_matching_rules, options
|
49
|
+
request_hash = request_hash.keys.each_with_object({}) do | key, new_hash |
|
50
|
+
new_hash[key] = Pact::MatchingRules.merge(request_hash[key], look_up_matching_rules(key, request_matching_rules), options)
|
51
|
+
end
|
52
|
+
Pact::Request::Expected.from_hash(request_hash)
|
53
|
+
end
|
54
|
+
|
55
|
+
def self.parse_response_with_non_string_body response_hash, response_matching_rules, options
|
56
|
+
response_hash = response_hash.keys.each_with_object({}) do | key, new_hash |
|
57
|
+
new_hash[key] = Pact::MatchingRules.merge(response_hash[key], look_up_matching_rules(key, response_matching_rules), options)
|
58
|
+
end
|
59
|
+
Pact::Response.from_hash(response_hash)
|
60
|
+
end
|
61
|
+
|
62
|
+
def self.parse_request_with_string_body request_hash, request_matching_rules, options
|
63
|
+
string_with_matching_rules = StringWithMatchingRules.new(request_hash['body'], options[:pact_specification_version], request_matching_rules)
|
64
|
+
Pact::Request::Expected.from_hash(request_hash.merge('body' => string_with_matching_rules))
|
65
|
+
end
|
66
|
+
|
67
|
+
def self.parse_response_with_string_body response_hash, response_matching_rules, options
|
68
|
+
string_with_matching_rules = StringWithMatchingRules.new(response_hash['body'], options[:pact_specification_version], response_matching_rules)
|
69
|
+
Pact::Response.from_hash(response_hash.merge('body' => string_with_matching_rules))
|
70
|
+
end
|
71
|
+
|
72
|
+
def self.parse_provider_states provider_states
|
73
|
+
(provider_states || []).collect do | provider_state_hash |
|
74
|
+
Pact::ProviderState.new(provider_state_hash['name'], provider_state_hash['params'])
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def self.parse_metadata metadata_hash
|
79
|
+
symbolize_keys(metadata_hash)
|
80
|
+
end
|
81
|
+
|
82
|
+
def self.look_up_matching_rules(key, matching_rules)
|
83
|
+
# The matching rules for the path operate on the object itself and don't have sub paths
|
84
|
+
# Convert it into the format that Merge expects.
|
85
|
+
if key == 'path'
|
86
|
+
matching_rules[key] ? { '$.' => matching_rules[key] } : nil
|
87
|
+
else
|
88
|
+
matching_rules[key]
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
@@ -0,0 +1,157 @@
|
|
1
|
+
require "net/http"
|
2
|
+
require "pact/configuration"
|
3
|
+
require "pact/http/authorization_header_redactor"
|
4
|
+
|
5
|
+
module Pact
|
6
|
+
module PactFile
|
7
|
+
extend self
|
8
|
+
|
9
|
+
OPEN_TIMEOUT = 5
|
10
|
+
READ_TIMEOUT = 5
|
11
|
+
RETRY_LIMIT = 3
|
12
|
+
|
13
|
+
class HttpError < StandardError
|
14
|
+
attr_reader :uri, :response
|
15
|
+
|
16
|
+
def initialize(uri, response)
|
17
|
+
@uri, @response = uri, response
|
18
|
+
super("HTTP request failed: status=#{response.code}")
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def read uri, options = {}
|
23
|
+
uri_string = uri.to_s
|
24
|
+
pact = render_pact(uri_string, options)
|
25
|
+
if options[:save_pactfile_to_tmp]
|
26
|
+
save_pactfile_to_tmp pact, ::File.basename(uri_string)
|
27
|
+
end
|
28
|
+
pact
|
29
|
+
rescue StandardError => e
|
30
|
+
$stderr.puts "Error reading file from #{uri}"
|
31
|
+
$stderr.puts "#{e.to_s} #{e.backtrace.join("\n")}"
|
32
|
+
raise e
|
33
|
+
end
|
34
|
+
|
35
|
+
def save_pactfile_to_tmp pact, name
|
36
|
+
::FileUtils.mkdir_p Pact.configuration.tmp_dir
|
37
|
+
::File.open(Pact.configuration.tmp_dir + "/#{name}", "w") { |file| file << pact}
|
38
|
+
rescue Errno::EROFS => e
|
39
|
+
# do nothing, probably on RunKit
|
40
|
+
end
|
41
|
+
|
42
|
+
def render_pact(uri_string, options)
|
43
|
+
local?(uri_string) ? get_local(uri_string, options) : get_remote_with_retry(uri_string, options)
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
|
48
|
+
def local? uri
|
49
|
+
!uri.start_with?("http://", "https://")
|
50
|
+
end
|
51
|
+
|
52
|
+
def get_local(filepath, _)
|
53
|
+
File.read windows_safe(filepath)
|
54
|
+
end
|
55
|
+
|
56
|
+
def get_remote_with_retry(uri_string, options)
|
57
|
+
uri = URI(uri_string)
|
58
|
+
if uri.userinfo
|
59
|
+
options[:username] = uri.user unless options[:username]
|
60
|
+
options[:password] = uri.password unless options[:password]
|
61
|
+
end
|
62
|
+
((options[:retry_limit] || RETRY_LIMIT) + 1).times do |i|
|
63
|
+
begin
|
64
|
+
response = get_remote(uri, options)
|
65
|
+
case
|
66
|
+
when success?(response)
|
67
|
+
return response.body
|
68
|
+
when retryable?(response)
|
69
|
+
raise HttpError.new(uri, response) if abort_retry?(i, options)
|
70
|
+
delay_retry(i + 1)
|
71
|
+
next
|
72
|
+
else
|
73
|
+
raise HttpError.new(uri, response)
|
74
|
+
end
|
75
|
+
rescue Timeout::Error => e
|
76
|
+
raise e if abort_retry?(i, options)
|
77
|
+
delay_retry(i + 1)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def get_remote(uri, options)
|
83
|
+
request = Net::HTTP::Get.new(uri)
|
84
|
+
request = prepare_auth(request, options) if options[:username] || options[:token]
|
85
|
+
|
86
|
+
http = prepare_request(uri, options)
|
87
|
+
response = perform_http_request(http, request, options)
|
88
|
+
|
89
|
+
if response.is_a?(Net::HTTPRedirection)
|
90
|
+
uri = URI(response.header['location'])
|
91
|
+
req = Net::HTTP::Get.new(uri)
|
92
|
+
req = prepare_auth(req, options) if options[:username] || options[:token]
|
93
|
+
|
94
|
+
http = prepare_request(uri, options)
|
95
|
+
response = perform_http_request(http, req, options)
|
96
|
+
end
|
97
|
+
response
|
98
|
+
end
|
99
|
+
|
100
|
+
def prepare_auth(request, options)
|
101
|
+
request.basic_auth(options[:username], options[:password]) if options[:username]
|
102
|
+
request['Authorization'] = "Bearer #{options[:token]}" if options[:token]
|
103
|
+
request
|
104
|
+
end
|
105
|
+
|
106
|
+
def prepare_request(uri, options)
|
107
|
+
http = Net::HTTP.new(uri.host, uri.port, :ENV)
|
108
|
+
http.use_ssl = (uri.scheme == 'https')
|
109
|
+
http.ca_file = ENV['SSL_CERT_FILE'] if ENV['SSL_CERT_FILE'] && ENV['SSL_CERT_FILE'] != ''
|
110
|
+
http.ca_path = ENV['SSL_CERT_DIR'] if ENV['SSL_CERT_DIR'] && ENV['SSL_CERT_DIR'] != ''
|
111
|
+
http.set_debug_output(Pact::Http::AuthorizationHeaderRedactor.new(Pact.configuration.output_stream)) if verbose?(options)
|
112
|
+
if disable_ssl_verification?
|
113
|
+
if verbose?(options)
|
114
|
+
Pact.configuration.output_stream.puts("SSL verification is disabled")
|
115
|
+
end
|
116
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
117
|
+
end
|
118
|
+
http
|
119
|
+
end
|
120
|
+
|
121
|
+
def perform_http_request(http, request, options)
|
122
|
+
http.start do |http|
|
123
|
+
http.open_timeout = options[:open_timeout] || OPEN_TIMEOUT
|
124
|
+
http.read_timeout = options[:read_timeout] || READ_TIMEOUT
|
125
|
+
http.request(request)
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
def success?(response)
|
130
|
+
response.code.to_i == 200
|
131
|
+
end
|
132
|
+
|
133
|
+
def retryable?(response)
|
134
|
+
(500...600).cover?(response.code.to_i)
|
135
|
+
end
|
136
|
+
|
137
|
+
def abort_retry?(count, options)
|
138
|
+
count >= (options[:retry_limit] || RETRY_LIMIT)
|
139
|
+
end
|
140
|
+
|
141
|
+
def delay_retry(count)
|
142
|
+
Kernel.sleep(2 ** count * 0.3)
|
143
|
+
end
|
144
|
+
|
145
|
+
def windows_safe(uri)
|
146
|
+
uri.start_with?("http") ? uri : uri.gsub("\\", File::SEPARATOR)
|
147
|
+
end
|
148
|
+
|
149
|
+
def verbose?(options)
|
150
|
+
options[:verbose] || ENV['VERBOSE'] == 'true'
|
151
|
+
end
|
152
|
+
|
153
|
+
def disable_ssl_verification?
|
154
|
+
ENV['PACT_DISABLE_SSL_VERIFICATION'] == 'true' || ENV['PACT_BROKER_DISABLE_SSL_VERIFICATION'] == 'true'
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module Pact
|
2
|
+
class ProviderState
|
3
|
+
|
4
|
+
attr_reader :name, :params
|
5
|
+
|
6
|
+
def initialize name, params = {}
|
7
|
+
@name = name
|
8
|
+
@params = params
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.from_hash(hash)
|
12
|
+
new(hash["name"], hash["params"])
|
13
|
+
end
|
14
|
+
|
15
|
+
def ==(other)
|
16
|
+
other.is_a?(Pact::ProviderState) && other.name == self.name && other.params == self.params
|
17
|
+
end
|
18
|
+
|
19
|
+
def to_hash
|
20
|
+
{
|
21
|
+
"name" => name,
|
22
|
+
"params" => params
|
23
|
+
}
|
24
|
+
end
|
25
|
+
|
26
|
+
def to_json(opts = {})
|
27
|
+
as_json(opts).to_json(opts)
|
28
|
+
end
|
29
|
+
|
30
|
+
def as_json(opts = {})
|
31
|
+
to_hash
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,138 @@
|
|
1
|
+
require 'pact/consumer_contract/query_hash'
|
2
|
+
require 'pact/consumer_contract/query_string'
|
3
|
+
|
4
|
+
module Pact
|
5
|
+
class Query
|
6
|
+
DEFAULT_SEP = /[&;] */n
|
7
|
+
COMMON_SEP = { ";" => /[;] */n, ";," => /[;,] */n, "&" => /[&] */n }
|
8
|
+
|
9
|
+
class NestedQuery < Hash; end
|
10
|
+
|
11
|
+
def self.create query
|
12
|
+
if query.is_a? Hash
|
13
|
+
Pact::QueryHash.new(query)
|
14
|
+
else
|
15
|
+
Pact::QueryString.new(query)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.is_a_query_object?(object)
|
20
|
+
object.is_a?(Pact::QueryHash) || object.is_a?(Pact::QueryString)
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.parsed_as_nested?(object)
|
24
|
+
object.is_a?(NestedQuery)
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.parse_string query_string
|
28
|
+
parsed_query = parse_string_as_non_nested_query(query_string)
|
29
|
+
|
30
|
+
# If Rails nested params...
|
31
|
+
if parsed_query.keys.any?{ | key| key =~ /\[.*\]/ }
|
32
|
+
parse_string_as_nested_query(query_string)
|
33
|
+
else
|
34
|
+
parsed_query.each_with_object({}) do | (key, value), new_hash |
|
35
|
+
new_hash[key] = [*value]
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
# Ripped from Rack to avoid adding an unnecessary dependency, thank you Rack
|
41
|
+
# https://github.com/rack/rack/blob/649c72bab9e7b50d657b5b432d0c205c95c2be07/lib/rack/utils.rb
|
42
|
+
def self.parse_string_as_non_nested_query(qs, d = nil, &unescaper)
|
43
|
+
unescaper ||= method(:unescape)
|
44
|
+
|
45
|
+
params = {}
|
46
|
+
|
47
|
+
(qs || '').split(d ? (COMMON_SEP[d] || /[#{d}] */n) : DEFAULT_SEP).each do |p|
|
48
|
+
next if p.empty?
|
49
|
+
k, v = p.split('=', 2).map!(&unescaper)
|
50
|
+
|
51
|
+
if cur = params[k]
|
52
|
+
if cur.class == Array
|
53
|
+
params[k] << v
|
54
|
+
else
|
55
|
+
params[k] = [cur, v]
|
56
|
+
end
|
57
|
+
else
|
58
|
+
params[k] = v
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
return params.to_h
|
63
|
+
end
|
64
|
+
|
65
|
+
def self.parse_string_as_nested_query(qs, d = nil)
|
66
|
+
params = {}
|
67
|
+
|
68
|
+
unless qs.nil? || qs.empty?
|
69
|
+
(qs || '').split(d ? (COMMON_SEP[d] || /[#{d}] */n) : DEFAULT_SEP).each do |p|
|
70
|
+
k, v = p.split('=', 2).map! { |s| unescape(s) }
|
71
|
+
|
72
|
+
normalize_params(params, k, v)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
return NestedQuery[params.to_h]
|
77
|
+
end
|
78
|
+
|
79
|
+
def self.normalize_params(params, name, v)
|
80
|
+
name =~ %r(\A[\[\]]*([^\[\]]+)\]*)
|
81
|
+
k = $1 || ''
|
82
|
+
after = $' || ''
|
83
|
+
|
84
|
+
if k.empty?
|
85
|
+
if !v.nil? && name == "[]"
|
86
|
+
return Array(v)
|
87
|
+
else
|
88
|
+
return
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
if after == ''
|
93
|
+
params[k] = v
|
94
|
+
elsif after == "["
|
95
|
+
params[name] = v
|
96
|
+
elsif after == "[]"
|
97
|
+
params[k] ||= []
|
98
|
+
raise ParameterTypeError, "expected Array (got #{params[k].class.name}) for param `#{k}'" unless params[k].is_a?(Array)
|
99
|
+
params[k] << v
|
100
|
+
elsif after =~ %r(^\[\]\[([^\[\]]+)\]$) || after =~ %r(^\[\](.+)$)
|
101
|
+
child_key = $1
|
102
|
+
params[k] ||= []
|
103
|
+
raise ParameterTypeError, "expected Array (got #{params[k].class.name}) for param `#{k}'" unless params[k].is_a?(Array)
|
104
|
+
if params_hash_type?(params[k].last) && !params_hash_has_key?(params[k].last, child_key)
|
105
|
+
normalize_params(params[k].last, child_key, v)
|
106
|
+
else
|
107
|
+
params[k] << normalize_params({}, child_key, v)
|
108
|
+
end
|
109
|
+
else
|
110
|
+
params[k] ||= {}
|
111
|
+
raise ParameterTypeError, "expected Hash (got #{params[k].class.name}) for param `#{k}'" unless params_hash_type?(params[k])
|
112
|
+
params[k] = normalize_params(params[k], after, v)
|
113
|
+
end
|
114
|
+
|
115
|
+
params
|
116
|
+
end
|
117
|
+
|
118
|
+
def self.params_hash_type?(obj)
|
119
|
+
obj.is_a?(Hash)
|
120
|
+
end
|
121
|
+
|
122
|
+
def self.params_hash_has_key?(hash, key)
|
123
|
+
return false if key =~ /\[\]/
|
124
|
+
|
125
|
+
key.split(/[\[\]]+/).inject(hash) do |h, part|
|
126
|
+
next h if part == ''
|
127
|
+
return false unless params_hash_type?(h) && h.key?(part)
|
128
|
+
h[part]
|
129
|
+
end
|
130
|
+
|
131
|
+
true
|
132
|
+
end
|
133
|
+
|
134
|
+
def self.unescape(s, encoding = Encoding::UTF_8)
|
135
|
+
URI.decode_www_form_component(s, encoding)
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
require 'cgi'
|
2
|
+
require 'pact/shared/active_support_support'
|
3
|
+
require 'pact/symbolize_keys'
|
4
|
+
|
5
|
+
module Pact
|
6
|
+
class QueryHash
|
7
|
+
|
8
|
+
include ActiveSupportSupport
|
9
|
+
include SymbolizeKeys
|
10
|
+
|
11
|
+
attr_reader :original_string
|
12
|
+
|
13
|
+
def initialize(query, original_string = nil, nested = false)
|
14
|
+
@hash = query.nil? ? query : convert_to_hash_of_arrays(query)
|
15
|
+
@original_string = original_string
|
16
|
+
@nested = nested
|
17
|
+
end
|
18
|
+
|
19
|
+
def nested?
|
20
|
+
@nested
|
21
|
+
end
|
22
|
+
|
23
|
+
def any_key_contains_square_brackets?
|
24
|
+
query.keys.any?{ |key| key =~ /\[.*\]/ }
|
25
|
+
end
|
26
|
+
|
27
|
+
def as_json(opts = {})
|
28
|
+
@hash
|
29
|
+
end
|
30
|
+
|
31
|
+
def to_json(opts = {})
|
32
|
+
as_json(opts).to_json(opts)
|
33
|
+
end
|
34
|
+
|
35
|
+
def eql?(other)
|
36
|
+
self == other
|
37
|
+
end
|
38
|
+
|
39
|
+
def ==(other)
|
40
|
+
QueryHash === other && other.query == query
|
41
|
+
end
|
42
|
+
|
43
|
+
# other will always be a QueryString, not a QueryHash, as it will have ben created
|
44
|
+
# from the actual query string.
|
45
|
+
def difference(other)
|
46
|
+
require 'pact/matchers' # avoid recursive loop between this file, pact/reification and pact/matchers
|
47
|
+
|
48
|
+
if any_key_contains_square_brackets?
|
49
|
+
other_query_hash_non_nested = Query.parse_string_as_non_nested_query(other.query)
|
50
|
+
Pact::Matchers.diff(query, convert_to_hash_of_arrays(other_query_hash_non_nested), allow_unexpected_keys: false)
|
51
|
+
else
|
52
|
+
other_query_hash = Query.parse_string(other.query)
|
53
|
+
Pact::Matchers.diff(query, symbolize_keys(convert_to_hash_of_arrays(other_query_hash)), allow_unexpected_keys: false)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def query
|
58
|
+
@hash
|
59
|
+
end
|
60
|
+
|
61
|
+
def to_s
|
62
|
+
@hash.inspect
|
63
|
+
end
|
64
|
+
|
65
|
+
def empty?
|
66
|
+
@hash && @hash.empty?
|
67
|
+
end
|
68
|
+
|
69
|
+
def to_hash
|
70
|
+
@hash
|
71
|
+
end
|
72
|
+
|
73
|
+
private
|
74
|
+
|
75
|
+
def convert_to_hash_of_arrays(query)
|
76
|
+
query.each_with_object({}) {|(k, v), hash| insert(hash, k, v) }
|
77
|
+
end
|
78
|
+
|
79
|
+
def insert(hash, k, v)
|
80
|
+
if Hash === v
|
81
|
+
v.each {|k2, v2| insert(hash, :"#{k}[#{k2}]", v2) }
|
82
|
+
elsif Pact::ArrayLike === v
|
83
|
+
hash[k.to_sym] = v
|
84
|
+
else
|
85
|
+
hash[k.to_sym] = [*v]
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
require 'pact/shared/active_support_support'
|
2
|
+
|
3
|
+
module Pact
|
4
|
+
class QueryString
|
5
|
+
|
6
|
+
include ActiveSupportSupport
|
7
|
+
|
8
|
+
def initialize query
|
9
|
+
@query = query.nil? ? query : query.dup
|
10
|
+
end
|
11
|
+
|
12
|
+
def as_json opts = {}
|
13
|
+
@query
|
14
|
+
end
|
15
|
+
|
16
|
+
def to_json opts = {}
|
17
|
+
as_json(opts).to_json(opts)
|
18
|
+
end
|
19
|
+
|
20
|
+
def eql? other
|
21
|
+
self == other
|
22
|
+
end
|
23
|
+
|
24
|
+
def == other
|
25
|
+
QueryString === other && other.query == query
|
26
|
+
end
|
27
|
+
|
28
|
+
def difference(other)
|
29
|
+
require 'pact/matchers' # avoid recursive loop between this file and pact/matchers
|
30
|
+
Pact::Matchers.diff(query, other.query)
|
31
|
+
end
|
32
|
+
|
33
|
+
def query
|
34
|
+
@query
|
35
|
+
end
|
36
|
+
|
37
|
+
def to_s
|
38
|
+
@query
|
39
|
+
end
|
40
|
+
|
41
|
+
def empty?
|
42
|
+
@query && @query.empty?
|
43
|
+
end
|
44
|
+
|
45
|
+
# Naughty...
|
46
|
+
def nil?
|
47
|
+
@query.nil?
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
require 'pact/shared/request'
|
2
|
+
require 'pact/shared/null_expectation'
|
3
|
+
|
4
|
+
module Pact
|
5
|
+
module Request
|
6
|
+
class Expected < Pact::Request::Base
|
7
|
+
|
8
|
+
DEFAULT_OPTIONS = {:allow_unexpected_keys => false}.freeze
|
9
|
+
attr_accessor :options #Temporary hack
|
10
|
+
|
11
|
+
def self.from_hash(hash)
|
12
|
+
sym_hash = symbolize_keys hash
|
13
|
+
method = sym_hash.fetch(:method)
|
14
|
+
path = sym_hash.fetch(:path)
|
15
|
+
query = sym_hash.fetch(:query, key_not_found)
|
16
|
+
headers = sym_hash.fetch(:headers, key_not_found)
|
17
|
+
body = sym_hash.fetch(:body, key_not_found)
|
18
|
+
options = sym_hash.fetch(:options, {})
|
19
|
+
new(method, path, headers, body, query, options)
|
20
|
+
end
|
21
|
+
|
22
|
+
def initialize(method, path, headers, body, query, options = {})
|
23
|
+
super(method, path, headers, body, query)
|
24
|
+
@options = options
|
25
|
+
end
|
26
|
+
|
27
|
+
def matches?(actual_request)
|
28
|
+
difference(actual_request).empty?
|
29
|
+
end
|
30
|
+
|
31
|
+
def matches_route? actual_request
|
32
|
+
require 'pact/matchers' # avoid recusive loop between pact/reification, pact/matchers and this file
|
33
|
+
route = {:method => method.upcase, :path => path}
|
34
|
+
other_route = {:method => actual_request.method.upcase, :path => actual_request.path}
|
35
|
+
Pact::Matchers.diff(route, other_route).empty?
|
36
|
+
end
|
37
|
+
|
38
|
+
def difference(actual_request)
|
39
|
+
require 'pact/matchers' # avoid recusive loop between pact/reification, pact/matchers and this file
|
40
|
+
request_diff = Pact::Matchers.diff(to_hash_without_body_or_query, actual_request.to_hash_without_body_or_query)
|
41
|
+
request_diff.merge!(query_diff(actual_request.query))
|
42
|
+
request_diff.merge!(body_diff(actual_request.body))
|
43
|
+
end
|
44
|
+
|
45
|
+
protected
|
46
|
+
|
47
|
+
def query_diff actual_query
|
48
|
+
if specified?(:query)
|
49
|
+
query_diff = query.difference(actual_query)
|
50
|
+
query_diff.any? ? {query: query_diff} : {}
|
51
|
+
else
|
52
|
+
{}
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def self.key_not_found
|
57
|
+
Pact::NullExpectation.new
|
58
|
+
end
|
59
|
+
|
60
|
+
private
|
61
|
+
|
62
|
+
# Options is a dirty hack to allow Condor to send extra keys in the request,
|
63
|
+
# as it's too much work to set up an exactly matching expectation.
|
64
|
+
# Need to implement a proper matching strategy and remove this.
|
65
|
+
# Do not rely on it!
|
66
|
+
def runtime_options
|
67
|
+
DEFAULT_OPTIONS.merge(symbolize_keys(options))
|
68
|
+
end
|
69
|
+
|
70
|
+
def body_diff(actual_body)
|
71
|
+
if specified?(:body)
|
72
|
+
body_difference = body_differ.call(body, actual_body, allow_unexpected_keys: runtime_options[:allow_unexpected_keys_in_body])
|
73
|
+
return { body: body_difference } if body_difference.any?
|
74
|
+
end
|
75
|
+
{}
|
76
|
+
end
|
77
|
+
|
78
|
+
def body_differ
|
79
|
+
Pact.configuration.body_differ_for_content_type content_type
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
require 'pact/consumer_contract/headers'
|
2
|
+
require 'pact/symbolize_keys'
|
3
|
+
|
4
|
+
module Pact
|
5
|
+
|
6
|
+
class Response < Hash
|
7
|
+
|
8
|
+
include SymbolizeKeys
|
9
|
+
|
10
|
+
ALLOWED_KEYS = [:status, :headers, :body, 'status', 'headers', 'body'].freeze
|
11
|
+
private_constant :ALLOWED_KEYS
|
12
|
+
|
13
|
+
def initialize attributes
|
14
|
+
merge!(attributes.reject{|key, value| !ALLOWED_KEYS.include?(key)})
|
15
|
+
end
|
16
|
+
|
17
|
+
def status
|
18
|
+
self[:status]
|
19
|
+
end
|
20
|
+
|
21
|
+
def headers
|
22
|
+
self[:headers]
|
23
|
+
end
|
24
|
+
|
25
|
+
def body
|
26
|
+
self[:body]
|
27
|
+
end
|
28
|
+
|
29
|
+
def specified? key
|
30
|
+
self.key?(key.to_sym)
|
31
|
+
end
|
32
|
+
|
33
|
+
def body_allows_any_value?
|
34
|
+
body_not_specified? || body_is_empty_hash?
|
35
|
+
end
|
36
|
+
|
37
|
+
def [] key
|
38
|
+
super key.to_sym
|
39
|
+
end
|
40
|
+
|
41
|
+
def self.from_hash hash
|
42
|
+
headers = Headers.new(hash[:headers] || hash['headers'] || {})
|
43
|
+
new(symbolize_keys(hash).merge(headers: headers))
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
|
48
|
+
def body_is_empty_hash?
|
49
|
+
body.is_a?(Hash) && body.empty?
|
50
|
+
end
|
51
|
+
|
52
|
+
def body_not_specified?
|
53
|
+
!specified?(:body)
|
54
|
+
end
|
55
|
+
|
56
|
+
end
|
57
|
+
|
58
|
+
end
|