webmock 3.0.1 → 3.18.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (90) hide show
  1. checksums.yaml +5 -5
  2. data/.github/workflows/CI.yml +38 -0
  3. data/CHANGELOG.md +496 -2
  4. data/Gemfile +1 -1
  5. data/README.md +169 -34
  6. data/Rakefile +12 -4
  7. data/lib/webmock/api.rb +12 -0
  8. data/lib/webmock/http_lib_adapters/async_http_client_adapter.rb +221 -0
  9. data/lib/webmock/http_lib_adapters/curb_adapter.rb +19 -5
  10. data/lib/webmock/http_lib_adapters/em_http_request_adapter.rb +7 -4
  11. data/lib/webmock/http_lib_adapters/excon_adapter.rb +5 -2
  12. data/lib/webmock/http_lib_adapters/http_rb/client.rb +2 -1
  13. data/lib/webmock/http_lib_adapters/http_rb/request.rb +7 -1
  14. data/lib/webmock/http_lib_adapters/http_rb/response.rb +27 -3
  15. data/lib/webmock/http_lib_adapters/http_rb/streamer.rb +9 -3
  16. data/lib/webmock/http_lib_adapters/http_rb/webmock.rb +7 -3
  17. data/lib/webmock/http_lib_adapters/httpclient_adapter.rb +28 -9
  18. data/lib/webmock/http_lib_adapters/manticore_adapter.rb +33 -15
  19. data/lib/webmock/http_lib_adapters/net_http.rb +36 -89
  20. data/lib/webmock/http_lib_adapters/net_http_response.rb +1 -1
  21. data/lib/webmock/http_lib_adapters/patron_adapter.rb +4 -4
  22. data/lib/webmock/matchers/any_arg_matcher.rb +13 -0
  23. data/lib/webmock/matchers/hash_argument_matcher.rb +21 -0
  24. data/lib/webmock/matchers/hash_excluding_matcher.rb +15 -0
  25. data/lib/webmock/matchers/hash_including_matcher.rb +4 -23
  26. data/lib/webmock/rack_response.rb +1 -1
  27. data/lib/webmock/request_body_diff.rb +1 -1
  28. data/lib/webmock/request_execution_verifier.rb +2 -3
  29. data/lib/webmock/request_pattern.rb +129 -51
  30. data/lib/webmock/request_registry.rb +1 -1
  31. data/lib/webmock/request_signature.rb +3 -3
  32. data/lib/webmock/request_signature_snippet.rb +4 -4
  33. data/lib/webmock/request_stub.rb +15 -0
  34. data/lib/webmock/response.rb +19 -13
  35. data/lib/webmock/rspec.rb +10 -3
  36. data/lib/webmock/stub_registry.rb +26 -11
  37. data/lib/webmock/stub_request_snippet.rb +10 -6
  38. data/lib/webmock/test_unit.rb +1 -3
  39. data/lib/webmock/util/hash_counter.rb +3 -3
  40. data/lib/webmock/util/headers.rb +17 -2
  41. data/lib/webmock/util/json.rb +1 -2
  42. data/lib/webmock/util/query_mapper.rb +9 -7
  43. data/lib/webmock/util/uri.rb +10 -10
  44. data/lib/webmock/util/values_stringifier.rb +20 -0
  45. data/lib/webmock/version.rb +1 -1
  46. data/lib/webmock/webmock.rb +20 -3
  47. data/lib/webmock.rb +53 -48
  48. data/minitest/webmock_spec.rb +3 -3
  49. data/spec/acceptance/async_http_client/async_http_client_spec.rb +375 -0
  50. data/spec/acceptance/async_http_client/async_http_client_spec_helper.rb +73 -0
  51. data/spec/acceptance/curb/curb_spec.rb +44 -0
  52. data/spec/acceptance/em_http_request/em_http_request_spec.rb +57 -1
  53. data/spec/acceptance/em_http_request/em_http_request_spec_helper.rb +2 -2
  54. data/spec/acceptance/excon/excon_spec.rb +4 -2
  55. data/spec/acceptance/excon/excon_spec_helper.rb +2 -0
  56. data/spec/acceptance/http_rb/http_rb_spec.rb +20 -0
  57. data/spec/acceptance/http_rb/http_rb_spec_helper.rb +5 -2
  58. data/spec/acceptance/httpclient/httpclient_spec.rb +8 -1
  59. data/spec/acceptance/manticore/manticore_spec.rb +51 -0
  60. data/spec/acceptance/net_http/net_http_shared.rb +47 -10
  61. data/spec/acceptance/net_http/net_http_spec.rb +102 -24
  62. data/spec/acceptance/net_http/real_net_http_spec.rb +1 -1
  63. data/spec/acceptance/patron/patron_spec.rb +26 -21
  64. data/spec/acceptance/patron/patron_spec_helper.rb +3 -3
  65. data/spec/acceptance/shared/allowing_and_disabling_net_connect.rb +14 -14
  66. data/spec/acceptance/shared/callbacks.rb +3 -2
  67. data/spec/acceptance/shared/complex_cross_concern_behaviors.rb +1 -1
  68. data/spec/acceptance/shared/request_expectations.rb +14 -0
  69. data/spec/acceptance/shared/returning_declared_responses.rb +36 -15
  70. data/spec/acceptance/shared/stubbing_requests.rb +95 -0
  71. data/spec/acceptance/typhoeus/typhoeus_hydra_spec.rb +1 -1
  72. data/spec/acceptance/typhoeus/typhoeus_hydra_spec_helper.rb +1 -1
  73. data/spec/support/webmock_server.rb +1 -0
  74. data/spec/unit/api_spec.rb +103 -3
  75. data/spec/unit/matchers/hash_excluding_matcher_spec.rb +61 -0
  76. data/spec/unit/request_execution_verifier_spec.rb +12 -12
  77. data/spec/unit/request_pattern_spec.rb +207 -49
  78. data/spec/unit/request_signature_snippet_spec.rb +2 -2
  79. data/spec/unit/request_signature_spec.rb +21 -1
  80. data/spec/unit/request_stub_spec.rb +35 -0
  81. data/spec/unit/response_spec.rb +51 -19
  82. data/spec/unit/stub_request_snippet_spec.rb +30 -10
  83. data/spec/unit/util/query_mapper_spec.rb +13 -0
  84. data/spec/unit/util/uri_spec.rb +74 -2
  85. data/spec/unit/webmock_spec.rb +108 -5
  86. data/test/shared_test.rb +15 -2
  87. data/test/test_webmock.rb +6 -0
  88. data/webmock.gemspec +15 -7
  89. metadata +86 -37
  90. data/.travis.yml +0 -20
