webmock 3.26.4 → 3.34.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.
@@ -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
@@ -10,6 +10,10 @@ module WebMock
10
10
  def ==(other)
11
11
  true
12
12
  end
13
+
14
+ def inspect
15
+ "anything"
16
+ end
13
17
  end
14
18
  end
15
19
  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
@@ -27,47 +27,56 @@ module WebMock
27
27
 
28
28
  def build_rack_env(request)
29
29
  uri = request.uri
30
+ port = uri.inferred_port
30
31
  headers = (request.headers || {}).dup
31
- body = request.body || ''
32
+ body = (request.body || '').b
33
+ content_type = headers.delete('Content-Type')
34
+ headers.delete('Content-Length')
35
+ # The body is already decoded, as a server passes it on.
36
+ headers.delete('Transfer-Encoding')
32
37
 
33
38
  env = {
34
39
  # CGI variables specified by Rack
35
- 'REQUEST_METHOD' => request.method.to_s.upcase,
36
- 'CONTENT_TYPE' => headers.delete('Content-Type'),
37
- 'CONTENT_LENGTH' => body.bytesize,
38
- 'PATH_INFO' => uri.path,
39
- 'QUERY_STRING' => uri.query || '',
40
- 'SERVER_NAME' => uri.host,
41
- 'SERVER_PORT' => uri.port,
42
- 'SCRIPT_NAME' => ""
40
+ 'REQUEST_METHOD' => request.method.to_s.upcase,
41
+ 'CONTENT_LENGTH' => body.bytesize.to_s,
42
+ 'PATH_INFO' => uri.path,
43
+ 'QUERY_STRING' => uri.query || '',
44
+ 'SERVER_NAME' => uri.host,
45
+ 'SERVER_PORT' => port.to_s,
46
+ 'SERVER_PROTOCOL' => 'HTTP/1.1',
47
+ 'SCRIPT_NAME' => ""
43
48
  }
49
+ env['CONTENT_TYPE'] = header_value('Content-Type', content_type) if content_type
44
50
 
45
51
  env['HTTP_AUTHORIZATION'] = 'Basic ' + [uri.userinfo].pack('m').delete("\r\n") if uri.userinfo
46
52
 
47
53
  # Rack-specific variables
48
54
  env['rack.input'] = StringIO.new(body)
49
55
  env['rack.errors'] = $stderr
50
- if !Rack.const_defined?(:RELEASE) || Rack::RELEASE < "3"
51
- env['rack.version'] = Rack::VERSION
56
+ if !Rack.const_defined?(:RELEASE) || Gem::Version.new(Rack::RELEASE) < Gem::Version.new("3")
57
+ env['rack.version'] = Rack::VERSION
58
+ env['rack.multithread'] = false
59
+ env['rack.multiprocess'] = false
52
60
  end
53
61
  env['rack.url_scheme'] = uri.scheme
54
62
  env['rack.run_once'] = true
55
- env['rack.session'] = session
56
- env['rack.session.options'] = session_options
57
63
 
58
64
  headers.each do |k, v|
59
- env["HTTP_#{k.tr('-','_').upcase}"] = v
65
+ env["HTTP_#{k.tr('-','_').upcase}"] = header_value(k, v)
60
66
  end
61
67
 
68
+ # Many clients add Host only when sending, so fill it in as they would.
69
+ env['HTTP_HOST'] ||= port == uri.default_port ? uri.host : "#{uri.host}:#{port}"
70
+
62
71
  env
63
72
  end
64
73
 
65
- def session
66
- @session ||= {}
67
- end
74
+ private
68
75
 
69
- def session_options
70
- @session_options ||= {}
76
+ # Like a server, pass header values as bytes and join repeated ones. Rack
77
+ # splits cookies on "; ", so join those that way.
78
+ def header_value(name, value)
79
+ Array(value).map { |v| v.to_s.b }.join(name == 'Cookie' ? '; ' : WebMock::Util::Headers::JOIN)
71
80
  end
72
81
  end
