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.
- checksums.yaml +7 -0
- data/.github/workflows/CI.yml +37 -0
- data/.gitignore +6 -0
- data/CHANGELOG.md +1198 -0
- data/Gemfile +3 -15
- data/README.md +761 -305
- data/Rakefile +13 -40
- data/lib/webmock/api.rb +63 -17
- data/lib/webmock/callback_registry.rb +1 -1
- data/lib/webmock/config.rb +8 -0
- data/lib/webmock/cucumber.rb +2 -0
- data/lib/webmock/errors.rb +8 -24
- data/lib/webmock/http_lib_adapters/async_http_client_adapter.rb +216 -0
- data/lib/webmock/http_lib_adapters/curb_adapter.rb +148 -84
- data/lib/webmock/http_lib_adapters/em_http_request_adapter.rb +224 -4
- data/lib/webmock/http_lib_adapters/excon_adapter.rb +104 -34
- data/lib/webmock/http_lib_adapters/http_rb/client.rb +17 -0
- data/lib/webmock/http_lib_adapters/http_rb/request.rb +16 -0
- data/lib/webmock/http_lib_adapters/http_rb/response.rb +64 -0
- data/lib/webmock/http_lib_adapters/http_rb/streamer.rb +29 -0
- data/lib/webmock/http_lib_adapters/http_rb/webmock.rb +68 -0
- data/lib/webmock/http_lib_adapters/http_rb_adapter.rb +37 -0
- data/lib/webmock/http_lib_adapters/httpclient_adapter.rb +152 -86
- data/lib/webmock/http_lib_adapters/manticore_adapter.rb +145 -0
- data/lib/webmock/http_lib_adapters/net_http.rb +155 -46
- data/lib/webmock/http_lib_adapters/net_http_response.rb +1 -1
- data/lib/webmock/http_lib_adapters/patron_adapter.rb +16 -15
- data/lib/webmock/http_lib_adapters/typhoeus_hydra_adapter.rb +76 -82
- data/lib/webmock/matchers/any_arg_matcher.rb +13 -0
- data/lib/webmock/matchers/hash_argument_matcher.rb +21 -0
- data/lib/webmock/matchers/hash_excluding_matcher.rb +15 -0
- data/lib/webmock/matchers/hash_including_matcher.rb +4 -12
- data/lib/webmock/minitest.rb +29 -3
- data/lib/webmock/rack_response.rb +14 -7
- data/lib/webmock/request_body_diff.rb +64 -0
- data/lib/webmock/request_execution_verifier.rb +38 -17
- data/lib/webmock/request_pattern.rb +158 -38
- data/lib/webmock/request_registry.rb +3 -3
- data/lib/webmock/request_signature.rb +7 -3
- data/lib/webmock/request_signature_snippet.rb +61 -0
- data/lib/webmock/request_stub.rb +9 -6
- data/lib/webmock/response.rb +30 -15
- data/lib/webmock/rspec/matchers/request_pattern_matcher.rb +38 -2
- data/lib/webmock/rspec/matchers/webmock_matcher.rb +23 -2
- data/lib/webmock/rspec/matchers.rb +0 -1
- data/lib/webmock/rspec.rb +11 -2
- data/lib/webmock/stub_registry.rb +31 -10
- data/lib/webmock/stub_request_snippet.rb +14 -6
- data/lib/webmock/test_unit.rb +4 -4
- data/lib/webmock/util/hash_counter.rb +20 -6
- data/lib/webmock/util/hash_keys_stringifier.rb +5 -3
- data/lib/webmock/util/hash_validator.rb +17 -0
- data/lib/webmock/util/headers.rb +23 -2
- data/lib/webmock/util/json.rb +20 -7
- data/lib/webmock/util/query_mapper.rb +281 -0
- data/lib/webmock/util/uri.rb +29 -19
- data/lib/webmock/util/values_stringifier.rb +20 -0
- data/lib/webmock/util/version_checker.rb +40 -2
- data/lib/webmock/version.rb +1 -1
- data/lib/webmock/webmock.rb +56 -17
- data/lib/webmock.rb +56 -46
- data/minitest/test_helper.rb +8 -3
- data/minitest/test_webmock.rb +4 -1
- data/minitest/webmock_spec.rb +16 -6
- data/spec/acceptance/async_http_client/async_http_client_spec.rb +375 -0
- data/spec/acceptance/async_http_client/async_http_client_spec_helper.rb +73 -0
- data/spec/acceptance/curb/curb_spec.rb +227 -68
- data/spec/acceptance/curb/curb_spec_helper.rb +11 -8
- data/spec/acceptance/em_http_request/em_http_request_spec.rb +322 -28
- data/spec/acceptance/em_http_request/em_http_request_spec_helper.rb +15 -10
- data/spec/acceptance/excon/excon_spec.rb +66 -4
- data/spec/acceptance/excon/excon_spec_helper.rb +21 -7
- data/spec/acceptance/http_rb/http_rb_spec.rb +93 -0
- data/spec/acceptance/http_rb/http_rb_spec_helper.rb +54 -0
- data/spec/acceptance/httpclient/httpclient_spec.rb +152 -11
- data/spec/acceptance/httpclient/httpclient_spec_helper.rb +25 -16
- data/spec/acceptance/manticore/manticore_spec.rb +107 -0
- data/spec/acceptance/manticore/manticore_spec_helper.rb +35 -0
- data/spec/acceptance/net_http/net_http_shared.rb +52 -24
- data/spec/acceptance/net_http/net_http_spec.rb +164 -50
- data/spec/acceptance/net_http/net_http_spec_helper.rb +19 -10
- data/spec/acceptance/net_http/real_net_http_spec.rb +1 -1
- data/spec/acceptance/patron/patron_spec.rb +29 -40
- data/spec/acceptance/patron/patron_spec_helper.rb +15 -11
- data/spec/acceptance/shared/allowing_and_disabling_net_connect.rb +229 -58
- data/spec/acceptance/shared/callbacks.rb +32 -30
- data/spec/acceptance/shared/complex_cross_concern_behaviors.rb +20 -5
- data/spec/acceptance/shared/enabling_and_disabling_webmock.rb +14 -14
- data/spec/acceptance/shared/precedence_of_stubs.rb +6 -6
- data/spec/acceptance/shared/request_expectations.rb +560 -296
- data/spec/acceptance/shared/returning_declared_responses.rb +180 -138
- data/spec/acceptance/shared/stubbing_requests.rb +385 -154
- data/spec/acceptance/typhoeus/typhoeus_hydra_spec.rb +78 -17
- data/spec/acceptance/typhoeus/typhoeus_hydra_spec_helper.rb +19 -15
- data/spec/acceptance/webmock_shared.rb +2 -2
- data/spec/fixtures/test.txt +1 -0
- data/spec/quality_spec.rb +27 -3
- data/spec/spec_helper.rb +11 -20
- data/spec/support/failures.rb +9 -0
- data/spec/support/my_rack_app.rb +8 -3
- data/spec/support/network_connection.rb +7 -13
- data/spec/support/webmock_server.rb +8 -3
- data/spec/unit/api_spec.rb +175 -0
- data/spec/unit/errors_spec.rb +116 -19
- data/spec/unit/http_lib_adapters/http_lib_adapter_registry_spec.rb +1 -1
- data/spec/unit/http_lib_adapters/http_lib_adapter_spec.rb +2 -2
- data/spec/unit/matchers/hash_excluding_matcher_spec.rb +61 -0
- data/spec/unit/matchers/hash_including_matcher_spec.rb +87 -0
- data/spec/unit/rack_response_spec.rb +54 -16
- data/spec/unit/request_body_diff_spec.rb +90 -0
- data/spec/unit/request_execution_verifier_spec.rb +147 -39
- data/spec/unit/request_pattern_spec.rb +462 -198
- data/spec/unit/request_registry_spec.rb +29 -9
- data/spec/unit/request_signature_snippet_spec.rb +89 -0
- data/spec/unit/request_signature_spec.rb +91 -49
- data/spec/unit/request_stub_spec.rb +71 -70
- data/spec/unit/response_spec.rb +100 -81
- data/spec/unit/stub_registry_spec.rb +37 -20
- data/spec/unit/stub_request_snippet_spec.rb +51 -31
- data/spec/unit/util/hash_counter_spec.rb +6 -6
- data/spec/unit/util/hash_keys_stringifier_spec.rb +4 -4
- data/spec/unit/util/headers_spec.rb +4 -4
- data/spec/unit/util/json_spec.rb +29 -3
- data/spec/unit/util/query_mapper_spec.rb +157 -0
- data/spec/unit/util/uri_spec.rb +150 -36
- data/spec/unit/util/version_checker_spec.rb +15 -9
- data/spec/unit/webmock_spec.rb +57 -4
- data/test/http_request.rb +3 -3
- data/test/shared_test.rb +45 -13
- data/test/test_helper.rb +1 -1
- data/test/test_webmock.rb +6 -0
- data/webmock.gemspec +30 -11
- metadata +308 -199
- data/.rvmrc +0 -1
- data/.travis.yml +0 -11
- data/Guardfile +0 -24
- data/lib/webmock/http_lib_adapters/em_http_request/em_http_request_0_x.rb +0 -151
- 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
|
-
|
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
|
data/lib/webmock/minitest.rb
CHANGED
@@ -1,7 +1,18 @@
|
|
1
|
-
|
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
|
-
|
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 =
|
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
|
-
:
|
14
|
-
:
|
15
|
-
: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.
|
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
|
-
|
14
|
-
|
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
|
-
|
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
|
36
|
-
|
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
|
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
|
-
|
51
|
-
|
52
|
-
|
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)
|
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
|
-
|
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 =
|
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
|
-
|
109
|
-
|
110
|
-
|
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
|
-
|
114
|
-
|
115
|
-
|
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
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
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
|
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
|
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
|
139
|
-
|
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
|
-
|
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
|
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
|
-
|
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
|
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
|
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.
|
17
|
+
self.requested_signatures.select do |request_signature|
|
18
18
|
request_pattern.matches?(request_signature)
|
19
|
-
|
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
|