webmock 1.8.6 → 3.14.0

Sign up to get free protection for your applications and to get access to all the features.
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