webmock 3.9.1 → 3.18.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (49) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/CI.yml +38 -0
  3. data/CHANGELOG.md +180 -6
  4. data/Gemfile +1 -1
  5. data/README.md +63 -30
  6. data/Rakefile +12 -2
  7. data/lib/webmock/http_lib_adapters/async_http_client_adapter.rb +11 -4
  8. data/lib/webmock/http_lib_adapters/curb_adapter.rb +2 -2
  9. data/lib/webmock/http_lib_adapters/em_http_request_adapter.rb +6 -3
  10. data/lib/webmock/http_lib_adapters/http_rb/client.rb +2 -1
  11. data/lib/webmock/http_lib_adapters/http_rb/response.rb +17 -3
  12. data/lib/webmock/http_lib_adapters/http_rb/streamer.rb +4 -2
  13. data/lib/webmock/http_lib_adapters/http_rb/webmock.rb +6 -2
  14. data/lib/webmock/http_lib_adapters/httpclient_adapter.rb +23 -6
  15. data/lib/webmock/http_lib_adapters/manticore_adapter.rb +8 -1
  16. data/lib/webmock/http_lib_adapters/net_http.rb +29 -115
  17. data/lib/webmock/request_pattern.rb +30 -8
  18. data/lib/webmock/request_signature.rb +2 -2
  19. data/lib/webmock/request_stub.rb +15 -0
  20. data/lib/webmock/response.rb +19 -13
  21. data/lib/webmock/stub_registry.rb +24 -9
  22. data/lib/webmock/test_unit.rb +1 -3
  23. data/lib/webmock/version.rb +1 -1
  24. data/lib/webmock/webmock.rb +12 -2
  25. data/minitest/webmock_spec.rb +1 -1
  26. data/spec/acceptance/async_http_client/async_http_client_spec.rb +27 -5
  27. data/spec/acceptance/curb/curb_spec.rb +11 -0
  28. data/spec/acceptance/em_http_request/em_http_request_spec.rb +57 -1
  29. data/spec/acceptance/em_http_request/em_http_request_spec_helper.rb +1 -1
  30. data/spec/acceptance/excon/excon_spec.rb +2 -2
  31. data/spec/acceptance/manticore/manticore_spec.rb +32 -0
  32. data/spec/acceptance/net_http/net_http_shared.rb +46 -9
  33. data/spec/acceptance/net_http/net_http_spec.rb +75 -23
  34. data/spec/acceptance/net_http/real_net_http_spec.rb +1 -1
  35. data/spec/acceptance/patron/patron_spec.rb +19 -21
  36. data/spec/acceptance/patron/patron_spec_helper.rb +2 -2
  37. data/spec/acceptance/shared/allowing_and_disabling_net_connect.rb +14 -14
  38. data/spec/acceptance/shared/callbacks.rb +2 -2
  39. data/spec/acceptance/shared/complex_cross_concern_behaviors.rb +1 -1
  40. data/spec/acceptance/shared/stubbing_requests.rb +17 -0
  41. data/spec/unit/request_pattern_spec.rb +82 -46
  42. data/spec/unit/request_signature_spec.rb +21 -1
  43. data/spec/unit/request_stub_spec.rb +35 -0
  44. data/spec/unit/response_spec.rb +51 -19
  45. data/spec/unit/webmock_spec.rb +54 -0
  46. data/test/test_webmock.rb +6 -0
  47. data/webmock.gemspec +6 -5
  48. metadata +49 -35
  49. data/.travis.yml +0 -24
@@ -43,6 +43,9 @@ if defined?(::HTTPClient)
43
43
  end
44
44
 
45
45
  module WebMockHTTPClients
46
+
47
+ REQUEST_RESPONSE_LOCK = Mutex.new
48
+
46
49
  def do_get_block(req, proxy, conn, &block)
47
50
  do_get(req, proxy, conn, false, &block)
48
51
  end
@@ -57,7 +60,7 @@ if defined?(::HTTPClient)
57
60
  WebMock::RequestRegistry.instance.requested_signatures.put(request_signature)
58
61
 
59
62
  if webmock_responses[request_signature]
60
- webmock_response = webmock_responses.delete(request_signature)
63
+ webmock_response = synchronize_request_response { webmock_responses.delete(request_signature) }
61
64
  response = build_httpclient_response(webmock_response, stream, req.header, &block)
62
65
  @request_filter.each do |filter|
63
66
  filter.filter_response(req, response)
@@ -68,7 +71,7 @@ if defined?(::HTTPClient)
68
71
  res