73
82
  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)} "
@@ -10,11 +10,15 @@ module WebMock
10
10
  def rSpecHashExcludingMatcher?(matcher)
11
11
  matcher.class.name =~ /R?Spec::Mocks::ArgumentMatchers::HashExcludingMatcher/
12
12
  end
13
+
14
+ def rSpecAnyArgMatcher?(matcher)
15
+ matcher.class.name =~ /R?Spec::Mocks::ArgumentMatchers::AnyArgMatcher/
16
+ end
13
17
  end
14
18
 
15
19
  class RequestPattern
16
20
 
17
- attr_reader :method_pattern, :uri_pattern, :body_pattern, :headers_pattern
21
+ attr_reader :method_pattern, :uri_pattern, :body_pattern, :headers_pattern, :with_block
18
22
 
19
23
  def initialize(method, uri, options = {})
20
24
  @method_pattern = MethodPattern.new(method)
@@ -33,11 +37,9 @@ module WebMock
33
37
  end
34
38
 
35
39
  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
40
  @method_pattern.matches?(request_signature.method) &&
39
41
  @uri_pattern.matches?(request_signature.uri) &&
40
- (@body_pattern.nil? || @body_pattern.matches?(request_signature.body, content_type || "")) &&
42
+ (@body_pattern.nil? || @body_pattern.matches?(request_signature.body, request_signature.content_type)) &&
41
43
  (@headers_pattern.nil? || @headers_pattern.matches?(request_signature.headers)) &&
42
44
  (@with_block.nil? || @with_block.call(request_signature))
43
45
  end
@@ -96,8 +98,11 @@ module WebMock
96
98
 
97
99
 
98
100
  class MethodPattern
101
+ # Request signatures store the method as a lower case Symbol, so a method
102
+ # given as a String or in upper case, like "get" or :GET, matches too.
103
+ # Anything else, such as nil, is kept as given and never matches.
99
104
  def initialize(pattern)
100
- @pattern = pattern
105
+ @pattern = pattern.respond_to?(:to_sym) ? pattern.to_sym.downcase : pattern
101
106
  end
102
107
 
103
108
  def matches?(method)
@@ -128,6 +133,10 @@ module WebMock
128
133
  def add_query_params(query_params)
129
134
  @query_params = if query_params.is_a?(Hash)
130
135
  query_params
136
+ elsif query_params.equal?(:any) \
137
+ || query_params.is_a?(WebMock::Matchers::AnyArgMatcher) \
138
+ || rSpecAnyArgMatcher?(query_params)
139
+ WebMock::Matchers::AnyArgMatcher.new(nil)
131
140
  elsif query_params.is_a?(WebMock::Matchers::HashIncludingMatcher) \
132
141
  || query_params.is_a?(WebMock::Matchers::HashExcludingMatcher)
133
142
  query_params
@@ -135,8 +144,10 @@ module WebMock
135
144
  WebMock::Matchers::HashIncludingMatcher.from_rspec_matcher(query_params)
136
145
  elsif rSpecHashExcludingMatcher?(query_params)
137
146
  WebMock::Matchers::HashExcludingMatcher.from_rspec_matcher(query_params)
138
- else
147
+ elsif query_params.nil? || query_params.is_a?(String)
139
148
  WebMock::Util::QueryMapper.query_to_values(query_params, notation: Config.instance.query_values_notation)
149
+ else
150
+ raise ArgumentError, "Query should be a String, Hash, :any, nil, or a hash_including or hash_excluding matcher. Got: #{query_params.class}"
140
151
  end
141
152
  end
142
153
 
@@ -152,6 +163,10 @@ module WebMock
152
163
 
153
164
  private
154
165
 
166
+ def raise_any_query_with_uri_query
167
+ raise ArgumentError, "query: :any can't be combined with a query in the URI #{pattern_inspect}, because no request would match. Remove the query from the URI."
168
+ end
169
+
155
170
  def pattern_inspect
156
171
  @pattern.inspect
