webmock 3.26.4 → 3.34.0
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 +4 -4
- data/CHANGELOG.md +176 -0
- data/README.md +70 -2
- data/lib/webmock/http_lib_adapters/async_http_client_adapter.rb +20 -2
- data/lib/webmock/http_lib_adapters/curb_adapter.rb +13 -2
- data/lib/webmock/http_lib_adapters/net_http.rb +30 -6
- data/lib/webmock/http_lib_adapters/typhoeus_hydra_adapter.rb +54 -26
- data/lib/webmock/json_body_hint.rb +77 -0
- data/lib/webmock/matchers/any_arg_matcher.rb +4 -0
- data/lib/webmock/matchers/hash_excluding_matcher.rb +1 -1
- data/lib/webmock/rack_response.rb +28 -19
- data/lib/webmock/request_execution_verifier.rb +22 -0
- data/lib/webmock/request_pattern.rb +40 -30
- data/lib/webmock/request_signature.rb +15 -5
- data/lib/webmock/request_signature_snippet.rb +10 -10
- data/lib/webmock/request_stub.rb +13 -5
- data/lib/webmock/rspec/matchers/request_pattern_matcher.rb +47 -2
- data/lib/webmock/util/content_type.rb +39 -0
- data/lib/webmock/util/uri.rb +2 -2
- data/lib/webmock/version.rb +1 -1
- data/lib/webmock/webmock.rb +25 -5
- data/lib/webmock.rb +2 -0
- metadata +7 -5
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module WebMock
|
|
6
|
+
# A body declared as a string must match the request body exactly, which for
|
|
7
|
+
# JSON includes key order and whitespace. When a request fails to match only
|
|
8
|
+
# because of that, failure messages suggest declaring the body as a hash.
|
|
9
|
+
# See #485.
|
|
10
|
+
module JsonBodyHint
|
|
11
|
+
ADVICE = "A body declared as a string must match exactly. To match it as JSON, " \
|
|
12
|
+
"declare it as a hash, for example with(body: JSON.parse(json_string))."
|
|
13
|
+
|
|
14
|
+
STUB_HINT = "Hint: the request body is the same JSON as the body of this stub, " \
|
|
15
|
+
"but formatted differently, for example with keys in another order. #{ADVICE}"
|
|
16
|
+
|
|
17
|
+
EXPECTATION_HINT = "Hint: a request was made with the same JSON body as expected, " \
|
|
18
|
+
"but formatted differently, for example with keys in another order. #{ADVICE}"
|
|
19
|
+
|
|
20
|
+
# True when one of the patterns declares its body as a JSON object or array
|
|
21
|
+
# string, the request has a JSON content type and a body that parses to the
|
|
22
|
+
# same JSON but is written differently, and the method, URI and headers of
|
|
23
|
+
# every pattern match the request. Any other patterns the request has to
|
|
24
|
+
# match, such as the conditions of an RSpec expectation, must not declare
|
|
25
|
+
# a body of their own.
|
|
26
|
+
#
|
|
27
|
+
# Building a failure message must not call a block given to with, which
|
|
28
|
+
# could raise and replace the failure, or repeat side effects. So there is
|
|
29
|
+
# no hint when any of the patterns has one.
|
|
30
|
+
#
|
|
31
|
+
# The body is checked first, since that runs no user code, so URI and
|
|
32
|
+
# header matchers are only evaluated when a hint is otherwise possible.
|
|
33
|
+
def self.applies?(request_pattern, request_signature)
|
|
34
|
+
patterns = parts_of(request_pattern)
|
|
35
|
+
return false unless patterns.all? { |pattern| pattern.is_a?(RequestPattern) && pattern.with_block.nil? }
|
|
36
|
+
|
|
37
|
+
with_body = patterns.select(&:body_pattern)
|
|
38
|
+
return false unless with_body.size == 1
|
|
39
|
+
return false unless body_differs_only_in_formatting?(with_body.first.body_pattern, request_signature)
|
|
40
|
+
|
|
41
|
+
patterns.all? { |pattern| matches_except_body?(pattern, request_signature) }
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# A pattern made of parts that must all match, such as an RSpec expectation
|
|
45
|
+
# with conditions, responds to #patterns. A stub stands for its pattern.
|
|
46
|
+
def self.parts_of(request_pattern)
|
|
47
|
+
parts = request_pattern.respond_to?(:patterns) ? request_pattern.patterns : [request_pattern]
|
|
48
|
+
parts.map { |part| part.is_a?(RequestStub) ? part.request_pattern : part }
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def self.body_differs_only_in_formatting?(body_pattern, request_signature)
|
|
52
|
+
expected = body_pattern.pattern
|
|
53
|
+
actual = request_signature.body
|
|
54
|
+
return false unless expected.is_a?(String) && actual.is_a?(String) && expected != actual
|
|
55
|
+
return false unless request_signature.json_headers?
|
|
56
|
+
|
|
57
|
+
expected_json = parse_json(expected)
|
|
58
|
+
return false unless expected_json.is_a?(Hash) || expected_json.is_a?(Array)
|
|
59
|
+
|
|
60
|
+
# eql? compares nested values strictly, so 1 and 1.0 are different.
|
|
61
|
+
expected_json.eql?(parse_json(actual))
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def self.matches_except_body?(request_pattern, request_signature)
|
|
65
|
+
request_pattern.method_pattern.matches?(request_signature.method) &&
|
|
66
|
+
request_pattern.uri_pattern.matches?(request_signature.uri) &&
|
|
67
|
+
(request_pattern.headers_pattern.nil? || request_pattern.headers_pattern.matches?(request_signature.headers))
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def self.parse_json(string)
|
|
71
|
+
::JSON.parse(string)
|
|
72
|
+
rescue ::JSON::JSONError, EncodingError
|
|
73
|
+
nil
|
|
74
|
+
end
|
|
75
|
+
private_class_method :parts_of, :body_differs_only_in_formatting?, :matches_except_body?, :parse_json
|
|
76
|
+
end
|
|
77
|
+
end
|
|
@@ -6,7 +6,7 @@ module WebMock
|
|
|
6
6
|
# https://github.com/rspec/rspec-mocks/blob/master/lib/rspec/mocks/argument_matchers.rb
|
|
7
7
|
class HashExcludingMatcher < HashArgumentMatcher
|
|
8
8
|
def ==(actual)
|
|
9
|
-
super { |key, value| !actual.key?(key) || value
|
|
9
|
+
super { |key, value| !actual.key?(key) || !(value === actual[key]) }
|
|
10
10
|
end
|
|
11
11
|
|
|
12
12
|
def inspect
|
|
@@ -27,47 +27,56 @@ module WebMock
|
|
|
27
27
|
|
|
28
28
|
def build_rack_env(request)
|
|
29
29
|
uri = request.uri
|
|
30
|
+
port = uri.inferred_port
|
|
30
31
|
headers = (request.headers || {}).dup
|
|
31
|
-
body = request.body || ''
|
|
32
|
+
body = (request.body || '').b
|
|
33
|
+
content_type = headers.delete('Content-Type')
|
|
34
|
+
headers.delete('Content-Length')
|
|
35
|
+
# The body is already decoded, as a server passes it on.
|
|
36
|
+
headers.delete('Transfer-Encoding')
|
|
32
37
|
|
|
33
38
|
env = {
|
|
34
39
|
# CGI variables specified by Rack
|
|
35
|
-
'REQUEST_METHOD'
|
|
36
|
-
'
|
|
37
|
-
'
|
|
38
|
-
'
|
|
39
|
-
'
|
|
40
|
-
'
|
|
41
|
-
'
|
|
42
|
-
'SCRIPT_NAME'
|
|
40
|
+
'REQUEST_METHOD' => request.method.to_s.upcase,
|
|
41
|
+
'CONTENT_LENGTH' => body.bytesize.to_s,
|
|
42
|
+
'PATH_INFO' => uri.path,
|
|
43
|
+
'QUERY_STRING' => uri.query || '',
|
|
44
|
+
'SERVER_NAME' => uri.host,
|
|
45
|
+
'SERVER_PORT' => port.to_s,
|
|
46
|
+
'SERVER_PROTOCOL' => 'HTTP/1.1',
|
|
47
|
+
'SCRIPT_NAME' => ""
|
|
43
48
|
}
|
|
49
|
+
env['CONTENT_TYPE'] = header_value('Content-Type', content_type) if content_type
|
|
44
50
|
|
|
45
51
|
env['HTTP_AUTHORIZATION'] = 'Basic ' + [uri.userinfo].pack('m').delete("\r\n") if uri.userinfo
|
|
46
52
|
|
|
47
53
|
# Rack-specific variables
|
|
48
54
|
env['rack.input'] = StringIO.new(body)
|
|
49
55
|
env['rack.errors'] = $stderr
|
|
50
|
-
if !Rack.const_defined?(:RELEASE) || Rack::RELEASE < "3"
|
|
51
|
-
env['rack.version']
|
|
56
|
+
if !Rack.const_defined?(:RELEASE) || Gem::Version.new(Rack::RELEASE) < Gem::Version.new("3")
|
|
57
|
+
env['rack.version'] = Rack::VERSION
|
|
58
|
+
env['rack.multithread'] = false
|
|
59
|
+
env['rack.multiprocess'] = false
|
|
52
60
|
end
|
|
53
61
|
env['rack.url_scheme'] = uri.scheme
|
|
54
62
|
env['rack.run_once'] = true
|
|
55
|
-
env['rack.session'] = session
|
|
56
|
-
env['rack.session.options'] = session_options
|
|
57
63
|
|
|
58
64
|
headers.each do |k, v|
|
|
59
|
-
env["HTTP_#{k.tr('-','_').upcase}"] = v
|
|
65
|
+
env["HTTP_#{k.tr('-','_').upcase}"] = header_value(k, v)
|
|
60
66
|
end
|
|
61
67
|
|
|
68
|
+
# Many clients add Host only when sending, so fill it in as they would.
|
|
69
|
+
env['HTTP_HOST'] ||= port == uri.default_port ? uri.host : "#{uri.host}:#{port}"
|
|
70
|
+
|
|
62
71
|
env
|
|
63
72
|
end
|
|
64
73
|
|
|
65
|
-
|
|
66
|
-
@session ||= {}
|
|
67
|
-
end
|
|
74
|
+
private
|
|
68
75
|
|
|
69
|
-
|
|
70
|
-
|
|
76
|
+
# Like a server, pass header values as bytes and join repeated ones. Rack
|
|
77
|
+
# splits cookies on "; ", so join those that way.
|
|
78
|
+
def header_value(name, value)
|
|
79
|
+
Array(value).map { |v| v.to_s.b }.join(name == 'Cookie' ? '; ' : WebMock::Util::Headers::JOIN)
|
|
71
80
|
end
|
|
72
81
|
end
|
|
73
82
|
end
|
|
@@ -56,9 +56,31 @@ module WebMock
|
|
|
56
56
|
def failure_message_phrase(is_negated=false)
|
|
57
57
|
negation = is_negated ? "was not" : "was"
|
|
58
58
|
"The request #{request_pattern} #{negation} expected to execute #{quantity_phrase(is_negated)}but it executed #{times(times_executed)}" +
|
|
59
|
+
(is_negated ? "" : json_body_hint) +
|
|
59
60
|
self.class.executed_requests_message
|
|
60
61
|
end
|
|
61
62
|
|
|
63
|
+
# Only when too few requests matched: with too many, declaring the body
|
|
64
|
+
# as a hash would match even more of them.
|
|
65
|
+
def json_body_hint
|
|
66
|
+
return "" unless WebMock.show_body_diff? && too_few_requests_executed?
|
|
67
|
+
|
|
68
|
+
requests = RequestRegistry.instance.requested_signatures.select do |request_signature, _|
|
|
69
|
+
JsonBodyHint.applies?(request_pattern, request_signature)
|
|
70
|
+
end
|
|
71
|
+
requests.empty? ? "" : "\n\n#{JsonBodyHint::EXPECTATION_HINT}"
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def too_few_requests_executed?
|
|
75
|
+
if @at_least_times_executed
|
|
76
|
+
times_executed < @at_least_times_executed
|
|
77
|
+
elsif @at_most_times_executed
|
|
78
|
+
false
|
|
79
|
+
else
|
|
80
|
+
times_executed < (@expected_times_executed || 1)
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
62
84
|
def quantity_phrase(is_negated=false)
|
|
63
85
|
if @at_least_times_executed
|
|
64
86
|
"at least #{times(@at_least_times_executed)} "
|
|
@@ -10,11 +10,15 @@ module WebMock
|
|
|
10
10
|
def rSpecHashExcludingMatcher?(matcher)
|
|
11
11
|
matcher.class.name =~ /R?Spec::Mocks::ArgumentMatchers::HashExcludingMatcher/
|
|
12
12
|
end
|
|
13
|
+
|
|
14
|
+
def rSpecAnyArgMatcher?(matcher)
|
|
15
|
+
matcher.class.name =~ /R?Spec::Mocks::ArgumentMatchers::AnyArgMatcher/
|
|
16
|
+
end
|
|
13
17
|
end
|
|
14
18
|
|
|
15
19
|
class RequestPattern
|
|
16
20
|
|
|
17
|
-
attr_reader :method_pattern, :uri_pattern, :body_pattern, :headers_pattern
|
|
21
|
+
attr_reader :method_pattern, :uri_pattern, :body_pattern, :headers_pattern, :with_block
|
|
18
22
|
|
|
19
23
|
def initialize(method, uri, options = {})
|
|
20
24
|
@method_pattern = MethodPattern.new(method)
|
|
@@ -33,11 +37,9 @@ module WebMock
|
|
|
33
37
|
end
|
|
34
38
|
|
|
35
39
|
def matches?(request_signature)
|
|
36
|
-
content_type = request_signature.headers['Content-Type'] if request_signature.headers
|
|
37
|
-
content_type = content_type.split(';').first if content_type
|
|
38
40
|
@method_pattern.matches?(request_signature.method) &&
|
|
39
41
|
@uri_pattern.matches?(request_signature.uri) &&
|
|
40
|
-
(@body_pattern.nil? || @body_pattern.matches?(request_signature.body, content_type
|
|
42
|
+
(@body_pattern.nil? || @body_pattern.matches?(request_signature.body, request_signature.content_type)) &&
|
|
41
43
|
(@headers_pattern.nil? || @headers_pattern.matches?(request_signature.headers)) &&
|
|
42
44
|
(@with_block.nil? || @with_block.call(request_signature))
|
|
43
45
|
end
|
|
@@ -96,8 +98,11 @@ module WebMock
|
|
|
96
98
|
|
|
97
99
|
|
|
98
100
|
class MethodPattern
|
|
101
|
+
# Request signatures store the method as a lower case Symbol, so a method
|
|
102
|
+
# given as a String or in upper case, like "get" or :GET, matches too.
|
|
103
|
+
# Anything else, such as nil, is kept as given and never matches.
|
|
99
104
|
def initialize(pattern)
|
|
100
|
-
@pattern = pattern
|
|
105
|
+
@pattern = pattern.respond_to?(:to_sym) ? pattern.to_sym.downcase : pattern
|
|
101
106
|
end
|
|
102
107
|
|
|
103
108
|
def matches?(method)
|
|
@@ -128,6 +133,10 @@ module WebMock
|
|
|
128
133
|
def add_query_params(query_params)
|
|
129
134
|
@query_params = if query_params.is_a?(Hash)
|
|
130
135
|
query_params
|
|
136
|
+
elsif query_params.equal?(:any) \
|
|
137
|
+
|| query_params.is_a?(WebMock::Matchers::AnyArgMatcher) \
|
|
138
|
+
|| rSpecAnyArgMatcher?(query_params)
|
|
139
|
+
WebMock::Matchers::AnyArgMatcher.new(nil)
|
|
131
140
|
elsif query_params.is_a?(WebMock::Matchers::HashIncludingMatcher) \
|
|
132
141
|
|| query_params.is_a?(WebMock::Matchers::HashExcludingMatcher)
|
|
133
142
|
query_params
|
|
@@ -135,8 +144,10 @@ module WebMock
|
|
|
135
144
|
WebMock::Matchers::HashIncludingMatcher.from_rspec_matcher(query_params)
|
|
136
145
|
elsif rSpecHashExcludingMatcher?(query_params)
|
|
137
146
|
WebMock::Matchers::HashExcludingMatcher.from_rspec_matcher(query_params)
|
|
138
|
-
|
|
147
|
+
elsif query_params.nil? || query_params.is_a?(String)
|
|
139
148
|
WebMock::Util::QueryMapper.query_to_values(query_params, notation: Config.instance.query_values_notation)
|
|
149
|
+
else
|
|
150
|
+
raise ArgumentError, "Query should be a String, Hash, :any, nil, or a hash_including or hash_excluding matcher. Got: #{query_params.class}"
|
|
140
151
|
end
|
|
141
152
|
end
|
|
142
153
|
|
|
@@ -152,6 +163,10 @@ module WebMock
|
|
|
152
163
|
|
|
153
164
|
private
|
|
154
165
|
|
|
166
|
+
def raise_any_query_with_uri_query
|
|
167
|
+
raise ArgumentError, "query: :any can't be combined with a query in the URI #{pattern_inspect}, because no request would match. Remove the query from the URI."
|
|
168
|
+
end
|
|
169
|
+
|
|
155
170
|
def pattern_inspect
|
|
156
171
|
@pattern.inspect
|
|
157
172
|
end
|
|
@@ -179,12 +194,17 @@ module WebMock
|
|
|
179
194
|
|
|
180
195
|
class URIAddressablePattern < URIPattern
|
|
181
196
|
def add_query_params(query_params)
|
|
197
|
+
super(query_params)
|
|
198
|
+
# With query params, the template is matched against the URI without its
|
|
199
|
+
# query, so a literal query in the template can never match.
|
|
200
|
+
if @query_params.is_a?(WebMock::Matchers::AnyArgMatcher) && @pattern.pattern.gsub(/\{[^}]*\}/, '').include?('?')
|
|
201
|
+
raise_any_query_with_uri_query
|
|
202
|
+
end
|
|
182
203
|
@@add_query_params_warned ||= false
|
|
183
204
|
if not @@add_query_params_warned
|
|
184
205
|
@@add_query_params_warned = true
|
|
185
206
|
warn "WebMock warning: ignoring query params in RFC 6570 template and checking them with WebMock"
|
|
186
207
|
end
|
|
187
|
-
super(query_params)
|
|
188
208
|
end
|
|
189
209
|
|
|
190
210
|
private
|
|
@@ -229,6 +249,9 @@ module WebMock
|
|
|
229
249
|
query_hash = (WebMock::Util::QueryMapper.query_to_values(@pattern.query, notation: Config.instance.query_values_notation) || {}).merge(@query_params)
|
|
230
250
|
@pattern.query = WebMock::Util::QueryMapper.values_to_query(query_hash, notation: WebMock::Config.instance.query_values_notation)
|
|
231
251
|
@query_params = nil
|
|
252
|
+
elsif @query_params.is_a?(WebMock::Matchers::AnyArgMatcher) && @pattern.query
|
|
253
|
+
# The URI is then compared without its query, so it could never match.
|
|
254
|
+
raise_any_query_with_uri_query
|
|
232
255
|
end
|
|
233
256
|
end
|
|
234
257
|
|
|
@@ -255,18 +278,8 @@ module WebMock
|
|
|
255
278
|
class BodyPattern
|
|
256
279
|
include RSpecMatcherDetector
|
|
257
280
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
'application/xml' => :xml,
|
|
261
|
-
'application/json' => :json,
|
|
262
|
-
'text/json' => :json,
|
|
263
|
-
'application/javascript' => :json,
|
|
264
|
-
'text/javascript' => :json,
|
|
265
|
-
'text/html' => :html,
|
|
266
|
-
'application/x-yaml' => :yaml,
|
|
267
|
-
'text/yaml' => :yaml,
|
|
268
|
-
'text/plain' => :plain
|
|
269
|
-
}
|
|
281
|
+
# Kept so existing references to the constant still resolve.
|
|
282
|
+
BODY_FORMATS = WebMock::Util::ContentType::BODY_FORMATS
|
|
270
283
|
|
|
271
284
|
attr_reader :pattern
|
|
272
285
|
|
|
@@ -275,12 +288,16 @@ module WebMock
|
|
|
275
288
|
normalize_hash(pattern)
|
|
276
289
|
elsif rSpecHashIncludingMatcher?(pattern)
|
|
277
290
|
WebMock::Matchers::HashIncludingMatcher.from_rspec_matcher(pattern)
|
|
291
|
+
elsif rSpecHashExcludingMatcher?(pattern)
|
|
292
|
+
WebMock::Matchers::HashExcludingMatcher.from_rspec_matcher(pattern)
|
|
278
293
|
else
|
|
279
294
|
pattern
|
|
280
295
|
end
|
|
281
296
|
end
|
|
282
297
|
|
|
283
298
|
def matches?(body, content_type = "")
|
|
299
|
+
# Checked on the raw value, so body patterns used with mixed case
|
|
300
|
+
# multipart content types keep working as in earlier versions.
|
|
284
301
|
assert_non_multipart_body(content_type)
|
|
285
302
|
|
|
286
303
|
if (@pattern).is_a?(Hash)
|
|
@@ -304,7 +321,9 @@ module WebMock
|
|
|
304
321
|
private
|
|
305
322
|
|
|
306
323
|
def body_as_hash(body, content_type)
|
|
307
|
-
|
|
324
|
+
return if body.nil?
|
|
325
|
+
|
|
326
|
+
case WebMock::Util::ContentType.body_format(content_type)
|
|
308
327
|
when :json then
|
|
309
328
|
WebMock::Util::Parsers::JSON.parse(body)
|
|
310
329
|
when :xml then
|
|
@@ -316,11 +335,6 @@ module WebMock
|
|
|
316
335
|
nil
|
|
317
336
|
end
|
|
318
337
|
|
|
319
|
-
def body_format(content_type)
|
|
320
|
-
normalized_content_type = content_type.sub(/\A(application\/)[a-zA-Z0-9.-]+\+(json|xml)\Z/,'\1\2')
|
|
321
|
-
BODY_FORMATS[normalized_content_type]
|
|
322
|
-
end
|
|
323
|
-
|
|
324
338
|
def assert_non_multipart_body(content_type)
|
|
325
339
|
if content_type =~ %r{^multipart/form-data}
|
|
326
340
|
raise ArgumentError.new("WebMock does not support matching body for multipart/form-data requests yet :(")
|
|
@@ -376,7 +390,7 @@ module WebMock
|
|
|
376
390
|
return matching_body_hashes?(actual, expected, content_type) if actual.is_a?(Hash) && expected.is_a?(Hash)
|
|
377
391
|
return matching_body_array?(actual, expected, content_type) if actual.is_a?(Array) && expected.is_a?(Array)
|
|
378
392
|
|
|
379
|
-
expected = WebMock::Util::ValuesStringifier.stringify_values(expected) if
|
|
393
|
+
expected = WebMock::Util::ValuesStringifier.stringify_values(expected) if WebMock::Util::ContentType.url_encoded?(content_type)
|
|
380
394
|
expected === actual
|
|
381
395
|
end
|
|
382
396
|
|
|
@@ -387,10 +401,6 @@ module WebMock
|
|
|
387
401
|
def normalize_hash(hash)
|
|
388
402
|
Hash[WebMock::Util::HashKeysStringifier.stringify_keys!(hash, deep: true).sort]
|
|
389
403
|
end
|
|
390
|
-
|
|
391
|
-
def url_encoded_body?(content_type)
|
|
392
|
-
content_type =~ %r{^application/x-www-form-urlencoded}
|
|
393
|
-
end
|
|
394
404
|
end
|
|
395
405
|
|
|
396
406
|
class HeadersPattern
|
|
@@ -4,11 +4,11 @@ module WebMock
|
|
|
4
4
|
|
|
5
5
|
class RequestSignature
|
|
6
6
|
|
|
7
|
-
attr_accessor :
|
|
8
|
-
attr_reader :headers
|
|
7
|
+
attr_accessor :uri, :body
|
|
8
|
+
attr_reader :method, :headers
|
|
9
9
|
|
|
10
10
|
def initialize(method, uri, options = {})
|
|
11
|
-
self.method = method
|
|
11
|
+
self.method = method
|
|
12
12
|
self.uri = uri.is_a?(Addressable::URI) ? uri : WebMock::Util::URI.normalize_uri(uri)
|
|
13
13
|
assign_options(options)
|
|
14
14
|
end
|
|
@@ -23,6 +23,10 @@ module WebMock
|
|
|
23
23
|
string
|
|
24
24
|
end
|
|
25
25
|
|
|
26
|
+
def method=(method)
|
|
27
|
+
@method = method.to_sym.downcase
|
|
28
|
+
end
|
|
29
|
+
|
|
26
30
|
def headers=(headers)
|
|
27
31
|
@headers = WebMock::Util::Headers.normalize_headers(headers)
|
|
28
32
|
end
|
|
@@ -36,12 +40,18 @@ module WebMock
|
|
|
36
40
|
end
|
|
37
41
|
alias == eql?
|
|
38
42
|
|
|
43
|
+
# A request can carry Content-Type more than once, in which case the
|
|
44
|
+
# normalized header value is an Array. Use its first value.
|
|
45
|
+
def content_type
|
|
46
|
+
Array(headers&.fetch('Content-Type', nil)).first
|
|
47
|
+
end
|
|
48
|
+
|
|
39
49
|
def url_encoded?
|
|
40
|
-
|
|
50
|
+
WebMock::Util::ContentType.url_encoded?(content_type)
|
|
41
51
|
end
|
|
42
52
|
|
|
43
53
|
def json_headers?
|
|
44
|
-
|
|
54
|
+
WebMock::Util::ContentType.body_format(content_type) == :json
|
|
45
55
|
end
|
|
46
56
|
|
|
47
57
|
private
|
|
@@ -25,7 +25,10 @@ module WebMock
|
|
|
25
25
|
text = "registered request stubs:\n".dup
|
|
26
26
|
WebMock::StubRegistry.instance.request_stubs.each do |stub|
|
|
27
27
|
text << "\n#{WebMock::StubRequestSnippet.new(stub).to_s(false)}"
|
|
28
|
-
|
|
28
|
+
if WebMock.show_body_diff?
|
|
29
|
+
add_body_diff(stub, text)
|
|
30
|
+
add_json_body_hint(stub, text)
|
|
31
|
+
end
|
|
29
32
|
end
|
|
30
33
|
text
|
|
31
34
|
end
|
|
@@ -37,20 +40,17 @@ module WebMock
|
|
|
37
40
|
text << "\n\n#{body_diff_str}" unless body_diff_str.empty?
|
|
38
41
|
end
|
|
39
42
|
|
|
43
|
+
def add_json_body_hint(stub, text)
|
|
44
|
+
return unless JsonBodyHint.applies?(stub.request_pattern, request_signature)
|
|
45
|
+
|
|
46
|
+
text << "\n\n#{JsonBodyHint::STUB_HINT}\n"
|
|
47
|
+
end
|
|
48
|
+
|
|
40
49
|
def signature_stub_body_diff(stub)
|
|
41
50
|
diff = RequestBodyDiff.new(request_signature, stub).body_diff
|
|
42
51
|
diff.empty? ? "" : "Body diff:\n #{pretty_print_to_string(diff)}"
|
|
43
52
|
end
|
|
44
53
|
|
|
45
|
-
def request_params
|
|
46
|
-
@request_params ||=
|
|
47
|
-
if request_signature.json_headers?
|
|
48
|
-
JSON.parse(request_signature.body)
|
|
49
|
-
else
|
|
50
|
-
""
|
|
51
|
-
end
|
|
52
|
-
end
|
|
53
|
-
|
|
54
54
|
def pretty_print_to_string(string_to_print)
|
|
55
55
|
StringIO.open("".dup) do |stream|
|
|
56
56
|
PP.pp(string_to_print, stream)
|
data/lib/webmock/request_stub.rb
CHANGED
|
@@ -39,13 +39,10 @@ module WebMock
|
|
|
39
39
|
else
|
|
40
40
|
body.call
|
|
41
41
|
end
|
|
42
|
-
b
|
|
43
|
-
b
|
|
42
|
+
json_response_body(b)
|
|
44
43
|
}
|
|
45
|
-
elsif !body.is_a?(String)
|
|
46
|
-
body.to_json
|
|
47
44
|
else
|
|
48
|
-
body
|
|
45
|
+
json_response_body(body)
|
|
49
46
|
end
|
|
50
47
|
|
|
51
48
|
resp_h.merge(
|
|
@@ -130,5 +127,16 @@ module WebMock
|
|
|
130
127
|
end
|
|
131
128
|
stub
|
|
132
129
|
end
|
|
130
|
+
|
|
131
|
+
private
|
|
132
|
+
|
|
133
|
+
def json_response_body(body)
|
|
134
|
+
case body
|
|
135
|
+
when String, IO, Pathname
|
|
136
|
+
body
|
|
137
|
+
else
|
|
138
|
+
body.to_json
|
|
139
|
+
end
|
|
140
|
+
end
|
|
133
141
|
end
|
|
134
142
|
end
|
|
@@ -52,13 +52,22 @@ module WebMock
|
|
|
52
52
|
self
|
|
53
53
|
end
|
|
54
54
|
|
|
55
|
+
# Counts only the requests that also match the given conditions, which
|
|
56
|
+
# take the same options and block as RequestPattern#with. Like
|
|
57
|
+
# RequestPattern#with, calling it again adds to the conditions. The stub
|
|
58
|
+
# or request pattern under test is not changed.
|
|
59
|
+
def with(options = {}, &block)
|
|
60
|
+
(@conditions ||= RequestPattern.new(:any, ANY_URI)).with(options, &block)
|
|
61
|
+
self
|
|
62
|
+
end
|
|
63
|
+
|
|
55
64
|
def matches?(request_pattern)
|
|
56
|
-
@request_execution_verifier.request_pattern = request_pattern
|
|
65
|
+
@request_execution_verifier.request_pattern = with_conditions(request_pattern)
|
|
57
66
|
@request_execution_verifier.matches?
|
|
58
67
|
end
|
|
59
68
|
|
|
60
69
|
def does_not_match?(request_pattern)
|
|
61
|
-
@request_execution_verifier.request_pattern = request_pattern
|
|
70
|
+
@request_execution_verifier.request_pattern = with_conditions(request_pattern)
|
|
62
71
|
@request_execution_verifier.does_not_match?
|
|
63
72
|
end
|
|
64
73
|
|
|
@@ -76,5 +85,41 @@ module WebMock
|
|
|
76
85
|
|
|
77
86
|
# RSpec 2 compatibility:
|
|
78
87
|
alias_method :negative_failure_message, :failure_message_when_negated
|
|
88
|
+
|
|
89
|
+
private
|
|
90
|
+
|
|
91
|
+
ANY_URI = ->(_uri) { true }
|
|
92
|
+
private_constant :ANY_URI
|
|
93
|
+
|
|
94
|
+
# How the description of conditions starts when nothing was given to
|
|
95
|
+
# #with. What was given follows it, as in ' with body "abc"'.
|
|
96
|
+
EMPTY_CONDITIONS = RequestPattern.new(:any, ANY_URI).to_s.freeze
|
|
97
|
+
private_constant :EMPTY_CONDITIONS
|
|
98
|
+
|
|
99
|
+
def with_conditions(request_pattern)
|
|
100
|
+
@conditions ? PatternWithConditions.new(request_pattern, @conditions) : request_pattern
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Matches a request when both the pattern and the conditions match it.
|
|
104
|
+
class PatternWithConditions
|
|
105
|
+
def initialize(pattern, conditions)
|
|
106
|
+
@pattern = pattern
|
|
107
|
+
@conditions = conditions
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# The parts that must all match, so JsonBodyHint can check each of them.
|
|
111
|
+
def patterns
|
|
112
|
+
[@pattern, @conditions]
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def matches?(request_signature)
|
|
116
|
+
@pattern.matches?(request_signature) && @conditions.matches?(request_signature)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def to_s
|
|
120
|
+
"#{@pattern} (only requests#{@conditions.to_s.delete_prefix(EMPTY_CONDITIONS)})"
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
private_constant :PatternWithConditions
|
|
79
124
|
end
|
|
80
125
|
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module WebMock
|
|
4
|
+
module Util
|
|
5
|
+
module ContentType
|
|
6
|
+
BODY_FORMATS = {
|
|
7
|
+
'text/xml' => :xml,
|
|
8
|
+
'application/xml' => :xml,
|
|
9
|
+
'application/json' => :json,
|
|
10
|
+
'text/json' => :json,
|
|
11
|
+
'application/javascript' => :json,
|
|
12
|
+
'text/javascript' => :json,
|
|
13
|
+
'text/html' => :html,
|
|
14
|
+
'application/x-yaml' => :yaml,
|
|
15
|
+
'text/yaml' => :yaml,
|
|
16
|
+
'text/plain' => :plain
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
def self.media_type(content_type)
|
|
20
|
+
content_type.to_s.scrub.split(';', 2).first.to_s.strip.downcase
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def self.url_encoded?(content_type)
|
|
24
|
+
media_type(content_type) == 'application/x-www-form-urlencoded'
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def self.body_format(content_type)
|
|
28
|
+
type = media_type(content_type)
|
|
29
|
+
|
|
30
|
+
BODY_FORMATS.fetch(type) do
|
|
31
|
+
case type
|
|
32
|
+
when %r{\A[^/\s]+/[^/\s]+\+json\z} then :json
|
|
33
|
+
when %r{\A[^/\s]+/[^/\s]+\+xml\z} then :xml
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
data/lib/webmock/util/uri.rb
CHANGED
|
@@ -58,8 +58,8 @@ module WebMock
|
|
|
58
58
|
|
|
59
59
|
def self.strip_default_port_from_uri_string(uri_string)
|
|
60
60
|
case uri_string
|
|
61
|
-
when %r{^http://} then uri_string.sub(%r{:80(
|
|
62
|
-
when %r{^https://} then uri_string.sub(%r{:443(
|
|
61
|
+
when %r{^http://} then uri_string.sub(%r{\A(http://[^/?#]+):80(?=[/?#]|\z)}, '\1')
|
|
62
|
+
when %r{^https://} then uri_string.sub(%r{\A(https://[^/?#]+):443(?=[/?#]|\z)}, '\1')
|
|
63
63
|
else uri_string
|
|
64
64
|
end
|
|
65
65
|
end
|
data/lib/webmock/version.rb
CHANGED