@@ -0,0 +1,21 @@
1
+ module WebMock
2
+ module Matchers
3
+ # Base class for Hash matchers
4
+ # https://github.com/rspec/rspec-mocks/blob/master/lib/rspec/mocks/argument_matchers.rb
5
+ class HashArgumentMatcher
6
+ def initialize(expected)
7
+ @expected = Hash[WebMock::Util::HashKeysStringifier.stringify_keys!(expected, deep: true).sort]
8
+ end
9
+
10
+ def ==(_actual, &block)
11
+ @expected.all?(&block)
12
+ rescue NoMethodError
13
+ false
14
+ end
15
+
16
+ def self.from_rspec_matcher(matcher)
17
+ new(matcher.instance_variable_get(:@expected))
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,15 @@
1
+ module WebMock
2
+ module Matchers
3
+ # this is a based on RSpec::Mocks::ArgumentMatchers::HashExcludingMatcher
4
+ # https://github.com/rspec/rspec-mocks/blob/master/lib/rspec/mocks/argument_matchers.rb
5
+ class HashExcludingMatcher < HashArgumentMatcher
6
+ def ==(actual)
7
+ super { |key, value| !actual.key?(key) || value != actual[key] }
8
+ end
9
+
10
+ def inspect
11
+ "hash_excluding(#{@expected.inspect})"
12
+ end
13
+ end
14
+ end
15
+ end
@@ -1,14 +1,10 @@
1
1
  module WebMock
2
2
  module Matchers
3
- #this is a based on RSpec::Mocks::ArgumentMatchers::HashIncludingMatcher
4
- #https://github.com/rspec/rspec-mocks/blob/master/lib/rspec/mocks/argument_matchers.rb
5
- class HashIncludingMatcher
6
- def initialize(expected)
7
- @expected = Hash[WebMock::Util::HashKeysStringifier.stringify_keys!(expected, deep: true).sort]
8
- end
9
-
3
+ # this is a based on RSpec::Mocks::ArgumentMatchers::HashIncludingMatcher
4
+ # https://github.com/rspec/rspec-mocks/blob/master/lib/rspec/mocks/argument_matchers.rb
5
+ class HashIncludingMatcher < HashArgumentMatcher
10
6
  def ==(actual)
