webmock 3.0.1 → 3.18.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 (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)