157
172
  end
@@ -179,12 +194,17 @@ module WebMock
179
194
 
180
195
  class URIAddressablePattern < URIPattern
181
196
  def add_query_params(query_params)
197
+ super(query_params)
198
+ # With query params, the template is matched against the URI without its
199
+ # query, so a literal query in the template can never match.
200
+ if @query_params.is_a?(WebMock::Matchers::AnyArgMatcher) && @pattern.pattern.gsub(/\{[^}]*\}/, '').include?('?')
201
+ raise_any_query_with_uri_query
202
+ end
182
203
  @@add_query_params_warned ||= false
183
204
  if not @@add_query_params_warned
184
205
  @@add_query_params_warned = true
185
206
  warn "WebMock warning: ignoring query params in RFC 6570 template and checking them with WebMock"
186
207
  end
187
- super(query_params)
188
208
  end
189
209
 
190
210
  private
@@ -229,6 +249,9 @@ module WebMock
229
249
  query_hash = (WebMock::Util::QueryMapper.query_to_values(@pattern.query, notation: Config.instance.query_values_notation) || {}).merge(@query_params)
230
250
  @pattern.query = WebMock::Util::QueryMapper.values_to_query(query_hash, notation: WebMock::Config.instance.query_values_notation)
231
251
  @query_params = nil
252
+ elsif @query_params.is_a?(WebMock::Matchers::AnyArgMatcher) && @pattern.query
253
+ # The URI is then compared without its query, so it could never match.
254
+ raise_any_query_with_uri_query
232
255
  end
233
256
  end
234
257
 
@@ -255,18 +278,8 @@ module WebMock
255
278
  class BodyPattern
256
279
  include RSpecMatcherDetector
257
280
 
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
- }
281
+ # Kept so existing references to the constant still resolve.
282
+ BODY_FORMATS = WebMock::Util::ContentType::BODY_FORMATS
270
283
 
271
284
  attr_reader :pattern
272
285
 
@@ -275,12 +288,16 @@ module WebMock
275
288
  normalize_hash(pattern)
276
289
  elsif rSpecHashIncludingMatcher?(pattern)
277
290
  WebMock::Matchers::HashIncludingMatcher.from_rspec_matcher(pattern)
291
+ elsif rSpecHashExcludingMatcher?(pattern)
292
+ WebMock::Matchers::HashExcludingMatcher.from_rspec_matcher(pattern)
278
293
  else
279
294
  pattern
280
295
  end
281
296
  end
282
297
 
283
298
  def matches?(body, content_type = "")
299
+ # Checked on the raw value, so body patterns used with mixed case
300
+ # multipart content types keep working as in earlier versions.
284
301
  assert_non_multipart_body(content_type)
285
302
 
286
303
  if (@pattern).is_a?(Hash)
@@ -304,7 +321,9 @@ module WebMock
304
321
  private
305
322
 
306
323
  def body_as_hash(body, content_type)
307
- case body_format(content_type)
324
+ return if body.nil?
325
+
326
+ case WebMock::Util::ContentType.body_format(content_type)
308
327
  when :json then
309
328
  WebMock::Util::Parsers::JSON.parse(body)
310
329
  when :xml then
@@ -316,11 +335,6 @@ module WebMock
316
335
  nil
317
336
  end
318
337
 
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
338
  def assert_non_multipart_body(content_type)
325
339
  if content_type =~ %r{^multipart/form-data}
326
340
  raise ArgumentError.new("WebMock does not support matching body for multipart/form-data requests yet :(")
@@ -376,7 +390,7 @@ module WebMock
376
390
  return matching_body_hashes?(actual, expected, content_type) if actual.is_a?(Hash) && expected.is_a?(Hash)
377
391
  return matching_body_array?(actual, expected, content_type) if actual.is_a?(Array) && expected.is_a?(Array)
378
392
 