11
- @expected.all? {|k,v| actual.has_key?(k) && v === actual[k]}
7
+ super { |key, value| actual.key?(key) && value === actual[key] }
12
8
  rescue NoMethodError
13
9
  false
14
10
  end
@@ -16,21 +12,6 @@ module WebMock
16
12
  def inspect
17
13
  "hash_including(#{@expected.inspect})"
18
14
  end
19
-
20
- def self.from_rspec_matcher(matcher)
21
- new(matcher.instance_variable_get(:@expected))
22
- end
23
- end
24
-
25
- #this is a based on RSpec::Mocks::ArgumentMatchers::AnyArgMatcher
26
- class AnyArgMatcher
27
- def initialize(ignore)
28
- end
29
-
30
- def ==(other)
31
- true
32
- end
33
15
  end
34
-
35
16
  end
36
17
  end
@@ -17,7 +17,7 @@ module WebMock
17
17
  end
18
18
 
19
19
  def body_from_rack_response(response)
20
- body = ""
20
+ body = "".dup
21
21
  response.each { |line| body << line }
22
22
  response.close if response.respond_to?(:close)
23
23
  return body
@@ -12,7 +12,7 @@ module WebMock
12
12
  def body_diff
13
13
  return {} unless request_signature_diffable? && request_stub_diffable?
14
14
 
15
- HashDiff.diff(request_signature_body_hash, request_stub_body_hash)
15
+ Hashdiff.diff(request_signature_body_hash, request_stub_body_hash)
16
16
  end
17
17
 
18
18
  attr_reader :request_signature, :request_stub
@@ -53,9 +53,8 @@ module WebMock
53
53
 
54
54
  def failure_message_phrase(is_negated=false)
55
55
  negation = is_negated ? "was not" : "was"
56
- text = "The request #{request_pattern} #{negation} expected to execute #{quantity_phrase(is_negated)}but it executed #{times(times_executed)}"
57
- text << self.class.executed_requests_message
58
- text
56
+ "The request #{request_pattern} #{negation} expected to execute #{quantity_phrase(is_negated)}but it executed #{times(times_executed)}" +
57
+ self.class.executed_requests_message
59
58
  end
60
59
 
61
60
  def quantity_phrase(is_negated=false)
@@ -4,6 +4,10 @@ module WebMock
4
4
  def rSpecHashIncludingMatcher?(matcher)
5
5
  matcher.class.name =~ /R?Spec::Mocks::ArgumentMatchers::HashIncludingMatcher/
6
6
  end
7
+
8
+ def rSpecHashExcludingMatcher?(matcher)
9
+ matcher.class.name =~ /R?Spec::Mocks::ArgumentMatchers::HashExcludingMatcher/
10
+ end
7
11
  end
8
12
 
9
13
  class RequestPattern
@@ -20,7 +24,7 @@ module WebMock
20
24
  end
21
25
 
22
26
  def with(options = {}, &block)
23
- raise ArgumentError.new('#with method invoked with no arguments. Either options hash or block must be specified.') if options.empty? && !block_given?
27
+ raise ArgumentError.new('#with method invoked with no arguments. Either options hash or block must be specified. Created a block with do..end? Try creating it with curly braces {} instead.') if options.empty? && !block_given?
24
28
  assign_options(options)
25
29
  @with_block = block
26
30
  self
@@ -37,7 +41,7 @@ module WebMock
37
41
  end
38
42
 
39
43
  def to_s
40
- string = "#{@method_pattern.to_s.upcase}"
44
+ string = "#{@method_pattern.to_s.upcase}".dup
41
45
  string << " #{@uri_pattern.to_s}"
42
46
  string << " with body #{@body_pattern.to_s}" if @body_pattern
43
47
  string << " with headers #{@headers_pattern.to_s}" if @headers_pattern
@@ -76,6 +80,8 @@ module WebMock
76
80
  URIRegexpPattern.new(uri)
77
81
  elsif uri.is_a?(Addressable::Template)
78
82
  URIAddressablePattern.new(uri)
83
+ elsif uri.respond_to?(:call)
84
+ URICallablePattern.new(uri)
79
85
  else