69
72
  elsif WebMock.net_connect_allowed?(request_signature.uri)
70
73
  # in case there is a nil entry in the hash...
71
- webmock_responses.delete(request_signature)
74
+ synchronize_request_response { webmock_responses.delete(request_signature) }
72
75
 
73
76
  res = if stream
74
77
  do_get_stream_without_webmock(req, proxy, conn, &block)
@@ -100,7 +103,7 @@ if defined?(::HTTPClient)
100
103
  def do_request_async(method, uri, query, body, extheader)
101
104
  req = create_request(method, uri, query, body, extheader)
102
105
  request_signature = build_request_signature(req)
103
- webmock_request_signatures << request_signature
106
+ synchronize_request_response { webmock_request_signatures << request_signature }
104
107
 
105
108
  if webmock_responses[request_signature] || WebMock.net_connect_allowed?(request_signature.uri)
106
109
  super
@@ -184,7 +187,9 @@ if defined?(::HTTPClient)
184
187
 
185
188
  def webmock_responses
186
189
  @webmock_responses ||= Hash.new do |hash, request_signature|
187
- hash[request_signature] = WebMock::StubRegistry.instance.response_for_request(request_signature)
190
+ synchronize_request_response do
191
+ hash[request_signature] = WebMock::StubRegistry.instance.response_for_request(request_signature)
192
+ end
188
193
  end
189
194
  end
190
195
 
@@ -193,8 +198,10 @@ if defined?(::HTTPClient)
193
198
  end
194
199
 
195
200
  def previous_signature_for(signature)
196
- return nil unless index = webmock_request_signatures.index(signature)
197
- webmock_request_signatures.delete_at(index)
201
+ synchronize_request_response do
202
+ return nil unless index = webmock_request_signatures.index(signature)
203
+ webmock_request_signatures.delete_at(index)
204
+ end
198
205
  end
199
206
 
200
207
  private
@@ -209,6 +216,16 @@ if defined?(::HTTPClient)
209
216
  hdrs
210
217
  end
211
218
  end
219
+
220
+ def synchronize_request_response
221
+ if REQUEST_RESPONSE_LOCK.owned?
222
+ yield
223
+ else
224
+ REQUEST_RESPONSE_LOCK.synchronize do
225
+ yield
226
+ end
227
+ end
228
+ end
212
229
  end
213
230
 
214
231
  class WebMockHTTPClient < HTTPClient
@@ -127,8 +127,15 @@ if defined?(Manticore)
127
127
  def generate_webmock_response(manticore_response)
128
128
  webmock_response = WebMock::Response.new
129
129
  webmock_response.status = [manticore_response.code, manticore_response.message]
130
- webmock_response.body = manticore_response.body
131
130
  webmock_response.headers = manticore_response.headers
131
+
132
+ # The attempt to read the body could fail if manticore is used in a streaming mode
133
+ webmock_response.body = begin
134
+ manticore_response.body
135
+ rescue ::Manticore::StreamClosedException
136
+ nil
137
+ end
138
+
132
139
  webmock_response
133
140
  end
134
141
  end
@@ -10,24 +10,19 @@ module WebMock
10
10
  adapter_for :net_http
11
11
 
12
12
  OriginalNetHTTP = Net::HTTP unless const_defined?(:OriginalNetHTTP)
13
- OriginalNetBufferedIO = Net::BufferedIO unless const_defined?(:OriginalNetBufferedIO)
14
13
 
15
14
  def self.enable!
16
- Net.send(:remove_const, :BufferedIO)
17
15
  Net.send(:remove_const, :HTTP)
18
16
  Net.send(:remove_const, :HTTPSession)
19
17
  Net.send(:const_set, :HTTP, @webMockNetHTTP)
20
18
  Net.send(:const_set, :HTTPSession, @webMockNetHTTP)
21
- Net.send(:const_set, :BufferedIO, Net::WebMockNetBufferedIO)
22
19
  end
23
20
 
24
21
  def self.disable!
25
- Net.send(:remove_const, :BufferedIO)
26
22
  Net.send(:remove_const, :HTTP)
27
23
  Net.send(:remove_const, :HTTPSession)
28
24
  Net.send(:const_set, :HTTP, OriginalNetHTTP)
29
25
  Net.send(:const_set, :HTTPSession, OriginalNetHTTP)
30
- Net.send(:const_set, :BufferedIO, OriginalNetBufferedIO)
31
26
 
32
27
  #copy all constants from @webMockNetHTTP to original Net::HTTP
