webmock 1.8.6 → 3.14.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.
Files changed (138) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/CI.yml +37 -0
  3. data/.gitignore +6 -0
  4. data/CHANGELOG.md +1198 -0
  5. data/Gemfile +3 -15
  6. data/README.md +761 -305
  7. data/Rakefile +13 -40
  8. data/lib/webmock/api.rb +63 -17
  9. data/lib/webmock/callback_registry.rb +1 -1
  10. data/lib/webmock/config.rb +8 -0
  11. data/lib/webmock/cucumber.rb +2 -0
  12. data/lib/webmock/errors.rb +8 -24
  13. data/lib/webmock/http_lib_adapters/async_http_client_adapter.rb +216 -0
  14. data/lib/webmock/http_lib_adapters/curb_adapter.rb +148 -84
  15. data/lib/webmock/http_lib_adapters/em_http_request_adapter.rb +224 -4
  16. data/lib/webmock/http_lib_adapters/excon_adapter.rb +104 -34
  17. data/lib/webmock/http_lib_adapters/http_rb/client.rb +17 -0
  18. data/lib/webmock/http_lib_adapters/http_rb/request.rb +16 -0
  19. data/lib/webmock/http_lib_adapters/http_rb/response.rb +64 -0
  20. data/lib/webmock/http_lib_adapters/http_rb/streamer.rb +29 -0
  21. data/lib/webmock/http_lib_adapters/http_rb/webmock.rb +68 -0
  22. data/lib/webmock/http_lib_adapters/http_rb_adapter.rb +37 -0
  23. data/lib/webmock/http_lib_adapters/httpclient_adapter.rb +152 -86
  24. data/lib/webmock/http_lib_adapters/manticore_adapter.rb +145 -0
  25. data/lib/webmock/http_lib_adapters/net_http.rb +155 -46
  26. data/lib/webmock/http_lib_adapters/net_http_response.rb +1 -1
  27. data/lib/webmock/http_lib_adapters/patron_adapter.rb +16 -15
  28. data/lib/webmock/http_lib_adapters/typhoeus_hydra_adapter.rb +76 -82
  29. data/lib/webmock/matchers/any_arg_matcher.rb +13 -0
  30. data/lib/webmock/matchers/hash_argument_matcher.rb +21 -0
  31. data/lib/webmock/matchers/hash_excluding_matcher.rb +15 -0
  32. data/lib/webmock/matchers/hash_including_matcher.rb +4 -12
  33. data/lib/webmock/minitest.rb +29 -3
  34. data/lib/webmock/rack_response.rb +14 -7
  35. data/lib/webmock/request_body_diff.rb +64 -0
  36. data/lib/webmock/request_execution_verifier.rb +38 -17
  37. data/lib/webmock/request_pattern.rb +158 -38
  38. data/lib/webmock/request_registry.rb +3 -3
  39. data/lib/webmock/request_signature.rb +7 -3
  40. data/lib/webmock/request_signature_snippet.rb +61 -0
  41. data/lib/webmock/request_stub.rb +9 -6
  42. data/lib/webmock/response.rb +30 -15
  43. data/lib/webmock/rspec/matchers/request_pattern_matcher.rb +38 -2
  44. data/lib/webmock/rspec/matchers/webmock_matcher.rb +23 -2
  45. data/lib/webmock/rspec/matchers.rb +0 -1
  46. data/lib/webmock/rspec.rb +11 -2
  47. data/lib/webmock/stub_registry.rb +31 -10
  48. data/lib/webmock/stub_request_snippet.rb +14 -6
  49. data/lib/webmock/test_unit.rb +4 -4
  50. data/lib/webmock/util/hash_counter.rb +20 -6
  51. data/lib/webmock/util/hash_keys_stringifier.rb +5 -3
  52. data/lib/webmock/util/hash_validator.rb +17 -0
  53. data/lib/webmock/util/headers.rb +23 -2
  54. data/lib/webmock/util/json.rb +20 -7
  55. data/lib/webmock/util/query_mapper.rb +281 -0
  56. data/lib/webmock/util/uri.rb +29 -19
  57. data/lib/webmock/util/values_stringifier.rb +20 -0
  58. data/lib/webmock/util/version_checker.rb +40 -2
  59. data/lib/webmock/version.rb +1 -1
  60. data/lib/webmock/webmock.rb +56 -17
  61. data/lib/webmock.rb +56 -46
  62. data/minitest/test_helper.rb +8 -3
  63. data/minitest/test_webmock.rb +4 -1
  64. data/minitest/webmock_spec.rb +16 -6
  65. data/spec/acceptance/async_http_client/async_http_client_spec.rb +375 -0
  66. data/spec/acceptance/async_http_client/async_http_client_spec_helper.rb +73 -0
  67. data/spec/acceptance/curb/curb_spec.rb +227 -68
  68. data/spec/acceptance/curb/curb_spec_helper.rb +11 -8
  69. data/spec/acceptance/em_http_request/em_http_request_spec.rb +322 -28
  70. data/spec/acceptance/em_http_request/em_http_request_spec_helper.rb +15 -10
  71. data/spec/acceptance/excon/excon_spec.rb +66 -4
  72. data/spec/acceptance/excon/excon_spec_helper.rb +21 -7
  73. data/spec/acceptance/http_rb/http_rb_spec.rb +93 -0
  74. data/spec/acceptance/http_rb/http_rb_spec_helper.rb +54 -0
  75. data/spec/acceptance/httpclient/httpclient_spec.rb +152 -11
  76. data/spec/acceptance/httpclient/httpclient_spec_helper.rb +25 -16
  77. data/spec/acceptance/manticore/manticore_spec.rb +107 -0
  78. data/spec/acceptance/manticore/manticore_spec_helper.rb +35 -0
  79. data/spec/acceptance/net_http/net_http_shared.rb +52 -24
  80. data/spec/acceptance/net_http/net_http_spec.rb +164 -50
  81. data/spec/acceptance/net_http/net_http_spec_helper.rb +19 -10
  82. data/spec/acceptance/net_http/real_net_http_spec.rb +1 -1
  83. data/spec/acceptance/patron/patron_spec.rb +29 -40
  84. data/spec/acceptance/patron/patron_spec_helper.rb +15 -11
  85. data/spec/acceptance/shared/allowing_and_disabling_net_connect.rb +229 -58
  86. data/spec/acceptance/shared/callbacks.rb +32 -30
  87. data/spec/acceptance/shared/complex_cross_concern_behaviors.rb +20 -5
  88. data/spec/acceptance/shared/enabling_and_disabling_webmock.rb +14 -14
  89. data/spec/acceptance/shared/precedence_of_stubs.rb +6 -6
  90. data/spec/acceptance/shared/request_expectations.rb +560 -296
  91. data/spec/acceptance/shared/returning_declared_responses.rb +180 -138
  92. data/spec/acceptance/shared/stubbing_requests.rb +385 -154
  93. data/spec/acceptance/typhoeus/typhoeus_hydra_spec.rb +78 -17
  94. data/spec/acceptance/typhoeus/typhoeus_hydra_spec_helper.rb +19 -15
  95. data/spec/acceptance/webmock_shared.rb +2 -2
  96. data/spec/fixtures/test.txt +1 -0
  97. data/spec/quality_spec.rb +27 -3
  98. data/spec/spec_helper.rb +11 -20
  99. data/spec/support/failures.rb +9 -0
  100. data/spec/support/my_rack_app.rb +8 -3
  101. data/spec/support/network_connection.rb +7 -13
  102. data/spec/support/webmock_server.rb +8 -3
  103. data/spec/unit/api_spec.rb +175 -0
  104. data/spec/unit/errors_spec.rb +116 -19
  105. data/spec/unit/http_lib_adapters/http_lib_adapter_registry_spec.rb +1 -1
  106. data/spec/unit/http_lib_adapters/http_lib_adapter_spec.rb +2 -2
  107. data/spec/unit/matchers/hash_excluding_matcher_spec.rb +61 -0
  108. data/spec/unit/matchers/hash_including_matcher_spec.rb +87 -0
  109. data/spec/unit/rack_response_spec.rb +54 -16
  110. data/spec/unit/request_body_diff_spec.rb +90 -0
  111. data/spec/unit/request_execution_verifier_spec.rb +147 -39
  112. data/spec/unit/request_pattern_spec.rb +462 -198
  113. data/spec/unit/request_registry_spec.rb +29 -9
  114. data/spec/unit/request_signature_snippet_spec.rb +89 -0
  115. data/spec/unit/request_signature_spec.rb +91 -49
  116. data/spec/unit/request_stub_spec.rb +71 -70
  117. data/spec/unit/response_spec.rb +100 -81
  118. data/spec/unit/stub_registry_spec.rb +37 -20
  119. data/spec/unit/stub_request_snippet_spec.rb +51 -31
  120. data/spec/unit/util/hash_counter_spec.rb +6 -6
  121. data/spec/unit/util/hash_keys_stringifier_spec.rb +4 -4
  122. data/spec/unit/util/headers_spec.rb +4 -4
  123. data/spec/unit/util/json_spec.rb +29 -3
  124. data/spec/unit/util/query_mapper_spec.rb +157 -0
  125. data/spec/unit/util/uri_spec.rb +150 -36
  126. data/spec/unit/util/version_checker_spec.rb +15 -9
  127. data/spec/unit/webmock_spec.rb +57 -4
  128. data/test/http_request.rb +3 -3
  129. data/test/shared_test.rb +45 -13
  130. data/test/test_helper.rb +1 -1
  131. data/test/test_webmock.rb +6 -0
  132. data/webmock.gemspec +30 -11
  133. metadata +308 -199
  134. data/.rvmrc +0 -1
  135. data/.travis.yml +0 -11
  136. data/Guardfile +0 -24
  137. data/lib/webmock/http_lib_adapters/em_http_request/em_http_request_0_x.rb +0 -151
  138. data/lib/webmock/http_lib_adapters/em_http_request/em_http_request_1_x.rb +0 -210