80
86
  URIStringPattern.new(uri)
81
87
  end
@@ -103,11 +109,13 @@ module WebMock
103
109
  include RSpecMatcherDetector
104
110
 
105
111
  def initialize(pattern)
106
- @pattern = case pattern
107
- when Addressable::URI, Addressable::Template
112
+ @pattern = if pattern.is_a?(Addressable::URI) \
113
+ || pattern.is_a?(Addressable::Template)
114
+ pattern
115
+ elsif pattern.respond_to?(:call)
108
116
  pattern
109
117
  else
110
- WebMock::Util::URI.normalize_uri(pattern)
118
+ WebMock::Util::URI.normalize_uri(pattern)
111
119
  end
112
120
  @query_params = nil
113
121
  end
@@ -115,65 +123,116 @@ module WebMock
115
123
  def add_query_params(query_params)
116
124
  @query_params = if query_params.is_a?(Hash)
117
125
  query_params
118
- elsif query_params.is_a?(WebMock::Matchers::HashIncludingMatcher)
126
+ elsif query_params.is_a?(WebMock::Matchers::HashIncludingMatcher) \
127
+ || query_params.is_a?(WebMock::Matchers::HashExcludingMatcher)
119
128
  query_params
120
129
  elsif rSpecHashIncludingMatcher?(query_params)
121
130
  WebMock::Matchers::HashIncludingMatcher.from_rspec_matcher(query_params)
131
+ elsif rSpecHashExcludingMatcher?(query_params)
132
+ WebMock::Matchers::HashExcludingMatcher.from_rspec_matcher(query_params)
122
133
  else
123
134
  WebMock::Util::QueryMapper.query_to_values(query_params, notation: Config.instance.query_values_notation)
124
135
  end
125
136
  end
126
137
 
138
+ def matches?(uri)
139
+ pattern_matches?(uri) && query_params_matches?(uri)
140
+ end
141
+
127
142
  def to_s
128
- str = @pattern.inspect
143
+ str = pattern_inspect
129
144
  str += " with query params #{@query_params.inspect}" if @query_params
130
145
  str
131
146
  end
147
+
148
+ private
149
+
150
+ def pattern_inspect
151
+ @pattern.inspect
152
+ end
153
+
154
+ def query_params_matches?(uri)
155
+ @query_params.nil? || @query_params == WebMock::Util::QueryMapper.query_to_values(uri.query, notation: Config.instance.query_values_notation)
156
+ end
132
157
  end
133
158
 
134
- class URIRegexpPattern < URIPattern
135
- def matches?(uri)
136
- WebMock::Util::URI.variations_of_uri_as_strings(uri).any? { |u| u.match(@pattern) } &&
137
- (@query_params.nil? || @query_params == WebMock::Util::QueryMapper.query_to_values(uri.query, notation: Config.instance.query_values_notation))
159
+ class URICallablePattern < URIPattern
160
+ private
161
+
162
+ def pattern_matches?(uri)
163
+ @pattern.call(uri)
138
164
  end
165
+ end
139
166
 
140
- def to_s
141
- str = @pattern.inspect
142
- str += " with query params #{@query_params.inspect}" if @query_params
143
- str
167
+ class URIRegexpPattern < URIPattern
168
+ private
169
+
170
+ def pattern_matches?(uri)
171
+ WebMock::Util::URI.variations_of_uri_as_strings(uri).any? { |u| u.match(@pattern) }
144
172
  end
145
173
  end
146
174
 
147
175
  class URIAddressablePattern < URIPattern
148
- def matches?(uri)
176
+ def add_query_params(query_params)
177
+ @@add_query_params_warned ||= false
178
+ if not @@add_query_params_warned
179
+ @@add_query_params_warned = true
180
+ warn "WebMock warning: ignoring query params in RFC 6570 template and checking them with WebMock"
181
+ end
182
+ super(query_params)
183
+ end
184
+
185
+ private
186
+
187
+ def pattern_matches?(uri)
149
188
  if @query_params.nil?
150
189
  # Let Addressable check the whole URI
151
- WebMock::Util::URI.variations_of_uri_as_strings(uri).any? { |u| @pattern.match(u) }
190
+ matches_with_variations?(uri)
152
191
  else
153
192
  # WebMock checks the query, Addressable checks everything else