33
28
  #in case any constants were added to @webMockNetHTTP instead of Net::HTTP
@@ -98,13 +93,8 @@ module WebMock
98
93
  after_request.call(response)
99
94
  }
100
95
  if started?
101
- if WebMock::Config.instance.net_http_connect_on_start
102
- super_with_after_request.call
103
- else
104
- start_with_connect_without_finish {
105
- super_with_after_request.call
106
- }
107
- end
96
+ ensure_actual_connection
97
+ super_with_after_request.call
108
98
  else
109
99
  start_with_connect {
110
100
  super_with_after_request.call
@@ -119,32 +109,33 @@ module WebMock
119
109
  raise IOError, 'HTTP session already opened' if @started
120
110
  if block_given?
121
111
  begin
112
+ @socket = Net::HTTP.socket_type.new
122
113
  @started = true
123
114
  return yield(self)
124
115
  ensure
125
116
  do_finish
126
117
  end
127
118
  end
119
+ @socket = Net::HTTP.socket_type.new
128
120
  @started = true
129
121
  self
130
122
  end
131
123
 
132
124
 
133
- def start_with_connect_without_finish # :yield: http
134
- if block_given?
135
- begin
136
- do_start
137
- return yield(self)
138
- end
125
+ def ensure_actual_connection
126
+ if @socket.is_a?(StubSocket)
127
+ @socket&.close
128
+ @socket = nil
129
+ do_start
139
130
  end
140
- do_start
141
- self
142
131
  end
143
132
 
144
133
  alias_method :start_with_connect, :start
145
134
 
146
135
  def start(&block)
147
- if WebMock::Config.instance.net_http_connect_on_start
136
+ uri = Addressable::URI.parse(WebMock::NetHTTPUtility.get_uri(self))
137
+
138
+ if WebMock.net_http_connect_on_start?(uri)
148
139
  super(&block)
149
140
  else
150
141
  start_without_connect(&block)
@@ -169,7 +160,7 @@ module WebMock
169
160
  response.extend Net::WebMockHTTPResponse
170
161
 
171
162
  if webmock_response.should_timeout
172
- raise timeout_exception, "execution expired"
163
+ raise Net::OpenTimeout, "execution expired"
173
164
  end
174
165
 
175
166
  webmock_response.raise_error_if_any
@@ -179,16 +170,6 @@ module WebMock
179
170
  response
180
171
  end
181
172
 
182
- def timeout_exception
183
- if defined?(Net::OpenTimeout)
184
- # Ruby 2.x
185
- Net::OpenTimeout
186
- else
187
- # Fallback, if things change
188
- Timeout::Error
189
- end
190
- end
191
-
192
173
  def build_webmock_response(net_http_response)
193
174
  webmock_response = WebMock::Response.new
194
175
  webmock_response.status = [
@@ -222,31 +203,21 @@ module WebMock
222
203
  end
223
204
  end
224
205
 
225
- # patch for StringIO behavior in Ruby 2.2.3
226
- # https://github.com/bblimke/webmock/issues/558
227
- class PatchedStringIO < StringIO #:nodoc:
228
-
229
- alias_method :orig_read_nonblock, :read_nonblock
230
-
231
- def read_nonblock(size, *args, **kwargs)
232
- args.reject! {|arg| !arg.is_a?(Hash)}
233
- orig_read_nonblock(size, *args, **kwargs)
234
- end
235
-
236
- end
237
-
238
206
  class StubSocket #:nodoc:
239
207
 
240
208
  attr_accessor :read_timeout, :continue_timeout, :write_timeout
241
209
 
242
210
  def initialize(*args)
211
+ @closed = false
243
212
  end
244
213
 
245
214
  def closed?
246
- @closed ||= true
215
+ @closed
247
216
  end
248
217
 
249
218
  def close
219
+ @closed = true
220
+ nil
250
221
  end
251
222
 
252
223
  def readuntil(*args)
@@ -258,63 +229,17 @@ class StubSocket #:nodoc:
258
229
 
259
230
  class StubIO
260
231
  def setsockopt(*args); end
232
+ def peer_cert; end
233
+ def peeraddr; ["AF_INET", 443, "127.0.0.1", "127.0.0.1"] end
234
+ def ssl_version; "TLSv1.3" end
235
+ def cipher; ["TLS_AES_128_GCM_SHA256", "TLSv1.3", 128, 128] end
261
236
  end
262
237
  end
263
238
 
264
- module Net #:nodoc: all
265
-
266
- class WebMockNetBufferedIO < BufferedIO
267
- def initialize(io, *args, **kwargs)
268
- io = case io
269
- when Socket, OpenSSL::SSL::SSLSocket, IO
270
- io
271
- when StringIO
272
- PatchedStringIO.new(io.string)
273
- when String
274
- PatchedStringIO.new(io)
275
- end
276
- raise "Unable to create local socket" unless io
277
-
278
- # Prior to 2.4.0 `BufferedIO` only takes a single argument (`io`) with no
279
- # options. Here we pass through our full set of arguments only if we're
280
- # on 2.4.0 or later, and use a simplified invocation otherwise.
281
- if RUBY_VERSION >= '2.4.0'
282
- super
283
- else
284
- super(io)
285
- end
286
- end
287
-
288
- if RUBY_VERSION >= '2.6.0'
289
- # https://github.com/ruby/ruby/blob/7d02441f0d6e5c9d0a73a024519eba4f69e36dce/lib/net/protocol.rb#L208
290
- # Modified version of method from ruby, so that nil is always passed into orig_read_nonblock to avoid timeout
291
- def rbuf_fill
292
- case rv = @io.read_nonblock(BUFSIZE, nil, exception: false)
293
- when String
294
- return if rv.nil?
295
- @rbuf << rv
296
- rv.clear
297
- return
298
- when :wait_readable
299
- @io.to_io.wait_readable(@read_timeout) or raise Net::ReadTimeout
300
- when :wait_writable
301
- @io.to_io.wait_writable(@read_timeout) or raise Net::ReadTimeout
302
- when nil
303
- raise EOFError, 'end of file reached'
304
- end while true
305
- end
306
- end
307
- end
308
-
309
- end
310
-
311
-
312
239
  module WebMock
313
240
  module NetHTTPUtility
314
241
 
315
242
  def self.request_signature_from_request(net_http, request, body = nil)
316
- protocol = net_http.use_ssl? ? "https" : "http"
317
-
318
243
  path = request.path
319
244
 
320
245
  if path.respond_to?(:request_uri) #https://github.com/bblimke/webmock/issues/288
@@ -323,11 +248,10 @@ module WebMock
323
248
 
324
249
  path = WebMock::Util::URI.heuristic_parse(path).request_uri if path =~ /^http/
325
250
 
326
- uri = "#{protocol}://#{net_http.address}:#{net_http.port}#{path}"
251
+ uri = get_uri(net_http, path)
327
252
  method = request.method.downcase.to_sym
328
253
 
329
254
  headers = Hash[*request.to_hash.map {|k,v| [k, v]}.inject([]) {|r,x| r + x}]
330
- validate_headers(headers)
331
255
 
332
256
  if request.body_stream
333
257
  body = request.body_stream.read
@@ -343,23 +267,13 @@ module WebMock
343
267
  WebMock::RequestSignature.new(method, uri, body: request.body, headers: headers)
344
268
  end
345
269
 
346
- def self.validate_headers(headers)
347
- # For Ruby versions < 2.3.0, if you make a request with headers that are symbols
348
- # Net::HTTP raises a NoMethodError
349
- #
350
- # WebMock normalizes headers when creating a RequestSignature,
351
- # and will update all headers from symbols to strings.
352
- #
353
- # This could create a false positive in a test suite with WebMock.
354
- #
355
- # So before this point, WebMock raises an ArgumentError if any of the headers are symbols
356
- # instead of the cryptic NoMethodError "undefined method `split' ...` from Net::HTTP
357
- if Gem::Version.new(RUBY_VERSION.dup) < Gem::Version.new('2.3.0')
358
- header_as_symbol = headers.keys.find {|header| header.is_a? Symbol}
359
- if header_as_symbol
360
- raise ArgumentError.new("Net:HTTP does not accept headers as symbols")
361
- end
362
- end
270
+ def self.get_uri(net_http, path = nil)
271
+ protocol = net_http.use_ssl? ? "https" : "http"
272
+
273
+ hostname = net_http.address
274
+ hostname = "[#{hostname}]" if /\A\[.*\]\z/ !~ hostname && /:/ =~ hostname
275
+
276
+ "#{protocol}://#{hostname}:#{net_http.port}#{path}"
363
277
  end
364
278
 
365
279
  def self.check_right_http_connection
@@ -281,6 +281,8 @@ module WebMock
281
281
  if (@pattern).is_a?(Hash)
282
282
  return true if @pattern.empty?
283
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)
284
286
  elsif (@pattern).is_a?(WebMock::Matchers::HashIncludingMatcher)
285
287
  @pattern == body_as_hash(body, content_type)
286
288
  else
@@ -295,8 +297,9 @@ module WebMock
295
297
  end
296
298
 
297
299
  private
300
+
298
301
  def body_as_hash(body, content_type)
299
- case BODY_FORMATS[content_type]
302
+ case body_format(content_type)
300
303
  when :json then
301
304
  WebMock::Util::JSON.parse(body)
302
305
  when :xml then
@@ -306,6 +309,11 @@ module WebMock
306
309
  end
307
310
  end
308
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
+
309
317
  def assert_non_multipart_body(content_type)
310
318
  if content_type =~ %r{^multipart/form-data}
311
319
  raise ArgumentError.new("WebMock does not support matching body for multipart/form-data requests yet :(")
@@ -338,19 +346,33 @@ module WebMock
338
346
  def matching_body_hashes?(query_parameters, pattern, content_type)
339
347
  return false unless query_parameters.is_a?(Hash)
340
348
  return false unless query_parameters.keys.sort == pattern.keys.sort
341
- query_parameters.each do |key, actual|
349
+
350
+ query_parameters.all? do |key, actual|
342
351
  expected = pattern[key]
352
+ matching_values(actual, expected, content_type)
353
+ end
354
+ end
343
355
 
344
- if actual.is_a?(Hash) && expected.is_a?(Hash)
345
- return false unless matching_body_hashes?(actual, expected, content_type)
346
- else
347
- expected = WebMock::Util::ValuesStringifier.stringify_values(expected) if url_encoded_body?(content_type)
348
- return false unless expected === actual
349
- 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)
350
363
  end