@@ -0,0 +1,21 @@
1
+ module WebMock
2
+ module Matchers
3
+ # Base class for Hash matchers
4
+ # https://github.com/rspec/rspec-mocks/blob/master/lib/rspec/mocks/argument_matchers.rb
5
+ class HashArgumentMatcher
6
+ def initialize(expected)
7
+ @expected = Hash[WebMock::Util::HashKeysStringifier.stringify_keys!(expected, deep: true).sort]
8
+ end
9
+
10
+ def ==(_actual, &block)
11
+ @expected.all?(&block)
12
+ rescue NoMethodError
13
+ false
14
+ end
15
+
16
+ def self.from_rspec_matcher(matcher)
17
+ new(matcher.instance_variable_get(:@expected))
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,15 @@
1
+ module WebMock
2
+ module Matchers
3
+ # this is a based on RSpec::Mocks::ArgumentMatchers::HashExcludingMatcher
4
+ # https://github.com/rspec/rspec-mocks/blob/master/lib/rspec/mocks/argument_matchers.rb
5
+ class HashExcludingMatcher < HashArgumentMatcher
6
+ def ==(actual)
7
+ super { |key, value| !actual.key?(key) || value != actual[key] }
8
+ end
9
+
10
+ def inspect
11
+ "hash_excluding(#{@expected.inspect})"
12
+ end
13
+ end
14
+ end
15
+ end
@@ -1,14 +1,10 @@
1
1
  module WebMock
