webmock 3.26.4 → 3.30.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: eb0293b4433bbf3342d1f81abb5a1c773382f6f1a7fef6b44eb83fc0a38544a7
4
- data.tar.gz: 00fbb149216fae39f04b689fc02c0f561e59e47843011fd8aaab36b35e86ba67
3
+ metadata.gz: 0be0b6d1129e7014a36a5dbe7a7e77d3a25852d1668f54c455e0be96cd2d1287
4
+ data.tar.gz: db8c3267c3fbf2eef3b69f4b72764a82983e589a6508c9d0be8d2cd53047d484
5
5
  SHA512:
6
- metadata.gz: 664302057fd929d144d09bb739c0cc581f4b80b40b67093989a4df01a21337f12594b765d9da0bc980ba981cfbe8db451cf0d23ce4ed10fa5d57eb2ddd98de79
7
- data.tar.gz: bad8a582bd3b717e11182aef243b7167021187d63bd40b2dc1f0f54f13928e898e45ecf1cb598419e8dfc643d5b270db0d3a5408ab871ffb0725d5560dbd556b
6
+ metadata.gz: ad6692051819395acd28a095acb54762b81b379b9d75115f0572351f2693973558978b24392f2086bb2dd6bb3969730881efb0d2dc54210440c9114bc2c867b6
7
+ data.tar.gz: d4c877e82c741f3319e66d803421eb0fb52f3d1a798465bc19c8b55e52d00fc57fca89cd5eb82a52f88d9f0223c2784adcb7110838405a42f05e9e5c26f53561
data/CHANGELOG.md CHANGED
@@ -1,5 +1,71 @@
1
1
  # Changelog
2
2
 
