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.
Files changed (75) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +620 -0
  3. data/LICENSE.txt +22 -0
  4. data/README.md +5 -0
  5. data/lib/pact/array_like.rb +49 -0
  6. data/lib/pact/configuration.rb +193 -0
  7. data/lib/pact/consumer/request.rb +27 -0
  8. data/lib/pact/consumer_contract/consumer_contract.rb +97 -0
  9. data/lib/pact/consumer_contract/file_name.rb +22 -0
  10. data/lib/pact/consumer_contract/headers.rb +51 -0
  11. data/lib/pact/consumer_contract/http_consumer_contract_parser.rb +37 -0
  12. data/lib/pact/consumer_contract/interaction.rb +81 -0
  13. data/lib/pact/consumer_contract/interaction_parser.rb +23 -0
  14. data/lib/pact/consumer_contract/interaction_v2_parser.rb +57 -0
  15. data/lib/pact/consumer_contract/interaction_v3_parser.rb +92 -0
  16. data/lib/pact/consumer_contract/pact_file.rb +157 -0
  17. data/lib/pact/consumer_contract/provider_state.rb +34 -0
  18. data/lib/pact/consumer_contract/query.rb +138 -0
  19. data/lib/pact/consumer_contract/query_hash.rb +89 -0
  20. data/lib/pact/consumer_contract/query_string.rb +51 -0
  21. data/lib/pact/consumer_contract/request.rb +83 -0
  22. data/lib/pact/consumer_contract/response.rb +58 -0
  23. data/lib/pact/consumer_contract/service_consumer.rb +28 -0
  24. data/lib/pact/consumer_contract/service_provider.rb +28 -0
  25. data/lib/pact/consumer_contract/string_with_matching_rules.rb +17 -0
  26. data/lib/pact/consumer_contract.rb +1 -0
  27. data/lib/pact/errors.rb +21 -0
  28. data/lib/pact/helpers.rb +60 -0
  29. data/lib/pact/http/authorization_header_redactor.rb +32 -0
  30. data/lib/pact/logging.rb +14 -0
  31. data/lib/pact/matchers/actual_type.rb +16 -0
  32. data/lib/pact/matchers/base_difference.rb +39 -0
  33. data/lib/pact/matchers/differ.rb +153 -0
  34. data/lib/pact/matchers/difference.rb +13 -0
  35. data/lib/pact/matchers/difference_indicator.rb +26 -0
  36. data/lib/pact/matchers/embedded_diff_formatter.rb +60 -0
  37. data/lib/pact/matchers/expected_type.rb +35 -0
  38. data/lib/pact/matchers/extract_diff_messages.rb +76 -0
  39. data/lib/pact/matchers/index_not_found.rb +15 -0
  40. data/lib/pact/matchers/list_diff_formatter.rb +103 -0
  41. data/lib/pact/matchers/matchers.rb +285 -0
  42. data/lib/pact/matchers/multipart_form_diff_formatter.rb +41 -0
  43. data/lib/pact/matchers/no_diff_at_index.rb +18 -0
  44. data/lib/pact/matchers/regexp_difference.rb +13 -0
  45. data/lib/pact/matchers/type_difference.rb +16 -0
  46. data/lib/pact/matchers/unexpected_index.rb +11 -0
  47. data/lib/pact/matchers/unexpected_key.rb +11 -0
  48. data/lib/pact/matchers/unix_diff_formatter.rb +157 -0
  49. data/lib/pact/matchers.rb +1 -0
  50. data/lib/pact/matching_rules/extract.rb +91 -0
  51. data/lib/pact/matching_rules/jsonpath.rb +58 -0
  52. data/lib/pact/matching_rules/merge.rb +125 -0
  53. data/lib/pact/matching_rules/v3/extract.rb +94 -0
  54. data/lib/pact/matching_rules/v3/merge.rb +141 -0
  55. data/lib/pact/matching_rules.rb +30 -0
  56. data/lib/pact/reification.rb +56 -0
  57. data/lib/pact/rspec.rb +51 -0
  58. data/lib/pact/shared/active_support_support.rb +65 -0
  59. data/lib/pact/shared/dsl.rb +76 -0
  60. data/lib/pact/shared/form_differ.rb +32 -0
  61. data/lib/pact/shared/jruby_support.rb +18 -0
  62. data/lib/pact/shared/json_differ.rb +10 -0
  63. data/lib/pact/shared/key_not_found.rb +15 -0
  64. data/lib/pact/shared/multipart_form_differ.rb +16 -0
  65. data/lib/pact/shared/null_expectation.rb +31 -0
  66. data/lib/pact/shared/request.rb +106 -0
  67. data/lib/pact/shared/text_differ.rb +11 -0
  68. data/lib/pact/something_like.rb +49 -0
  69. data/lib/pact/specification_version.rb +18 -0
  70. data/lib/pact/support/version.rb +5 -0
  71. data/lib/pact/support.rb +12 -0
  72. data/lib/pact/symbolize_keys.rb +13 -0
  73. data/lib/pact/term.rb +85 -0
  74. data/lib/tasks/pact.rake +29 -0
  75. 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