2
2
  module Matchers
3
- #this is a based on RSpec::Mocks::ArgumentMatchers::HashIncludingMatcher
4
- #https://github.com/rspec/rspec-mocks/blob/master/lib/rspec/mocks/argument_matchers.rb
5
- class HashIncludingMatcher
6
- def initialize(expected)
7
- @expected = Hash[WebMock::Util::HashKeysStringifier.stringify_keys!(expected).sort]
8
- end
9
-
3
+ # this is a based on RSpec::Mocks::ArgumentMatchers::HashIncludingMatcher
4
+ # https://github.com/rspec/rspec-mocks/blob/master/lib/rspec/mocks/argument_matchers.rb
5
+ class HashIncludingMatcher < HashArgumentMatcher
10
6
  def ==(actual)
11
- @expected.all? {|k,v| actual.has_key?(k) && v == actual[k]}
7
+ super { |key, value| actual.key?(key) && value === actual[key] }
12
8
  rescue NoMethodError
13
9
  false
14
10
  end
@@ -16,10 +12,6 @@ module WebMock
16
12
  def inspect
17
13
  "hash_including(#{@expected.inspect})"
18
14
  end
19
-
20
- def self.from_rspec_matcher(matcher)
21
- new(matcher.instance_variable_get(:@expected))
22
- end
23
15
  end
24
16
  end
25
17
  end
@@ -1,7 +1,18 @@
1
- require 'minitest/unit'
1
+ begin
2
+ require 'minitest/test'
3
+ test_class = Minitest::Test
4
+ assertions = "assertions"
5
+ rescue LoadError
6
+ require "minitest/unit"
7
+ test_class = MiniTest::Unit::TestCase
8
+ assertions = "_assertions"
9
+ end
10
+
2
11
  require 'webmock'
3
12
 
4
- MiniTest::Unit::TestCase.class_eval do
13
+ WebMock.enable!
14
+
15
+ test_class.class_eval do
5
16
  include WebMock::API
6
17
 
7
18
  alias_method :teardown_without_webmock, :teardown
@@ -10,6 +21,21 @@ MiniTest::Unit::TestCase.class_eval do
10
21
  WebMock.reset!
11
22
  end
12
23
  alias_method :teardown, :teardown_with_webmock
24
+
25
+ [:assert_request_requested, :assert_request_not_requested].each do |name|
26
+ alias_method :"#{name}_without_assertions_count", name
27
+ define_method :"#{name}_with_assertions_count" do |*args|
28
+ self.send("#{assertions}=", self.send("#{assertions}") + 1)
29
+ send :"#{name}_without_assertions_count", *args
30
+ end
31
+ alias_method name, :"#{name}_with_assertions_count"
32
+ end
33
+ end
34
+
35
+ begin
36
+ error_class = MiniTest::Assertion
37
+ rescue NameError
38
+ error_class = Minitest::Assertion
13
39
  end