154
- WebMock::Util::URI.variations_of_uri_as_strings(uri.omit(:query)).any? { |u| @pattern.match(u) } &&
155
- @query_params == WebMock::Util::QueryMapper.query_to_values(uri.query)
193
+ matches_with_variations?(uri.omit(:query))
156
194
  end
157
195
  end
158
196
 
159
- def add_query_params(query_params)
160
- warn "WebMock warning: ignoring query params in RFC 6570 template and checking them with WebMock"
161
- super(query_params)
197
+ def pattern_inspect
198
+ @pattern.pattern.inspect
162
199
  end
163
200
 
164
- def to_s
165
- str = @pattern.pattern.inspect
166
- str += " with variables #{@pattern.variables.inspect}" if @pattern.variables
167
- str
201
+ def matches_with_variations?(uri)
202
+ template =
203
+ begin
204
+ Addressable::Template.new(WebMock::Util::URI.heuristic_parse(@pattern.pattern))
205
+ rescue Addressable::URI::InvalidURIError
206
+ Addressable::Template.new(@pattern.pattern)
207
+ end
208
+ WebMock::Util::URI.variations_of_uri_as_strings(uri).any? { |u|
209
+ template_matches_uri?(template, u)
210
+ }
211
+ end
212
+
213
+ def template_matches_uri?(template, uri)
214
+ template.match(uri)
215
+ rescue Addressable::URI::InvalidURIError
216
+ false
168
217
  end
169
218
  end
170
219
 
171
220
  class URIStringPattern < URIPattern
172
- def matches?(uri)
221
+ def add_query_params(query_params)
222
+ super
223
+ if @query_params.is_a?(Hash) || @query_params.is_a?(String)
224
+ query_hash = (WebMock::Util::QueryMapper.query_to_values(@pattern.query, notation: Config.instance.query_values_notation) || {}).merge(@query_params)
225
+ @pattern.query = WebMock::Util::QueryMapper.values_to_query(query_hash, notation: WebMock::Config.instance.query_values_notation)
226
+ @query_params = nil
227
+ end
228
+ end
229
+
230
+ private
231
+
232
+ def pattern_matches?(uri)
173
233
  if @pattern.is_a?(Addressable::URI)
174
234
  if @query_params
175
- uri.omit(:query) === @pattern &&
176
- (@query_params.nil? || @query_params == WebMock::Util::QueryMapper.query_to_values(uri.query, notation: Config.instance.query_values_notation))
235
+ uri.omit(:query) === @pattern
177
236
  else
178
237
  uri === @pattern
179
238
  end
@@ -182,19 +241,8 @@ module WebMock
182
241
  end
183
242
  end
184
243
 
185
- def add_query_params(query_params)
186
- super
187
- if @query_params.is_a?(Hash) || @query_params.is_a?(String)
188
- query_hash = (WebMock::Util::QueryMapper.query_to_values(@pattern.query, notation: Config.instance.query_values_notation) || {}).merge(@query_params)
189
- @pattern.query = WebMock::Util::QueryMapper.values_to_query(query_hash, notation: WebMock::Config.instance.query_values_notation)
190
- @query_params = nil
191
- end
192
- end
193
-
194
- def to_s
195
- str = WebMock::Util::URI.strip_default_port_from_uri_string(@pattern.to_s)
196
- str += " with query params #{@query_params.inspect}" if @query_params
197
- str
244
+ def pattern_inspect
245
+ WebMock::Util::URI.strip_default_port_from_uri_string(@pattern.to_s)
198
246
  end
199
247
  end
200
248
 
@@ -232,7 +280,9 @@ module WebMock
232
280
 
233
281
  if (@pattern).is_a?(Hash)
234
282
  return true if @pattern.empty?
235
- matching_hashes?(body_as_hash(body, content_type), @pattern)
283
+ matching_body_hashes?(body_as_hash(body, content_type), @pattern, content_type)
284
+ elsif (@pattern).is_a?(Array)
285
+ matching_body_array?(body_as_hash(body, content_type), @pattern, content_type)
236
286
  elsif (@pattern).is_a?(WebMock::Matchers::HashIncludingMatcher)
237
287
  @pattern == body_as_hash(body, content_type)
238
288
  else
@@ -247,8 +297,9 @@ module WebMock
247
297
  end
248
298
 
249
299
  private
300
+
250
301
  def body_as_hash(body, content_type)