364
+
351
365
  true
352
366
  end
353
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
+
354
376
  def empty_string?(string)
355
377
  string.nil? || string == ""
356
378
  end
@@ -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
@@ -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)
@@ -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 = {}
30
+ response_lock = Mutex.new
26
31
 
27
32
  stub = ::WebMock::RequestStub.new(:any, ->(uri) { true }).with { |request|
28
- responses[request.object_id] = yield(request)
29
- }.to_return(lambda { |request| responses.delete(request.object_id) })
30
-
31
- global_stubs.push stub
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)
@@ -8,12 +8,10 @@ module Test
8
8
  class TestCase
9
9
  include WebMock::API
10
10
 
11
- alias_method :teardown_without_webmock, :teardown
11
+ teardown
12
12
  def teardown_with_webmock
13
- teardown_without_webmock
14
13
  WebMock.reset!
15
14
  end
16
- alias_method :teardown, :teardown_with_webmock
17
15
 
18
16
  end
19
17
  end
@@ -1,3 +1,3 @@
1
1
  module WebMock
2
- VERSION = '3.9.1' unless defined?(::WebMock::VERSION)
2
+ VERSION = '3.18.1' unless defined?(::WebMock::VERSION)
3
3
  end
@@ -70,6 +70,16 @@ module WebMock
70
70
  Config.instance.allow && net_connect_explicit_allowed?(Config.instance.allow, uri) )