14
40
 
15
- WebMock::AssertionFailure.error_class = MiniTest::Assertion
41
+ WebMock::AssertionFailure.error_class = error_class
@@ -10,14 +10,14 @@ module WebMock
10
10
  status, headers, response = @app.call(env)
11
11
 
12
12
  Response.new(
13
- :body => body_from_rack_response(response),
14
- :headers => headers,
15
- :status => status
13
+ body: body_from_rack_response(response),
14
+ headers: headers,
15
+ status: [status, Rack::Utils::HTTP_STATUS_CODES[status]]
16
16
  )
17
17
  end
18
18
 
19
19
  def body_from_rack_response(response)
20
- body = ""
20
+ body = "".dup
21
21
  response.each { |line| body << line }
22
22
  response.close if response.respond_to?(:close)
23
23
  return body
@@ -25,28 +25,31 @@ module WebMock
25
25
 
26
26
  def build_rack_env(request)
27
27
  uri = request.uri
28
- headers = request.headers || {}
28
+ headers = (request.headers || {}).dup
29
29
  body = request.body || ''
30
30
 
31
31
  env = {
32
32
  # CGI variables specified by Rack
33
33
  'REQUEST_METHOD' => request.method.to_s.upcase,
34
34
  'CONTENT_TYPE' => headers.delete('Content-Type'),
35
- 'CONTENT_LENGTH' => body.size,
35
+ 'CONTENT_LENGTH' => body.bytesize,
36
36
  'PATH_INFO' => uri.path,
37
37
  'QUERY_STRING' => uri.query || '',
38
38
  'SERVER_NAME' => uri.host,
39
- 'SERVER_PORT' => uri.port
39
+ 'SERVER_PORT' => uri.port,
40
+ 'SCRIPT_NAME' => ""
40
41
  }
41
42
 
42
43
  env['HTTP_AUTHORIZATION'] = 'Basic ' + [uri.userinfo].pack('m').delete("\r\n") if uri.userinfo
43
44
 
44
45
  # Rack-specific variables
45
46
  env['rack.input'] = StringIO.new(body)
47
+ env['rack.errors'] = $stderr
46
48
  env['rack.version'] = Rack::VERSION
47
49
  env['rack.url_scheme'] = uri.scheme
48
50
  env['rack.run_once'] = true
49
51
  env['rack.session'] = session
52
+ env['rack.session.options'] = session_options
50
53
 
51
54
  headers.each do |k, v|
52
55
  env["HTTP_#{k.tr('-','_').upcase}"] = v
@@ -58,5 +61,9 @@ module WebMock
58
61
  def session
59
62
  @session ||= {}
60
63
  end
64
+
65
+ def session_options
66
+ @session_options ||= {}
67
+ end
61
68
  end
62
69
  end
@@ -0,0 +1,64 @@
1
+ require "hashdiff"
2
+ require "json"
3
+
4
+ module WebMock
5
+ class RequestBodyDiff
6
+
7
+ def initialize(request_signature, request_stub)
8
+ @request_signature = request_signature
9
+ @request_stub = request_stub
10
+ end
11
+
12
+ def body_diff
13
+ return {} unless request_signature_diffable? && request_stub_diffable?
14
+
15
+ Hashdiff.diff(request_signature_body_hash, request_stub_body_hash)
16
+ end
17
+
18
+ attr_reader :request_signature, :request_stub
19
+ private :request_signature, :request_stub
20
+
21
+ private
22
+
23
+ def request_signature_diffable?
24
+ request_signature.json_headers? && request_signature_parseable_json?
25
+ end
26
+
27
+ def request_stub_diffable?
28
+ request_stub_body.is_a?(Hash) || request_stub_parseable_json?
29
+ end
30
+
31
+ def request_signature_body_hash
32
+ JSON.parse(request_signature.body)
33
+ end
34
+
35
+ def request_stub_body_hash
36
+ return request_stub_body if request_stub_body.is_a?(Hash)
37
+
38
+ JSON.parse(request_stub_body)
39
+ end
40
+
41
+ def request_stub_body
42
+ request_stub.request_pattern &&
43
+ request_stub.request_pattern.body_pattern &&
44
+ request_stub.request_pattern.body_pattern.pattern
45
+ end
46
+
47
+ def request_signature_parseable_json?
48
+ parseable_json?(request_signature.body)
49
+ end
50
+
51
+ def request_stub_parseable_json?
52
+ parseable_json?(request_stub_body)
53
+ end
54
+
55
+ def parseable_json?(body_pattern)
56
+ return false unless body_pattern.is_a?(String)
57
+
58
+ JSON.parse(body_pattern)
59
+ true
60
+ rescue JSON::ParserError
61
+ false
62
+ end
63
+ end
64
+ end
@@ -1,17 +1,26 @@
1
1
  module WebMock