251
- case BODY_FORMATS[content_type]
302
+ case body_format(content_type)
252
303
  when :json then
253
304
  WebMock::Util::JSON.parse(body)
254
305
  when :xml then
@@ -258,6 +309,11 @@ module WebMock
258
309
  end
259
310
  end
260
311
 
312
+ def body_format(content_type)
313
+ normalized_content_type = content_type.sub(/\A(application\/)[a-zA-Z0-9.-]+\+(json|xml)\Z/,'\1\2')
314
+ BODY_FORMATS[normalized_content_type]
315
+ end
316
+
261
317
  def assert_non_multipart_body(content_type)
262
318
  if content_type =~ %r{^multipart/form-data}
263
319
  raise ArgumentError.new("WebMock does not support matching body for multipart/form-data requests yet :(")
@@ -287,21 +343,36 @@ module WebMock
287
343
  #
288
344
  # @return [Boolean] true if the paramaters match the comparison
289
345
  # hash, false if not.
290
- def matching_hashes?(query_parameters, pattern)
346
+ def matching_body_hashes?(query_parameters, pattern, content_type)
291
347
  return false unless query_parameters.is_a?(Hash)
292
348
  return false unless query_parameters.keys.sort == pattern.keys.sort
293
- query_parameters.each do |key, actual|
349
+
350
+ query_parameters.all? do |key, actual|
294
351
  expected = pattern[key]
352
+ matching_values(actual, expected, content_type)
353
+ end
354
+ end
295
355
 
296
- if actual.is_a?(Hash) && expected.is_a?(Hash)
297
- return false unless matching_hashes?(actual, expected)
298
- else
299
- return false unless expected === actual
300
- end
356
+ def matching_body_array?(query_parameters, pattern, content_type)
357
+ return false unless query_parameters.is_a?(Array)
358
+ return false unless query_parameters.length == pattern.length
359
+
360
+ query_parameters.each_with_index do |actual, index|
361
+ expected = pattern[index]
362
+ return false unless matching_values(actual, expected, content_type)
301
363
  end
364
+
302
365
  true
303
366
  end
304
367
 
368
+ def matching_values(actual, expected, content_type)
369
+ return matching_body_hashes?(actual, expected, content_type) if actual.is_a?(Hash) && expected.is_a?(Hash)
370
+ return matching_body_array?(actual, expected, content_type) if actual.is_a?(Array) && expected.is_a?(Array)
371
+
372
+ expected = WebMock::Util::ValuesStringifier.stringify_values(expected) if url_encoded_body?(content_type)
373
+ expected === actual
374
+ end
375
+
305
376
  def empty_string?(string)
306
377
  string.nil? || string == ""
307
378
  end
@@ -310,6 +381,9 @@ module WebMock
310
381
  Hash[WebMock::Util::HashKeysStringifier.stringify_keys!(hash, deep: true).sort]
311
382
  end
312
383
 
384
+ def url_encoded_body?(content_type)
385
+ content_type =~ %r{^application/x-www-form-urlencoded}
386
+ end
313
387
  end
314
388
 
315
389
  class HeadersPattern
@@ -333,6 +407,10 @@ module WebMock
333
407
  WebMock::Util::Headers.sorted_headers_string(@pattern)
334
408
  end
335
409
 
410
+ def pp_to_s
411
+ WebMock::Util::Headers.pp_headers_string(@pattern)
412
+ end
413
+
336
414
  private
337
415
 
338
416
  def empty_headers?(headers)
@@ -23,7 +23,7 @@ module WebMock
23
23
  if requested_signatures.hash.empty?
24
24
  "No requests were made."
25
25
  else
26
- text = ""
26
+ text = "".dup
27
27
  self.requested_signatures.each do |request_signature, times_executed|
28
28
  text << "#{request_signature} was made #{times_executed} time#{times_executed == 1 ? '' : 's' }\n"
29
29
  end
@@ -12,7 +12,7 @@ module WebMock
12
12
  end
13
13
 
14
14
  def to_s
15
- string = "#{self.method.to_s.upcase}"
15
+ string = "#{self.method.to_s.upcase}".dup
16
16
  string << " #{WebMock::Util::URI.strip_default_port_from_uri_string(self.uri.to_s)}"
17
17
  string << " with body '#{body.to_s}'" if body && body.to_s != ''
18
18
  if headers && !headers.empty?
