webmock 3.27.0 → 3.29.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: 9f11e25e75b578b0d4ce7d2c98cf02b893872c9766d7b707a21fb79e0293963c
4
- data.tar.gz: 205c01525a5240686ca49c63be018e962db268173bcd68406aef76a5452cc96b
3
+ metadata.gz: dd5a0e377558900faf49baabeca47478c71196acd38c625b30ed13d48cb3945a
4
+ data.tar.gz: 7956de226b2fa9ad8f692319e6ea5ea4ca666c77c48488feb58470e79474ce26
5
5
  SHA512:
6
- metadata.gz: b10e29592ef2d90396ec90b2d5717584e942748eccebe200748609295be1111616b14c706c503ad54543f3ee90e54e0e06ddfd9860b3ed0e093cda225df714a4
7
- data.tar.gz: cc80e50037959e1b9ff6fa600eea3c309e57c3aa6083d40e11288aecc1e81ebf1115cf07c10fba310543a58161674c8b62e829c7e5eedb9dadb8e6e81e894263
6
+ metadata.gz: dd9d32d168c7a3e3719b467c519393158cf1b279213acc1e515ac0ff1f88f4f60bc4831e975eae23d18b2fb0d08c84390b180ce52f68a492852d1981dfa5e29d
7
+ data.tar.gz: e4bb8d187695bde02c1bcadd89923b24d26e2174676f094ac93cc551e5e5688b242ee519c565151ad7423162557d47ee86bb97c981529134ae4bebfccbd4d5d2
data/CHANGELOG.md CHANGED
@@ -1,5 +1,39 @@
1
1
  # Changelog
2
2
 