2
2
  class RequestExecutionVerifier
3
3
 
4
- attr_accessor :request_pattern, :expected_times_executed, :times_executed
4
+ attr_accessor :request_pattern, :expected_times_executed, :times_executed, :at_least_times_executed, :at_most_times_executed
5
5
 
6
- def initialize(request_pattern = nil, expected_times_executed = nil)
6
+ def initialize(request_pattern = nil, expected_times_executed = nil, at_least_times_executed = nil, at_most_times_executed = nil)
7
7
  @request_pattern = request_pattern
8
8
  @expected_times_executed = expected_times_executed
9
+ @at_least_times_executed = at_least_times_executed
10
+ @at_most_times_executed = at_most_times_executed
9
11
  end
10
12
 
11
13
  def matches?
12
14
  @times_executed =
13
- RequestRegistry.instance.times_executed(@request_pattern)
14
- @times_executed == (@expected_times_executed || 1)
15
+ RequestRegistry.instance.times_executed(@request_pattern)
16
+
17
+ if @at_least_times_executed
18
+ @times_executed >= @at_least_times_executed
19
+ elsif @at_most_times_executed
20
+ @times_executed <= @at_most_times_executed
21
+ else
22
+ @times_executed == (@expected_times_executed || 1)
23
+ end
15
24
  end
16
25
 
17
26
  def does_not_match?
@@ -24,30 +33,42 @@ module WebMock
24
33
  end
25
34
  end
26
35
 
36
+ def description
37
+ "request #{request_pattern} #{quantity_phrase.strip}"
38
+ end
27
39
 
28
40
  def failure_message