71
71
  end
72
72
 
73
+ def self.net_http_connect_on_start?(uri)
74
+ allowed = Config.instance.net_http_connect_on_start || false
75
+
76
+ if [true, false].include?(allowed)
77
+ allowed
78
+ else
79
+ net_connect_explicit_allowed?(allowed, uri)
80
+ end
81
+ end
82
+
73
83
  def self.net_connect_explicit_allowed?(allowed, uri=nil)
74
84
  case allowed
75
85
  when Array
@@ -140,8 +150,8 @@ module WebMock
140
150
  puts WebMock::RequestExecutionVerifier.executed_requests_message
141
151
  end
142
152
 
143
- def self.globally_stub_request(&block)
144
- WebMock::StubRegistry.instance.register_global_stub(&block)
153
+ def self.globally_stub_request(order = :before_local_stubs, &block)
154
+ WebMock::StubRegistry.instance.register_global_stub(order, &block)
145
155
  end
146
156
 
147
157
  %w(
@@ -20,7 +20,7 @@ require File.expand_path(File.dirname(__FILE__) + '/test_helper')
20
20
  end
21
21
 
22
22
  it "should raise error on non stubbed request" do
23
- lambda { http_request(:get, "http://www.example.net/") }.must_raise(WebMock::NetConnectNotAllowedError)
23
+ expect { http_request(:get, "http://www.example.net/") }.must_raise(WebMock::NetConnectNotAllowedError)
24
24
  end
25
25
 
26
26
  it "should verify that expected request occured" do