3
+ # 3.29.0
4
+
5
+ * 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)
6
+
7
+ Thanks to [Olivier Brisse](https://github.com/ouranos)
8
+
9
+ * 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
10
+
11
+ * Suggest declaring a JSON body as a hash when a request fails to match only because the JSON is formatted differently (#649)
12
+
13
+ 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).
14
+
15
+ # 3.28.0
16
+
17
+ * Fix `to_rack` apps receiving a `rack.session` they did not set up (#985)
18
+
19
+ 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.
20
+
21
+ Thanks to [Jan Graichen](https://github.com/jgraichen)
22
+
23
+ * Fix crash when matching a hash body pattern against a JSON or XML request that has no body
24
+
25
+ Thanks to [Chris](https://github.com/ChrisJr404)
26
+
27
+ * No longer warn about curb versions newer than the last one tested
28
+
29
+ * Document regexp values in `hash_including`
30
+
31
+ Thanks to [Christian Bruckmayer](https://github.com/ChrisBr)
32
+
33
+ * Remove unused `RequestSignatureSnippet#request_params`
34
+
35
+ Thanks to [Daniel Pepper](https://github.com/dpep)
36
+
3
37
  # 3.27.0
4
38
 
5
39
  * Fix crash in async-http adapter when a request header has a nil value
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,12 @@ 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
187
195
  ```
188
196
 
189
197
  ### Matching request body against excluding partial hash.
@@ -438,6 +446,8 @@ stub_request(:get, "www.example.com").to_rack(MyRackApp)
438
446
  RestClient.post('www.example.com') # ===> "Hello"
439
447
  ```
440
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
+
441
451
  ### Raising errors
442
452
 
443
453
  #### Exception declared by class
@@ -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,53 @@
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 the pattern declares its body as a JSON object or array string,
21
+ # the request has a JSON content type and a body that parses to the same
22
+ # JSON but is written differently, and the pattern's method, URI and headers
23
+ # match the request. A block given to the pattern is not called.
24
+ def self.applies?(request_pattern, request_signature)
25
+ return false unless request_pattern.is_a?(RequestPattern) && request_pattern.body_pattern
26
+
27
+ expected = request_pattern.body_pattern.pattern
28
+ actual = request_signature.body
29
+ return false unless expected.is_a?(String) && actual.is_a?(String) && expected != actual
30
+ return false unless request_signature.json_headers?
31
+
32
+ expected_json = parse_json(expected)
33
+ return false unless expected_json.is_a?(Hash) || expected_json.is_a?(Array)
34
+ # eql? compares nested values strictly, so 1 and 1.0 are different.
35
+ return false unless expected_json.eql?(parse_json(actual))
36
+
37
+ matches_except_body?(request_pattern, request_signature)
38
+ end
39
+
40
+ def self.matches_except_body?(request_pattern, request_signature)
41
+ request_pattern.method_pattern.matches?(request_signature.method) &&
42
+ request_pattern.uri_pattern.matches?(request_signature.uri) &&
43
+ (request_pattern.headers_pattern.nil? || request_pattern.headers_pattern.matches?(request_signature.headers))
44
+ end
45
+
46
+ def self.parse_json(string)
47
+ ::JSON.parse(string)
48
+ rescue ::JSON::JSONError, EncodingError
49
+ nil
50
+ end
51
+ private_class_method :matches_except_body?, :parse_json
52
+ end
53
+ end
@@ -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)} "
@@ -33,11 +33,9 @@ module WebMock
33
33
  end
34
34
 
35
35
  def matches?(request_signature)
36
- content_type = request_signature.content_type
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
 
@@ -283,6 +271,8 @@ module WebMock
283
271
  end
284
272
 
285
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.
286
276
  assert_non_multipart_body(content_type)
287
277
 
288
278
  if (@pattern).is_a?(Hash)
@@ -306,7 +296,9 @@ module WebMock
306
296
  private
307
297
 
308
298
  def body_as_hash(body, content_type)
309
- case body_format(content_type)
299
+ return if body.nil?
300
+
301
+ case WebMock::Util::ContentType.body_format(content_type)
310
302
  when :json then
311
303
  WebMock::Util::Parsers::JSON.parse(body)
312
304
  when :xml then
@@ -318,11 +310,6 @@ module WebMock
318
310
  nil
319
311
  end
320
312
 
321
- def body_format(content_type)
322
- normalized_content_type = content_type.sub(/\A(application\/)[a-zA-Z0-9.-]+\+(json|xml)\Z/,'\1\2')
323
- BODY_FORMATS[normalized_content_type]
324
- end
325
-
326
313
  def assert_non_multipart_body(content_type)
327
314
  if content_type =~ %r{^multipart/form-data}
328
315
  raise ArgumentError.new("WebMock does not support matching body for multipart/form-data requests yet :(")
@@ -378,7 +365,7 @@ module WebMock
378
365
  return matching_body_hashes?(actual, expected, content_type) if actual.is_a?(Hash) && expected.is_a?(Hash)
379
366
  return matching_body_array?(actual, expected, content_type) if actual.is_a?(Array) && expected.is_a?(Array)
380
367
 
381
- 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)
382
369
  expected === actual
383
370
  end
384
371
 
@@ -389,10 +376,6 @@ module WebMock
389
376
  def normalize_hash(hash)
390
377
  Hash[WebMock::Util::HashKeysStringifier.stringify_keys!(hash, deep: true).sort]
391
378
  end
392
-
393
- def url_encoded_body?(content_type)
394
- content_type =~ %r{^application/x-www-form-urlencoded}
395
- end
396
379
  end
397
380
 
398
381
  class HeadersPattern
@@ -43,11 +43,11 @@ module WebMock
43
43
  end
44
44
 
45
45
  def url_encoded?
46
- !!content_type&.start_with?('application/x-www-form-urlencoded')
46
+ WebMock::Util::ContentType.url_encoded?(content_type)
47
47
  end
48
48
 
49
49
  def json_headers?
50
- !!content_type&.start_with?('application/json')
50
+ WebMock::Util::ContentType.body_format(content_type) == :json
51
51
  end
52
52
 
53
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)
@@ -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.27.0' unless defined?(::WebMock::VERSION)
4
+ VERSION = '3.29.0' unless defined?(::WebMock::VERSION)
5
5
  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,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: webmock
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.27.0
4
+ version: 3.29.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Bartosz Blimke
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2026-10-10 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: addressable
@@ -319,6 +318,7 @@ files:
319
318
  - lib/webmock/http_lib_adapters/net_http_response.rb
320
319
  - lib/webmock/http_lib_adapters/patron_adapter.rb
321
320
  - lib/webmock/http_lib_adapters/typhoeus_hydra_adapter.rb
321
+ - lib/webmock/json_body_hint.rb
322
322
  - lib/webmock/matchers/any_arg_matcher.rb
323
323
  - lib/webmock/matchers/hash_argument_matcher.rb
324
324
  - lib/webmock/matchers/hash_excluding_matcher.rb
@@ -341,6 +341,7 @@ files:
341
341
  - lib/webmock/stub_registry.rb
342
342
  - lib/webmock/stub_request_snippet.rb
343
343
  - lib/webmock/test_unit.rb
344
+ - lib/webmock/util/content_type.rb
344
345
  - lib/webmock/util/hash_counter.rb
345
346
  - lib/webmock/util/hash_keys_stringifier.rb
346
347
  - lib/webmock/util/hash_validator.rb
@@ -359,11 +360,10 @@ licenses:
359
360
  - MIT
360
361
  metadata:
361
362
  bug_tracker_uri: https://github.com/bblimke/webmock/issues
362
- changelog_uri: https://github.com/bblimke/webmock/blob/v3.27.0/CHANGELOG.md
363
- documentation_uri: https://www.rubydoc.info/gems/webmock/3.27.0
364
- source_code_uri: https://github.com/bblimke/webmock/tree/v3.27.0
363
+ changelog_uri: https://github.com/bblimke/webmock/blob/v3.29.0/CHANGELOG.md
364
+ documentation_uri: https://www.rubydoc.info/gems/webmock/3.29.0
365
+ source_code_uri: https://github.com/bblimke/webmock/tree/v3.29.0
365
366
  wiki_uri: https://github.com/bblimke/webmock/wiki
366
- post_install_message:
367
367
  rdoc_options: []
368
368
  require_paths:
369
369
  - lib
@@ -378,8 +378,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
378
378
  - !ruby/object:Gem::Version
379
379
  version: '0'
380
380
  requirements: []
381
- rubygems_version: 3.5.16
382
- signing_key:
381
+ rubygems_version: 4.0.20
383
382
  specification_version: 4
384
383
  summary: Library for stubbing HTTP requests in Ruby.
385
384
  test_files: []