@@ -35,11 +35,11 @@ module WebMock
35
35
  alias == eql?
36
36
 
37
37
  def url_encoded?
38
- !!(headers && headers['Content-Type'] == 'application/x-www-form-urlencoded')
38
+ !!(headers&.fetch('Content-Type', nil)&.start_with?('application/x-www-form-urlencoded'))
39
39
  end
40
40
 
41
41
  def json_headers?
42
- !!(headers && headers['Content-Type'] == 'application/json')
42
+ !!(headers&.fetch('Content-Type', nil)&.start_with?('application/json'))
43
43
  end
44
44
 
45
45
  private
@@ -13,14 +13,14 @@ module WebMock
13
13
  def stubbing_instructions
14
14
  return unless WebMock.show_stubbing_instructions?
15
15
 
16
- text = "You can stub this request with the following snippet:\n\n"
17
- text << WebMock::StubRequestSnippet.new(request_stub).to_s
16
+ "You can stub this request with the following snippet:\n\n" +
17
+ WebMock::StubRequestSnippet.new(request_stub).to_s
18
18
  end
19
19
 
20
20
  def request_stubs
21
21
  return if WebMock::StubRegistry.instance.request_stubs.empty?
22
22
 
23
- text = "registered request stubs:\n"
23
+ text = "registered request stubs:\n".dup
24
24
  WebMock::StubRegistry.instance.request_stubs.each do |stub|
25
25
  text << "\n#{WebMock::StubRequestSnippet.new(stub).to_s(false)}"
26
26
  add_body_diff(stub, text) if WebMock.show_body_diff?
@@ -50,7 +50,7 @@ module WebMock
50
50
  end
51
51
 
52
52
  def pretty_print_to_string(string_to_print)
53
- StringIO.open("") do |stream|
53
+ StringIO.open("".dup) do |stream|
54
54
  PP.pp(string_to_print, stream)
55
55
  stream.rewind
56
56
  stream.read
@@ -24,6 +24,21 @@ module WebMock
24
24
  end
25
25
  alias_method :and_return, :to_return
26
26
 
27
+ def to_return_json(*response_hashes)
28
+ raise ArgumentError, '#to_return_json does not support passing a block' if block_given?
29
+
30
+ json_response_hashes = [*response_hashes].flatten.map do |resp_h|
31
+ headers, body = resp_h.values_at(:headers, :body)
32
+ resp_h.merge(
33
+ headers: {content_type: 'application/json'}.merge(headers.to_h),
34
+ body: body.is_a?(Hash) ? body.to_json : body
35
+ )
36
+ end
37
+
38
+ to_return(json_response_hashes)
39
+ end
40
+ alias_method :and_return_json, :to_return_json
41
+
27
42
  def to_rack(app, options={})
28
43
  @responses_sequences << ResponsesSequence.new([RackResponse.new(app)])
29
44
  end
@@ -14,8 +14,11 @@ module WebMock
14
14
 
15
15
  class Response
16
16
  def initialize(options = {})
17
- if options.is_a?(IO) || options.is_a?(String)
17
+ case options
18
+ when IO, StringIO
18
19
  self.options = read_raw_response(options)
20
+ when String
21
+ self.options = read_raw_response(StringIO.new(options))
19
22
  else
20
23
  self.options = options
21
24
  end
@@ -91,10 +94,10 @@ module WebMock
91
94
 
92
95
  def ==(other)
93
96
  self.body == other.body &&
94
- self.headers === other.headers &&
95
- self.status == other.status &&
96
- self.exception == other.exception &&
97
- self.should_timeout == other.should_timeout
97
+ self.headers === other.headers &&
98
+ self.status == other.status &&
99
+ self.exception == other.exception &&
100
+ self.should_timeout == other.should_timeout
98
101
  end
99
102
 
100
103
  private
@@ -111,16 +114,17 @@ module WebMock
111
114
  valid_types = [Proc, IO, Pathname, String, Array]
112
115
  return if @body.nil?
113
116
  return if valid_types.any? { |c| @body.is_a?(c) }
114
- raise InvalidBody, "must be one of: #{valid_types}. '#{@body.class}' given"
115
- end
116
117
 