29
- expected_times_executed = @expected_times_executed || 1
30
- text = %Q(The request #{request_pattern.to_s} was expected to execute #{times(expected_times_executed)} but it executed #{times(times_executed)})
31
- text << self.class.executed_requests_message
32
- text
41
+ failure_message_phrase(false)
33
42
  end
34
43
 
35
- def negative_failure_message
36
- text = if @expected_times_executed
37
- %Q(The request #{request_pattern.to_s} was not expected to execute #{times(expected_times_executed)} but it executed #{times(times_executed)})
38
- else
39
- %Q(The request #{request_pattern.to_s} was expected to execute 0 times but it executed #{times(times_executed)})
40
- end
41
- text << self.class.executed_requests_message
42
- text
44
+ def failure_message_when_negated
45
+ failure_message_phrase(true)
43
46
  end
44
47
 
45
48
  def self.executed_requests_message
46
- "\n\nThe following requests were made:\n\n#{RequestRegistry.instance.to_s}\n" + "="*60
49
+ "\n\nThe following requests were made:\n\n#{RequestRegistry.instance}\n" + "="*60
47
50
  end
48
51
 
49
52
  private
50
53
 
54
+ def failure_message_phrase(is_negated=false)
55
+ negation = is_negated ? "was not" : "was"
56
+ "The request #{request_pattern} #{negation} expected to execute #{quantity_phrase(is_negated)}but it executed #{times(times_executed)}" +
57
+ self.class.executed_requests_message
58
+ end
59
+
60
+ def quantity_phrase(is_negated=false)
61
+ if @at_least_times_executed
62
+ "at least #{times(@at_least_times_executed)} "
63
+ elsif @at_most_times_executed
64
+ "at most #{times(@at_most_times_executed)} "
65
+ elsif @expected_times_executed
66
+ "#{times(@expected_times_executed)} "
67
+ else
68
+ is_negated ? "" : "#{times(1)} "
69
+ end
70
+ end
71
+
51
72
  def times(times)
52
73
  "#{times} time#{ (times == 1) ? '' : 's'}"
53
74
  end
@@ -4,6 +4,10 @@ module WebMock
4
4
  def rSpecHashIncludingMatcher?(matcher)
5
5
  matcher.class.name =~ /R?Spec::Mocks::ArgumentMatchers::HashIncludingMatcher/
6
6
  end
7
+
8
+ def rSpecHashExcludingMatcher?(matcher)
9
+ matcher.class.name =~ /R?Spec::Mocks::ArgumentMatchers::HashExcludingMatcher/
10
+ end
7
11
  end
8
12
 
9
13
  class RequestPattern
@@ -20,6 +24,7 @@ module WebMock
20
24
  end
21
25
 
22
26
  def with(options = {}, &block)
27
+ raise ArgumentError.new('#with method invoked with no arguments. Either options hash or block must be specified. Created a block with do..end? Try creating it with curly braces {} instead.') if options.empty? && !block_given?
23
28
  assign_options(options)
24
29
  @with_block = block
25
30
  self
@@ -27,6 +32,7 @@ module WebMock
27
32
 
28
33
  def matches?(request_signature)
29
34
  content_type = request_signature.headers['Content-Type'] if request_signature.headers
35
+ content_type = content_type.split(';').first if content_type
30
36
  @method_pattern.matches?(request_signature.method) &&
31
37
  @uri_pattern.matches?(request_signature.uri) &&
32
38
  (@body_pattern.nil? || @body_pattern.matches?(request_signature.body, content_type || "")) &&
@@ -35,7 +41,7 @@ module WebMock
35
41
  end
36
42
 
37
43
  def to_s
38
- string = "#{@method_pattern.to_s.upcase}"
44
+ string = "#{@method_pattern.to_s.upcase}".dup
39
45
  string << " #{@uri_pattern.to_s}"
40
46
  string << " with body #{@body_pattern.to_s}" if @body_pattern
41
47
  string << " with headers #{@headers_pattern.to_s}" if @headers_pattern
@@ -47,14 +53,35 @@ module WebMock
47
53
 
48
54
 
49
55
  def assign_options(options)
50
- @body_pattern = BodyPattern.new(options[:body]) if options.has_key?(:body)
51
- @headers_pattern = HeadersPattern.new(options[:headers]) if options.has_key?(:headers)
52
- @uri_pattern.add_query_params(options[:query]) if options.has_key?(:query)
56
+ options = WebMock::Util::HashKeysStringifier.stringify_keys!(options, deep: true)
57
+ HashValidator.new(options).validate_keys('body', 'headers', 'query', 'basic_auth')
58
+ set_basic_auth_as_headers!(options)
59
+ @body_pattern = BodyPattern.new(options['body']) if options.has_key?('body')
60
+ @headers_pattern = HeadersPattern.new(options['headers']) if options.has_key?('headers')
61
+ @uri_pattern.add_query_params(options['query']) if options.has_key?('query')
62
+ end
63
+
64
+ def set_basic_auth_as_headers!(options)
65
+ if basic_auth = options.delete('basic_auth')
66
+ validate_basic_auth!(basic_auth)
67
+ options['headers'] ||= {}
68
+ options['headers']['Authorization'] = WebMock::Util::Headers.basic_auth_header(basic_auth[0],basic_auth[1])
69
+ end
70
+ end
71
+
72
+ def validate_basic_auth!(basic_auth)
73
+ if !basic_auth.is_a?(Array) || basic_auth.map{|e| e.is_a?(String)}.uniq != [true]
74
+ raise "The basic_auth option value should be an array which contains 2 strings: username and password"
75
+ end
53
76
  end
54
77
 
55
78
  def create_uri_pattern(uri)
56
79
  if uri.is_a?(Regexp)
57
80
  URIRegexpPattern.new(uri)
81
+ elsif uri.is_a?(Addressable::Template)
82
+ URIAddressablePattern.new(uri)
83
+ elsif uri.respond_to?(:call)
84
+ URICallablePattern.new(uri)
58
85
  else
59
86
  URIStringPattern.new(uri)
60
87
  end
@@ -82,51 +109,130 @@ module WebMock
82
109
  include RSpecMatcherDetector
83
110
 
84
111
  def initialize(pattern)
85
- @pattern = pattern.is_a?(Addressable::URI) ? pattern : WebMock::Util::URI.normalize_uri(pattern)
112
+ @pattern = if pattern.is_a?(Addressable::URI) \
113
+ || pattern.is_a?(Addressable::Template)
114
+ pattern
115
+ elsif pattern.respond_to?(:call)
116
+ pattern
117
+ else
118
+ WebMock::Util::URI.normalize_uri(pattern)
119
+ end
120
+ @query_params = nil
86
121
  end
87
122
 
88
123
  def add_query_params(query_params)
89
124
  @query_params = if query_params.is_a?(Hash)
90
125
  query_params
91
- elsif query_params.is_a?(WebMock::Matchers::HashIncludingMatcher)
126
+ elsif query_params.is_a?(WebMock::Matchers::HashIncludingMatcher) \
127
+ || query_params.is_a?(WebMock::Matchers::HashExcludingMatcher)
92
128
  query_params
93
129
  elsif rSpecHashIncludingMatcher?(query_params)
94
130
  WebMock::Matchers::HashIncludingMatcher.from_rspec_matcher(query_params)
131
+ elsif rSpecHashExcludingMatcher?(query_params)
132
+ WebMock::Matchers::HashExcludingMatcher.from_rspec_matcher(query_params)
95
133
  else
96
- Addressable::URI.parse('?' + query_params).query_values
134
+ WebMock::Util::QueryMapper.query_to_values(query_params, notation: Config.instance.query_values_notation)
97
135
  end
98
136
  end
99
137
 
138
+ def matches?(uri)
139
+ pattern_matches?(uri) && query_params_matches?(uri)
140
+ end
141
+
100
142
  def to_s
101
- str = @pattern.inspect
143
+ str = pattern_inspect
102
144
  str += " with query params #{@query_params.inspect}" if @query_params
103
145
  str
104
146
  end
147
+
148
+ private
149
+
150
+ def pattern_inspect
151
+ @pattern.inspect
152
+ end
153
+
154
+ def query_params_matches?(uri)
155
+ @query_params.nil? || @query_params == WebMock::Util::QueryMapper.query_to_values(uri.query, notation: Config.instance.query_values_notation)
156
+ end
157
+ end
158
+
159
+ class URICallablePattern < URIPattern
160
+ private
161
+
162
+ def pattern_matches?(uri)
163
+ @pattern.call(uri)
164
+ end
105
165
  end
106
166
 
107
167
  class URIRegexpPattern < URIPattern
108
- def initialize *args, &block
109
- @query_params = nil
110
- super
168
+ private
169
+
170
+ def pattern_matches?(uri)
171
+ WebMock::Util::URI.variations_of_uri_as_strings(uri).any? { |u| u.match(@pattern) }
111
172
  end
173
+ end
112
174
 
113
- def matches?(uri)
114
- WebMock::Util::URI.variations_of_uri_as_strings(uri).any? { |u| u.match(@pattern) } &&
115
- (@query_params.nil? || @query_params == uri.query_values)
175
+ class URIAddressablePattern < URIPattern
176
+ def add_query_params(query_params)
177
+ @@add_query_params_warned ||= false
178
+ if not @@add_query_params_warned
179
+ @@add_query_params_warned = true
180
+ warn "WebMock warning: ignoring query params in RFC 6570 template and checking them with WebMock"
181
+ end
182
+ super(query_params)
116
183
  end
117
184
 
118
- def to_s
119
- str = @pattern.inspect
120
- str += " with query params #{@query_params.inspect}" if @query_params
121
- str
185
+ private
186
+
187
+ def pattern_matches?(uri)
188
+ if @query_params.nil?
189
+ # Let Addressable check the whole URI
190
+ matches_with_variations?(uri)
191
+ else
192
+ # WebMock checks the query, Addressable checks everything else
193
+ matches_with_variations?(uri.omit(:query))
194
+ end
195
+ end
196
+
197
+ def pattern_inspect
198
+ @pattern.pattern.inspect
199
+ end
200
+
201
+ def matches_with_variations?(uri)
202
+ template =
203
+ begin
204
+ Addressable::Template.new(WebMock::Util::URI.heuristic_parse(@pattern.pattern))
205
+ rescue Addressable::URI::InvalidURIError
206
+ Addressable::Template.new(@pattern.pattern)
207
+ end
208
+ WebMock::Util::URI.variations_of_uri_as_strings(uri).any? { |u|
209
+ template_matches_uri?(template, u)
210
+ }
211
+ end
212
+
213
+ def template_matches_uri?(template, uri)
214
+ template.match(uri)
215
+ rescue Addressable::URI::InvalidURIError
216
+ false
122
217
  end
123
218
  end
124
219
 
125
220
  class URIStringPattern < URIPattern
126
- def matches?(uri)
221
+ def add_query_params(query_params)
222
+ super
223
+ if @query_params.is_a?(Hash) || @query_params.is_a?(String)
224
+ query_hash = (WebMock::Util::QueryMapper.query_to_values(@pattern.query, notation: Config.instance.query_values_notation) || {}).merge(@query_params)
225
+ @pattern.query = WebMock::Util::QueryMapper.values_to_query(query_hash, notation: WebMock::Config.instance.query_values_notation)
226
+ @query_params = nil
227
+ end
228
+ end
229
+
230
+ private
231
+
232
+ def pattern_matches?(uri)
127
233
  if @pattern.is_a?(Addressable::URI)
128
234
  if @query_params
129
- uri.omit(:query) === @pattern && (@query_params.nil? || @query_params == uri.query_values)
235
+ uri.omit(:query) === @pattern
130
236
  else
131
237
  uri === @pattern
132
238
  end
@@ -135,18 +241,8 @@ module WebMock
135
241
  end
136
242
  end
137
243
 
138
- def add_query_params(query_params)
139
- super
140
- if @query_params.is_a?(Hash) || @query_params.is_a?(String)
141
- @pattern.query_values = (@pattern.query_values || {}).merge(@query_params)
142
- @query_params = nil
143
- end
144
- end
145
-
146
- def to_s
147
- str = WebMock::Util::URI.strip_default_port_from_uri_string(@pattern.to_s)
148
- str += " with query params #{@query_params.inspect}" if @query_params
149
- str
244
+ def pattern_inspect
245
+ WebMock::Util::URI.strip_default_port_from_uri_string(@pattern.to_s)
150
246
  end
151
247
  end
152
248
 
@@ -167,6 +263,8 @@ module WebMock
167
263
  'text/plain' => :plain
168
264
  }
169
265
 
266
+ attr_reader :pattern
267
+
170
268
  def initialize(pattern)
171
269
  @pattern = if pattern.is_a?(Hash)
172
270
  normalize_hash(pattern)
@@ -178,9 +276,11 @@ module WebMock
178
276
  end
179
277
 
180
278
  def matches?(body, content_type = "")
279
+ assert_non_multipart_body(content_type)
280
+
181
281
  if (@pattern).is_a?(Hash)
182
282
  return true if @pattern.empty?
183
- matching_hashes?(body_as_hash(body, content_type), @pattern)
283
+ matching_body_hashes?(body_as_hash(body, content_type), @pattern, content_type)
184
284
  elsif (@pattern).is_a?(WebMock::Matchers::HashIncludingMatcher)
185
285
  @pattern == body_as_hash(body, content_type)
186
286
  else
@@ -195,14 +295,26 @@ module WebMock
195
295
  end
196
296
 
197
297
  private
298
+
198
299
  def body_as_hash(body, content_type)
199
- case BODY_FORMATS[content_type]
300
+ case body_format(content_type)
200
301
  when :json then
201
302
  WebMock::Util::JSON.parse(body)
202
303
  when :xml then
203
304
  Crack::XML.parse(body)
204
305
  else
205
- Addressable::URI.parse('?' + body).query_values
306
+ WebMock::Util::QueryMapper.query_to_values(body, notation: Config.instance.query_values_notation)
307
+ end
308
+ end
309
+
310
+ def body_format(content_type)
311
+ normalized_content_type = content_type.sub(/\A(application\/)[a-zA-Z0-9.-]+\+(json|xml)\Z/,'\1\2')
312
+ BODY_FORMATS[normalized_content_type]
313
+ end
314
+
315
+ def assert_non_multipart_body(content_type)
316
+ if content_type =~ %r{^multipart/form-data}
317
+ raise ArgumentError.new("WebMock does not support matching body for multipart/form-data requests yet :(")
206
318
  end
207
319
  end
208
320
 
@@ -229,15 +341,16 @@ module WebMock
229
341
  #
230
342
  # @return [Boolean] true if the paramaters match the comparison
231
343
  # hash, false if not.
232
- def matching_hashes?(query_parameters, pattern)
344
+ def matching_body_hashes?(query_parameters, pattern, content_type)
233
345
  return false unless query_parameters.is_a?(Hash)
234
346
  return false unless query_parameters.keys.sort == pattern.keys.sort
235
347
  query_parameters.each do |key, actual|
236
348
  expected = pattern[key]
237
349
 
238
350
  if actual.is_a?(Hash) && expected.is_a?(Hash)
239
- return false unless matching_hashes?(actual, expected)
351
+ return false unless matching_body_hashes?(actual, expected, content_type)
240
352
  else
353
+ expected = WebMock::Util::ValuesStringifier.stringify_values(expected) if url_encoded_body?(content_type)
241
354
  return false unless expected === actual
242
355
  end
243
356
  end
@@ -249,9 +362,12 @@ module WebMock
249
362
  end
250
363
 
251
364
  def normalize_hash(hash)
252
- Hash[WebMock::Util::HashKeysStringifier.stringify_keys!(hash).sort]
365
+ Hash[WebMock::Util::HashKeysStringifier.stringify_keys!(hash, deep: true).sort]
253
366
  end
254
367
 
368
+ def url_encoded_body?(content_type)
369
+ content_type =~ %r{^application/x-www-form-urlencoded}
370
+ end
255
371
  end
256
372
 
257
373
  class HeadersPattern
@@ -275,6 +391,10 @@ module WebMock
275
391
  WebMock::Util::Headers.sorted_headers_string(@pattern)
276
392
  end
277
393
 
394
+ def pp_to_s
395
+ WebMock::Util::Headers.pp_headers_string(@pattern)
396
+ end
397
+
278
398
  private
279
399
 
280
400
  def empty_headers?(headers)
@@ -14,16 +14,16 @@ module WebMock
14
14
  end
15
15
 
16
16
  def times_executed(request_pattern)
17
- self.requested_signatures.hash.select { |request_signature, times_executed|
17
+ self.requested_signatures.select do |request_signature|
18
18
  request_pattern.matches?(request_signature)
19
- }.inject(0) {|sum, (_, times_executed)| sum + times_executed }
19
+ end.inject(0) { |sum, (_, times_executed)| sum + times_executed }
20
20
  end
21
21
 
22
22
  def to_s
23
23
  if requested_signatures.hash.empty?
24
24
  "No requests were made."
25
25
  else
26
- text = ""
26
+ text = "".dup
27
27
  self.requested_signatures.each do |request_signature, times_executed|
28
28
  text << "#{request_signature} was made #{times_executed} time#{times_executed == 1 ? '' : 's' }\n"
29
29
  end