379
- expected = WebMock::Util::ValuesStringifier.stringify_values(expected) if url_encoded_body?(content_type)
393
+ expected = WebMock::Util::ValuesStringifier.stringify_values(expected) if WebMock::Util::ContentType.url_encoded?(content_type)
380
394
  expected === actual
381
395
  end
382
396
 
@@ -387,10 +401,6 @@ module WebMock
387
401
  def normalize_hash(hash)
388
402
  Hash[WebMock::Util::HashKeysStringifier.stringify_keys!(hash, deep: true).sort]
389
403
  end
390
-
391
- def url_encoded_body?(content_type)
392
- content_type =~ %r{^application/x-www-form-urlencoded}
393
- end
394
404
  end
395
405
 
396
406
  class HeadersPattern
@@ -4,11 +4,11 @@ module WebMock
4
4
 
5
5
  class RequestSignature
6
6
 
7
- attr_accessor :method, :uri, :body
8
- attr_reader :headers
7
+ attr_accessor :uri, :body
8
+ attr_reader :method, :headers
9
9
 
10
10
  def initialize(method, uri, options = {})
11
- self.method = method.to_sym
11
+ self.method = method
12
12
  self.uri = uri.is_a?(Addressable::URI) ? uri : WebMock::Util::URI.normalize_uri(uri)
13
13
  assign_options(options)
14
14
  end
@@ -23,6 +23,10 @@ module WebMock
23
23
  string
24
24
  end
25
25
 
26
+ def method=(method)
27
+ @method = method.to_sym.downcase
28
+ end
29
+
26
30
  def headers=(headers)
27
31
  @headers = WebMock::Util::Headers.normalize_headers(headers)
28
32
  end
@@ -36,12 +40,18 @@ module WebMock
36
40
  end
37
41
  alias == eql?
38
42
 
43
+ # A request can carry Content-Type more than once, in which case the
44
+ # normalized header value is an Array. Use its first value.
45
+ def content_type
46
+ Array(headers&.fetch('Content-Type', nil)).first
47
+ end
48
+
39
49
  def url_encoded?
40
- !!(headers&.fetch('Content-Type', nil)&.start_with?('application/x-www-form-urlencoded'))
50
+ WebMock::Util::ContentType.url_encoded?(content_type)
41
51
  end
42
52
 
43
53
  def json_headers?
44
- !!(headers&.fetch('Content-Type', nil)&.start_with?('application/json'))
54
+ WebMock::Util::ContentType.body_format(content_type) == :json
45
55
  end
46
56
 
47
57
  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)
@@ -39,13 +39,10 @@ module WebMock
39
39
  else
40
40
  body.call
41
41
  end
42
- b = b.to_json unless b.is_a?(String)
43
- b
42
+ json_response_body(b)
44
43
  }
45
- elsif !body.is_a?(String)
46
- body.to_json
47
44
  else
48
- body
45
+ json_response_body(body)
49
46
  end
50
47
 
51
48
  resp_h.merge(
@@ -130,5 +127,16 @@ module WebMock
130
127
  end
131
128
  stub
132
129
  end
130
+
131
+ private
132
+
133
+ def json_response_body(body)
134
+ case body
135
+ when String, IO, Pathname
136
+ body
137
+ else
138
+ body.to_json
139
+ end
140
+ end
133
141
  end
134
142
  end
@@ -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
@@ -58,8 +58,8 @@ module WebMock
58
58
 
59
59
  def self.strip_default_port_from_uri_string(uri_string)
60
60
  case uri_string
61
- when %r{^http://} then uri_string.sub(%r{:80(/|$)}, '\1')
62
- when %r{^https://} then uri_string.sub(%r{:443(/|$)}, '\1')
61
+ when %r{^http://} then uri_string.sub(%r{\A(http://[^/?#]+):80(?=[/?#]|\z)}, '\1')
62
+ when %r{^https://} then uri_string.sub(%r{\A(https://[^/?#]+):443(?=[/?#]|\z)}, '\1')
63
63
  else uri_string
64
64
  end
65
65
  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.34.0' unless defined?(::WebMock::VERSION)
5
5
  end