117
- def read_raw_response(raw_response)
118
- if raw_response.is_a?(IO)
119
- string = raw_response.read
120
- raw_response.close
121
- raw_response = string
118
+ if @body.class.is_a?(Hash)
119
+ raise InvalidBody, "must be one of: #{valid_types}, but you've used a #{@body.class}' instead." \
120
+ "\n What shall we encode it to? try calling .to_json .to_xml instead on the hash instead, or otherwise convert it to a string."
121
+ else
122
+ raise InvalidBody, "must be one of: #{valid_types}. '#{@body.class}' given"
122
123
  end
123
- socket = ::Net::BufferedIO.new(raw_response)
124
+ end
125
+
126
+ def read_raw_response(io)
127
+ socket = ::Net::BufferedIO.new(io)
124
128
  response = ::Net::HTTPResponse.read_new(socket)
125
129
  transfer_encoding = response.delete('transfer-encoding') #chunks were already read by curl
126
130
  response.reading_body(socket, true) {}
@@ -132,6 +136,8 @@ module WebMock
132
136
  options[:body] = response.read_body
133
137
  options[:status] = [response.code.to_i, response.message]
134
138
  options
139
+ ensure
140
+ socket.close
135
141
  end
136
142
 
137
143
  InvalidBody = Class.new(StandardError)
data/lib/webmock/rspec.rb CHANGED
@@ -20,14 +20,21 @@ end
20
20
 
21
21
  require 'webmock/rspec/matchers'
22
22
 
23
- WebMock.enable!
24
-
25
23
  RSPEC_CONFIGURER.configure { |config|
26
24
 
27
25
  config.include WebMock::API
28
26
  config.include WebMock::Matchers
29
27
 
30
- config.after(:each) do
28
+ config.before(:suite) do
29
+ WebMock.enable!
30
+ end
31
+
32
+ config.after(:suite) do
33
+ WebMock.disable!
34
+ end
35
+
36
+ config.around(:each) do |example|
37
+ example.run
31
38
  WebMock.reset!
32
39
  end
33
40
  }
@@ -10,25 +10,39 @@ module WebMock
10
10
  end
11
11
 
12
12
  def global_stubs
13
- @global_stubs ||= []
13
+ @global_stubs ||= Hash.new { |h, k| h[k] = [] }
14
14
  end
15
15
 
16
16
  def reset!
17
17
  self.request_stubs = []
18
18
  end
19
19
 
20
- def register_global_stub(&block)
20
+ def register_global_stub(order = :before_local_stubs, &block)
21
+ unless %i[before_local_stubs after_local_stubs].include?(order)
22
+ raise ArgumentError.new("Wrong order. Use :before_local_stubs or :after_local_stubs")
23
+ end
24
+
21
25
  # This hash contains the responses returned by the block,
22
26
  # keyed by the exact request (using the object_id).
23
27
  # That way, there's no race condition in case #to_return
24
28
  # doesn't run immediately after stub.with.
25
29
  responses = {}
26
-
27
- stub = ::WebMock::RequestStub.new(:any, /.*/).with { |request|
28
- responses[request.object_id] = block.call(request)
29
- }.to_return(lambda { |request| responses.delete(request.object_id) })
30
-
31
- global_stubs.push stub
30
+ response_lock = Mutex.new
31
+
32
+ stub = ::WebMock::RequestStub.new(:any, ->(uri) { true }).with { |request|
33
+ update_response = -> { responses[request.object_id] = yield(request) }
34
+
35
+ # The block can recurse, so only lock if we don't already own it
36
+ if response_lock.owned?
37
+ update_response.call
38
+ else
39
+ response_lock.synchronize(&update_response)
40
+ end
41
+ }.to_return(lambda { |request|
42
+ response_lock.synchronize { responses.delete(request.object_id) }
43
+ })
44
+
45
+ global_stubs[order].push stub
32
46
  end
33
47
 
34
48
  def register_request_stub(stub)
@@ -54,9 +68,10 @@ module WebMock
54
68
  private
55
69
 
56
70
  def request_stub_for(request_signature)
57
- (global_stubs + request_stubs).detect { |registered_request_stub|
58
- registered_request_stub.request_pattern.matches?(request_signature)
59
- }
71
+ (global_stubs[:before_local_stubs] + request_stubs + global_stubs[:after_local_stubs])
72
+ .detect { |registered_request_stub|
73
+ registered_request_stub.request_pattern.matches?(request_signature)
74
+ }
60
75
  end
61
76
 
62
77
  def evaluate_response_for_request(response, request_signature)