3
+ # 3.30.0
4
+
5
+ * Allow narrowing an RSpec expectation on a stub with `with`, as in `expect(stub).to have_been_requested.once.with(body: "abc")` (#1128)
6
+
7
+ The expectation counts only the requests that match both the stub and the conditions given to `with`. The stub itself does not change.
8
+
9
+ Thanks to [Sébastien S.](https://github.com/inkstak)
10
+
11
+ * Show the JSON body hint for expectations narrowed with `with` too, and no longer show it when a block is given to the stub or expectation, since checking would mean calling the block
12
+
13
+ * Allow a full URL with a path in `disable_net_connect!(allow: ...)`, as in `allow: "https://github.com/mozilla/geckodriver/releases/latest"` (#977)
14
+
15
+ URLs are compared the same way as in stubs, so the default port, letter case in the host, and the order and encoding of query parameters don't matter. A URL without a path or query allows every path on that scheme, host and port; a trailing slash restricts requests to `/` with no query string. Credentials in either URL are ignored.
16
+
17
+ Thanks to [Henry Blyth](https://github.com/henrahmagix)
18
+
19
+ # 3.29.0
20
+
21
+ * Show a body diff for JSON requests whose content type has a `+json` suffix, such as `application/vnd.api+json`, and for the other content types WebMock already matches as JSON (#734)
22
+
23
+ Thanks to [Olivier Brisse](https://github.com/ouranos)
24
+
25
+ * Ignore the case of JSON, XML and url-encoded content types when matching request bodies, and match bodies of any `+json` or `+xml` media type, such as `image/svg+xml`, as JSON or XML
26
+
27
+ * Suggest declaring a JSON body as a hash when a request fails to match only because the JSON is formatted differently (#649)
28
+
29
+ A body declared as a string must match the request body exactly, including key order and whitespace. When that is the only difference, the error for an unregistered request and the failure message of a request expectation now say so, and suggest `with(body: JSON.parse(json_string))`. Matching JSON strings regardless of key order by default is planned for the next major version (#485).
30
+
31
+ # 3.28.0
32
+
33
+ * Fix `to_rack` apps receiving a `rack.session` they did not set up (#985)
34
+
35
+ WebMock used to add a session hash to the Rack env, shared by every request to the same stub. This confused apps that check whether a session is present, such as Rails API-only apps, and carried session data over between requests. WebMock now leaves `rack.session` and `rack.session.options` unset, as a real server does. If your app needs a session, include session middleware in the app you pass to `to_rack`. `RackResponse#session` and `RackResponse#session_options` have been removed.
36
+
37
+ Thanks to [Jan Graichen](https://github.com/jgraichen)
38
+
39
+ * Fix crash when matching a hash body pattern against a JSON or XML request that has no body
40
+
41
+ Thanks to [Chris](https://github.com/ChrisJr404)
42
+
43
+ * No longer warn about curb versions newer than the last one tested
44
+
45
+ * Document regexp values in `hash_including`
46
+
47
+ Thanks to [Christian Bruckmayer](https://github.com/ChrisBr)
48
+
49
+ * Remove unused `RequestSignatureSnippet#request_params`
50
+
51
+ Thanks to [Daniel Pepper](https://github.com/dpep)
52
+
53
+ # 3.27.0
54
+
55
+ * Fix crash in async-http adapter when a request header has a nil value
56
+
57
+ Thanks to [hikmetba-bit](https://github.com/hikmetba-bit)
58
+
59
+ * The async-http adapter now matches stubs against raw request header values instead of values parsed by protocol-http, consistent with the other adapters
60
+
61
+ * Add RSpec `hash_excluding` support to body matching
62
+
63
+ Thanks to [Chris Gunther](https://github.com/cgunther)
64
+
65
+ * Fix `hash_excluding` ignoring keys given without a value and regexp values, in both query and body matching
66
+
67
+ * Fix crash when matching a request that has more than one `Content-Type` header
68
+
3
69
  # 3.26.4
4
70
 
5
71
  * Fix race condition in `HTTPClient` adapter async requests by @koic in #1131
data/README.md CHANGED
@@ -176,6 +176,8 @@ RestClient.post('www.example.com', '<data a="1" b="five" />',
176
176
  content_type: 'application/xml') # ===> Success
177
177
  ```
178
178
 
179
+ A body declared as a string must match the request body exactly. For JSON, that includes key order and whitespace, so declare the body as a hash to match it as JSON, for example `with(body: JSON.parse(json_string))`.
180
+
179
181
  ### Matching request body against partial hash.
180
182
 
181
183
  ```ruby
@@ -184,6 +186,24 @@ stub_request(:post, "www.example.com").
184
186
 
185
187
  RestClient.post('www.example.com', "data[a]=1&data[b]=five&x=1",
186
188
  :content_type => 'application/x-www-form-urlencoded') # ===> Success
189
+
190
+ stub_request(:post, "www.example.com").
191
+ with(body: hash_including(id: /\d/))
192
+
193
+ RestClient.post('www.example.com', "id=1",
194
+ :content_type => 'application/x-www-form-urlencoded') # ===> Success
195
+ ```
196
+
197
+ ### Matching request body against excluding partial hash.
198
+
199
+ ```ruby
200
+ stub_request(:post, "www.example.com").
201
+ with(body: hash_excluding({data: {a: '2', b: 'six'}}))
202
+
203
+ RestClient.post('www.example.com', "data[a]=1&data[b]=five&x=1",
204
+ :content_type => 'application/x-www-form-urlencoded') # ===> Success
205
+ RestClient.post('www.example.com', "data[a]=2&data[b]=six&x=1",
206
+ :content_type => 'application/x-www-form-urlencoded') # ===> Failure
187
207
  ```
188
208
 
189
209
  ### Matching custom request headers
@@ -426,6 +446,8 @@ stub_request(:get, "www.example.com").to_rack(MyRackApp)
426
446
  RestClient.post('www.example.com') # ===> "Hello"
427
447
  ```
428
448
 
449
+ WebMock does not set `rack.session` in the Rack env, just as a real server would not. If your app needs a session, include session middleware in the app you pass to `to_rack`.
450
+
429
451
  ### Raising errors
430
452
 
431
453
  #### Exception declared by class
@@ -551,6 +573,21 @@ RestClient.get('www.example.org', '/') # ===> Failure
551
573
  RestClient.get('www.example.org:8080', '/') # ===> Allowed
552
574
  ```
553
575
 
576
+ With a `String` specifying a full URL:
577
+
578
+ ```ruby
579
+ WebMock.disable_net_connect!(allow: 'https://www.example.org/health')
580
+
581
+ RestClient.get('https://www.example.org/health') # ===> Allowed
582
+ RestClient.get('https://www.example.org/health?full=1') # ===> Failure
583
+ RestClient.get('https://www.example.org/health/db') # ===> Failure
584
+ RestClient.get('https://www.example.org/') # ===> Failure
585
+ ```
586
+
587
+ A URL with a path allows only that path and query string. URLs are compared the same way as in stubs, so the default port, letter case in the host, and the order and encoding of query parameters don't matter.
588
+
589
+ A URL without a path or query, such as `https://www.example.org`, allows every path on that scheme, host and port. Adding the trailing slash (`https://www.example.org/`) allows only the root path `/` with no query string. To allow every path under a prefix, use a `Regexp`.
590
+
554
591
  With a `Regexp` matching the URI:
555
592
 
556
593
  ```ruby
@@ -615,6 +652,8 @@ This forces WebMock Net::HTTP adapter to always connect on `Net::HTTP.start`. At
615
652
  WebMock.allow_net_connect!(net_http_connect_on_start: "www.example.com")
616
653
  ```
617
654
 
655
+ The path is not known when the connection is made, so a URL with a path given here never matches.
656
+
618
657
  ## Setting Expectations
619
658
 
620
659
  ### Setting expectations in Test::Unit
@@ -729,11 +768,17 @@ expect(a_request(:post, "www.example.com").
729
768
  ### Setting expectations in RSpec on the stub
730
769
 
731
770
  ```ruby
732
- stub = stub_request(:get, "www.example.com")
771
+ stub = stub_request(:post, "www.example.com")
772
+
733
773
  # ... make requests ...
734
- expect(stub).to have_been_requested
774
+
775
+ expect(stub).to have_been_requested.twice
776
+ expect(stub).to have_been_requested.once.with(body: {"a" => [1, 2, 3]})
777
+ expect(stub).to have_been_requested.once.with(body: {"a" => [4, 5, 6]})
735
778
  ```
736
779
 
780
+ Options and a block given to `with` take the same form as in `stub_request(...).with`. The expectation then counts only the requests that match both the stub and those conditions. The stub itself does not change.
781
+
737
782
  ## Clearing stubs and request history
738
783
 
739
784
  If you want to reset all current stubs and history of requests use `WebMock.reset!`
@@ -111,11 +111,23 @@ if defined?(Async::HTTP)
111
111
  WebMock::RequestSignature.new(
112
112
  request.method.downcase.to_sym,
113
113
  "#{request.scheme}://#{request.authority}#{request.path}",
114
- headers: request.headers.to_h,
114
+ headers: build_request_headers(request),
115
115
  body: body
116
116
  )
117
117
  end
118
118
 
119
+ # Protocol::HTTP::Headers#to_h parses values through the header policies
120
+ # (since protocol-http 0.56.0) and raises when a value is nil. Build the
121
+ # hash from the raw fields instead, skipping entries with no value since
122
+ # they carry nothing to match against. Keys are downcased so the same
123
+ # header sent in different casings is grouped together.
124
+ def build_request_headers(request)
125
+ request.headers.each.each_with_object({}) do |(key, value), headers|
126
+ next if value.nil?
127
+ (headers[key.downcase] ||= []) << value
128
+ end
129
+ end
130
+
119
131
  def build_webmock_response(response)
120
132
  body = response.read
121
133
  response.body = ::Protocol::HTTP::Body::Buffered.wrap(body)
@@ -13,7 +13,7 @@ if defined?(Curl)
13
13
  # in curb 1.3.0 through 1.3.5. Fixed in curb 1.3.6. See https://github.com/taf2/curb/issues/478
14
14
  unsupported_curb_versions += %w[1.3.0 1.3.1 1.3.2 1.3.3 1.3.4 1.3.5] if RUBY_VERSION < '2.7'
15
15
 
16
- WebMock::VersionChecker.new('Curb', Curl::CURB_VERSION, '0.7.16', '1.3.7', unsupported_curb_versions).check_version!
16
+ WebMock::VersionChecker.new('Curb', Curl::CURB_VERSION, '0.7.16', nil, unsupported_curb_versions).check_version!
17
17
 
18
18
  module WebMock
19
19
  module HttpLibAdapters
@@ -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 != actual[key] }
9
+ super { |key, value| !actual.key?(key) || !(value === actual[key]) }
10
10
  end
11
11
 
12
12
  def inspect
@@ -52,8 +52,6 @@ module WebMock
52
52
  end
53
53
  env['rack.url_scheme'] = uri.scheme
54
54
  env['rack.run_once'] = true
55
- env['rack.session'] = session
56
- env['rack.session.options'] = session_options
57
55
 
58
56
  headers.each do |k, v|
59
57
  env["HTTP_#{k.tr('-','_').upcase}"] = v
@@ -61,13 +59,5 @@ module WebMock
61
59
 
62
60
  env
63
61
  end
64
-
65
- def session
66
- @session ||= {}
67
- end
68
-
69
- def session_options
70
- @session_options ||= {}
71
- end
72
62
  end
73
63
  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)} "
@@ -14,7 +14,7 @@ module WebMock
14
14
 
15
15
  class RequestPattern
16
16
 
17
- attr_reader :method_pattern, :uri_pattern, :body_pattern, :headers_pattern
17
+ attr_reader :method_pattern, :uri_pattern, :body_pattern, :headers_pattern, :with_block
18
18
 
19
19
  def initialize(method, uri, options = {})
20
20
  @method_pattern = MethodPattern.new(method)
@@ -33,11 +33,9 @@ module WebMock
33
33
  end
34
34
 
35
35
  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
36
  @method_pattern.matches?(request_signature.method) &&
39
37
  @uri_pattern.matches?(request_signature.uri) &&
40
- (@body_pattern.nil? || @body_pattern.matches?(request_signature.body, content_type || "")) &&
38
+ (@body_pattern.nil? || @body_pattern.matches?(request_signature.body, request_signature.content_type)) &&
41
39
  (@headers_pattern.nil? || @headers_pattern.matches?(request_signature.headers)) &&
42
40
  (@with_block.nil? || @with_block.call(request_signature))
43
41
  end
@@ -255,18 +253,8 @@ module WebMock
255
253
  class BodyPattern
256
254
  include RSpecMatcherDetector
257
255
 
258
- BODY_FORMATS = {
259
- 'text/xml' => :xml,
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
- }
256
+ # Kept so existing references to the constant still resolve.
257
+ BODY_FORMATS = WebMock::Util::ContentType::BODY_FORMATS
270
258
 
271
259
  attr_reader :pattern
272
260
 
@@ -275,12 +263,16 @@ module WebMock
275
263
  normalize_hash(pattern)
276
264
  elsif rSpecHashIncludingMatcher?(pattern)
277
265
  WebMock::Matchers::HashIncludingMatcher.from_rspec_matcher(pattern)
266
+ elsif rSpecHashExcludingMatcher?(pattern)
267
+ WebMock::Matchers::HashExcludingMatcher.from_rspec_matcher(pattern)
278
268
  else
279
269
  pattern
280
270
  end
281
271
  end
282
272
 
283
273
  def matches?(body, content_type = "")
274
+ # Checked on the raw value, so body patterns used with mixed case
275
+ # multipart content types keep working as in earlier versions.
284
276
  assert_non_multipart_body(content_type)
285
277
 
286
278
  if (@pattern).is_a?(Hash)
@@ -304,7 +296,9 @@ module WebMock
304
296
  private
305
297
 
306
298
  def body_as_hash(body, content_type)
307
- case body_format(content_type)
299
+ return if body.nil?
300
+
301
+ case WebMock::Util::ContentType.body_format(content_type)
308
302
  when :json then
309
303
  WebMock::Util::Parsers::JSON.parse(body)
310
304
  when :xml then
@@ -316,11 +310,6 @@ module WebMock
316
310
  nil
317
311
  end
318
312
 
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
313
  def assert_non_multipart_body(content_type)
325
314
  if content_type =~ %r{^multipart/form-data}
326
315
  raise ArgumentError.new("WebMock does not support matching body for multipart/form-data requests yet :(")
@@ -376,7 +365,7 @@ module WebMock
376
365
  return matching_body_hashes?(actual, expected, content_type) if actual.is_a?(Hash) && expected.is_a?(Hash)
377
366
  return matching_body_array?(actual, expected, content_type) if actual.is_a?(Array) && expected.is_a?(Array)
378
367
 
379
- expected = WebMock::Util::ValuesStringifier.stringify_values(expected) if url_encoded_body?(content_type)
368
+ expected = WebMock::Util::ValuesStringifier.stringify_values(expected) if WebMock::Util::ContentType.url_encoded?(content_type)
380
369
  expected === actual
381
370
  end
382
371
 
@@ -387,10 +376,6 @@ module WebMock
387
376
  def normalize_hash(hash)
388
377
  Hash[WebMock::Util::HashKeysStringifier.stringify_keys!(hash, deep: true).sort]
389
378
  end
390
-
391
- def url_encoded_body?(content_type)
392
- content_type =~ %r{^application/x-www-form-urlencoded}
393
- end
394
379
  end
395
380
 
396
381
  class HeadersPattern
@@ -36,12 +36,18 @@ module WebMock
36
36
  end
37
37
  alias == eql?
38
38
 
39
+ # A request can carry Content-Type more than once, in which case the
40
+ # normalized header value is an Array. Use its first value.
41
+ def content_type
42
+ Array(headers&.fetch('Content-Type', nil)).first
43
+ end
44
+
39
45
  def url_encoded?
40
- !!(headers&.fetch('Content-Type', nil)&.start_with?('application/x-www-form-urlencoded'))
46
+ WebMock::Util::ContentType.url_encoded?(content_type)
41
47
  end
42
48
 
43
49
  def json_headers?
44
- !!(headers&.fetch('Content-Type', nil)&.start_with?('application/json'))
50
+ WebMock::Util::ContentType.body_format(content_type) == :json
45
51
  end
46
52
 
47
53
  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
- add_body_diff(stub, text) if WebMock.show_body_diff?
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)
@@ -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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module WebMock
4
- VERSION = '3.26.4' unless defined?(::WebMock::VERSION)
4
+ VERSION = '3.30.0' unless defined?(::WebMock::VERSION)
5
5
  end
@@ -87,14 +87,12 @@ module WebMock
87
87
  when Array
88
88
  allowed.any? { |allowed_item| net_connect_explicit_allowed?(allowed_item, uri) }
89
89
  when Regexp
90
- (uri.to_s =~ allowed) != nil ||
91
- (uri.omit(:port).to_s =~ allowed) != nil && uri.port == uri.default_port
90
+ allowed.match?(uri.to_s) ||
91
+ (uri.port == uri.default_port && allowed.match?(uri.omit(:port).to_s))
92
92
  when String
93
- allowed == uri.to_s ||
94
93
  allowed == uri.host ||
95
94
  allowed == "#{uri.host}:#{uri.port}" ||
96
- allowed == "#{uri.scheme}://#{uri.host}:#{uri.port}" ||
97
- allowed == "#{uri.scheme}://#{uri.host}" && uri.port == uri.default_port
95
+ net_connect_allowed_url?(allowed, uri)
98
96
  else
99
97
  if allowed.respond_to?(:call)
100
98
  allowed.call(uri)
@@ -102,6 +100,28 @@ module WebMock
102
100
  end
103
101
  end
104
102
 
103
+ def self.net_connect_allowed_url?(allowed, uri)
104
+ return false unless allowed.match?(%r{\Ahttps?://}i)
105
+
106
+ # Normalization turns an empty path into "/", so check the original URL.
107
+ allowed_uri = Addressable::URI.parse(allowed)
108
+ allow_any_path = allowed_uri.path.empty? && allowed_uri.query.nil?
109
+
110
+ # Connection URIs have no request path to match yet.
111
+ return false if uri.path.empty? && !allowed_uri.path.empty?
112
+
113
+ allowed_uri = WebMock::Util::URI.normalize_uri(allowed_uri)
114
+ request_uri = WebMock::Util::URI.normalize_uri(uri)
115
+
116
+ return false unless allowed_uri.origin == request_uri.origin
117
+ return true if allow_any_path
118
+
119
+ allowed_uri.path == request_uri.path && allowed_uri.query == request_uri.query
120
+ rescue Addressable::URI::InvalidURIError
121
+ false
122
+ end
123
+ private_class_method :net_connect_allowed_url?
124
+
105
125
  def self.show_body_diff!
106
126
  Config.instance.show_body_diff = true
107
127
  end
data/lib/webmock.rb CHANGED
@@ -13,6 +13,7 @@ require_relative 'webmock/errors'
13
13
  require_relative 'webmock/util/query_mapper'
14
14
  require_relative 'webmock/util/uri'
15
15
  require_relative 'webmock/util/headers'
16
+ require_relative 'webmock/util/content_type'
16
17
  require_relative 'webmock/util/hash_counter'
17
18
  require_relative 'webmock/util/hash_keys_stringifier'
18
19
  require_relative 'webmock/util/values_stringifier'
@@ -36,6 +37,7 @@ require_relative 'webmock/rack_response'
36
37
  require_relative 'webmock/stub_request_snippet'
37
38
  require_relative 'webmock/request_signature_snippet'
38
39
  require_relative 'webmock/request_body_diff'
40
+ require_relative 'webmock/json_body_hint'
39
41
 
40
42
  require_relative 'webmock/assertion_failure'
41
43
  require_relative 'webmock/request_execution_verifier'
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: webmock
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.26.4
4
+ version: 3.30.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Bartosz Blimke
@@ -318,6 +318,7 @@ files:
318
318
  - lib/webmock/http_lib_adapters/net_http_response.rb
319
319
  - lib/webmock/http_lib_adapters/patron_adapter.rb
320
320
  - lib/webmock/http_lib_adapters/typhoeus_hydra_adapter.rb
321
+ - lib/webmock/json_body_hint.rb
321
322
  - lib/webmock/matchers/any_arg_matcher.rb
322
323
  - lib/webmock/matchers/hash_argument_matcher.rb
323
324
  - lib/webmock/matchers/hash_excluding_matcher.rb
@@ -340,6 +341,7 @@ files:
340
341
  - lib/webmock/stub_registry.rb
341
342
  - lib/webmock/stub_request_snippet.rb
342
343
  - lib/webmock/test_unit.rb
344
+ - lib/webmock/util/content_type.rb
343
345
  - lib/webmock/util/hash_counter.rb
344
346
  - lib/webmock/util/hash_keys_stringifier.rb
345
347
  - lib/webmock/util/hash_validator.rb
@@ -358,9 +360,9 @@ licenses:
358
360
  - MIT
359
361
  metadata:
360
362
  bug_tracker_uri: https://github.com/bblimke/webmock/issues
361
- changelog_uri: https://github.com/bblimke/webmock/blob/v3.26.4/CHANGELOG.md
362
- documentation_uri: https://www.rubydoc.info/gems/webmock/3.26.4
363
- source_code_uri: https://github.com/bblimke/webmock/tree/v3.26.4
363
+ changelog_uri: https://github.com/bblimke/webmock/blob/v3.30.0/CHANGELOG.md
364
+ documentation_uri: https://www.rubydoc.info/gems/webmock/3.30.0
365
+ source_code_uri: https://github.com/bblimke/webmock/tree/v3.30.0
364
366
  wiki_uri: https://github.com/bblimke/webmock/wiki
365
367
  rdoc_options: []
366
368
  require_paths:
@@ -376,7 +378,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
376
378
  - !ruby/object:Gem::Version
377
379
  version: '0'
378
380
  requirements: []
379
- rubygems_version: 4.0.16
381
+ rubygems_version: 4.0.20
380
382
  specification_version: 4
381
383
  summary: Library for stubbing HTTP requests in Ruby.
382
384
  test_files: []