webmock 1.2.2 → 1.3.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG.md +55 -0
- data/README.md +63 -4
- data/Rakefile +2 -0
- data/VERSION +1 -1
- data/lib/webmock.rb +3 -0
- data/lib/webmock/callback_registry.rb +35 -0
- data/lib/webmock/http_lib_adapters/em_http_request.rb +114 -0
- data/lib/webmock/http_lib_adapters/httpclient.rb +34 -7
- data/lib/webmock/http_lib_adapters/net_http.rb +24 -2
- data/lib/webmock/http_lib_adapters/net_http_response.rb +49 -0
- data/lib/webmock/http_lib_adapters/patron.rb +22 -3
- data/lib/webmock/request_pattern.rb +90 -17
- data/lib/webmock/response.rb +15 -7
- data/lib/webmock/util/uri.rb +18 -7
- data/lib/webmock/webmock.rb +8 -0
- data/spec/benchmark.rb +61 -0
- data/spec/em_http_request_spec.rb +29 -0
- data/spec/em_http_request_spec_helper.rb +64 -0
- data/spec/httpclient_spec_helper.rb +4 -0
- data/spec/net_http_spec.rb +46 -1
- data/spec/net_http_spec_helper.rb +4 -0
- data/spec/other_net_http_libs_spec.rb +1 -0
- data/spec/patron_spec_helper.rb +4 -0
- data/spec/request_pattern_spec.rb +143 -32
- data/spec/response_spec.rb +14 -0
- data/spec/spec_helper.rb +4 -1
- data/spec/vendor/crack/lib/crack.rb +0 -0
- data/spec/webmock_spec.rb +362 -13
- data/test/test_webmock.rb +2 -2
- data/webmock.gemspec +21 -5
- metadata +58 -11
@@ -0,0 +1,49 @@
|
|
1
|
+
# This code is entierly copied from VCR (http://github.com/myronmarston/vcr) by courtesy of Myron Marston
|
2
|
+
|
3
|
+
# A Net::HTTP response that has already been read raises an IOError when #read_body
|
4
|
+
# is called with a destination string or block.
|
5
|
+
#
|
6
|
+
# This causes a problem when VCR records a response--it reads the body before yielding
|
7
|
+
# the response, and if the code that is consuming the HTTP requests uses #read_body, it
|
8
|
+
# can cause an error.
|
9
|
+
#
|
10
|
+
# This is a bit of a hack, but it allows a Net::HTTP response to be "re-read"
|
11
|
+
# after it has aleady been read. This attemps to preserve the behavior of
|
12
|
+
# #read_body, acting just as if it had never been read.
|
13
|
+
|
14
|
+
module WebMock
|
15
|
+
module Net
|
16
|
+
module HTTPResponse
|
17
|
+
def self.extended(object)
|
18
|
+
body_object = object.instance_variable_get(:@body)
|
19
|
+
object.instance_variable_set(:@__orig_body__,
|
20
|
+
case body_object
|
21
|
+
when String then body_object
|
22
|
+
else raise ArgumentError.new("Unexpected body object: #{body_object}")
|
23
|
+
end
|
24
|
+
)
|
25
|
+
end
|
26
|
+
|
27
|
+
def read_body(dest = nil, &block)
|
28
|
+
if @__orig_body__
|
29
|
+
if dest && block
|
30
|
+
raise ArgumentError.new("both arg and block given for HTTP method")
|
31
|
+
elsif dest
|
32
|
+
dest << @__orig_body__
|
33
|
+
elsif block
|
34
|
+
@body = ::Net::ReadAdapter.new(block)
|
35
|
+
@body << @__orig_body__
|
36
|
+
@body
|
37
|
+
else
|
38
|
+
@body = @__orig_body__
|
39
|
+
end
|
40
|
+
else
|
41
|
+
super
|
42
|
+
end
|
43
|
+
ensure
|
44
|
+
# allow subsequent calls to #read_body to proceed as normal, without our hack...
|
45
|
+
@__orig_body__ = nil
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -11,9 +11,19 @@ if defined?(Patron)
|
|
11
11
|
if WebMock.registered_request?(request_signature)
|
12
12
|
webmock_response = WebMock.response_for_request(request_signature)
|
13
13
|
handle_file_name(req, webmock_response)
|
14
|
-
build_patron_response(webmock_response)
|
14
|
+
res = build_patron_response(webmock_response)
|
15
|
+
WebMock::CallbackRegistry.invoke_callbacks(
|
16
|
+
{:lib => :patron}, request_signature, webmock_response)
|
17
|
+
res
|
15
18
|
elsif WebMock.net_connect_allowed?(request_signature.uri)
|
16
|
-
handle_request_without_webmock(req)
|
19
|
+
res = handle_request_without_webmock(req)
|
20
|
+
if WebMock::CallbackRegistry.any_callbacks?
|
21
|
+
webmock_response = build_webmock_response(res)
|
22
|
+
WebMock::CallbackRegistry.invoke_callbacks(
|
23
|
+
{:lib => :patron, :real_request => true}, request_signature,
|
24
|
+
webmock_response)
|
25
|
+
end
|
26
|
+
res
|
17
27
|
else
|
18
28
|
raise WebMock::NetConnectNotAllowedError.new(request_signature)
|
19
29
|
end
|
@@ -37,7 +47,7 @@ if defined?(Patron)
|
|
37
47
|
end
|
38
48
|
|
39
49
|
def build_request_signature(req)
|
40
|
-
uri =
|
50
|
+
uri = WebMock::Util::URI.heuristic_parse(req.url)
|
41
51
|
uri.path = uri.normalized_path.gsub("[^:]//","/")
|
42
52
|
uri.user = req.username
|
43
53
|
uri.password = req.password
|
@@ -74,6 +84,15 @@ if defined?(Patron)
|
|
74
84
|
res.instance_variable_set(:@headers, webmock_response.headers)
|
75
85
|
res
|
76
86
|
end
|
87
|
+
|
88
|
+
def build_webmock_response(patron_response)
|
89
|
+
webmock_response = WebMock::Response.new
|
90
|
+
reason = patron_response.status_line.scan(%r(\AHTTP/(\d+\.\d+)\s+(\d\d\d)\s*([^\r\n]+)?\r?\z))[0][2]
|
91
|
+
webmock_response.status = [patron_response.status, reason]
|
92
|
+
webmock_response.body = patron_response.body
|
93
|
+
webmock_response.headers = patron_response.headers
|
94
|
+
webmock_response
|
95
|
+
end
|
77
96
|
|
78
97
|
end
|
79
98
|
end
|
@@ -4,7 +4,7 @@ module WebMock
|
|
4
4
|
|
5
5
|
def initialize(method, uri, options = {})
|
6
6
|
@method_pattern = MethodPattern.new(method)
|
7
|
-
@uri_pattern =
|
7
|
+
@uri_pattern = create_uri_pattern(uri)
|
8
8
|
assign_options(options)
|
9
9
|
end
|
10
10
|
|
@@ -15,17 +15,18 @@ module WebMock
|
|
15
15
|
end
|
16
16
|
|
17
17
|
def matches?(request_signature)
|
18
|
+
content_type = request_signature.headers['Content-Type'] if request_signature.headers
|
18
19
|
@method_pattern.matches?(request_signature.method) &&
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
20
|
+
@uri_pattern.matches?(request_signature.uri) &&
|
21
|
+
(@body_pattern.nil? || @body_pattern.matches?(request_signature.body, content_type || "")) &&
|
22
|
+
(@headers_pattern.nil? || @headers_pattern.matches?(request_signature.headers)) &&
|
23
|
+
(@with_block.nil? || @with_block.call(request_signature))
|
23
24
|
end
|
24
25
|
|
25
26
|
def to_s
|
26
27
|
string = "#{@method_pattern.to_s.upcase}"
|
27
28
|
string << " #{@uri_pattern.to_s}"
|
28
|
-
string << " with body
|
29
|
+
string << " with body #{@body_pattern.to_s}" if @body_pattern
|
29
30
|
string << " with headers #{@headers_pattern.to_s}" if @headers_pattern
|
30
31
|
string << " with given block" if @with_block
|
31
32
|
string
|
@@ -37,10 +38,19 @@ module WebMock
|
|
37
38
|
def assign_options(options)
|
38
39
|
@body_pattern = BodyPattern.new(options[:body]) if options.has_key?(:body)
|
39
40
|
@headers_pattern = HeadersPattern.new(options[:headers]) if options.has_key?(:headers)
|
41
|
+
@uri_pattern.add_query_params(options[:query]) if options.has_key?(:query)
|
42
|
+
end
|
43
|
+
|
44
|
+
def create_uri_pattern(uri)
|
45
|
+
if uri.is_a?(Regexp)
|
46
|
+
URIRegexpPattern.new(uri)
|
47
|
+
else
|
48
|
+
URIStringPattern.new(uri)
|
49
|
+
end
|
40
50
|
end
|
41
51
|
|
42
52
|
end
|
43
|
-
|
53
|
+
|
44
54
|
|
45
55
|
class MethodPattern
|
46
56
|
def initialize(pattern)
|
@@ -56,40 +66,98 @@ module WebMock
|
|
56
66
|
end
|
57
67
|
end
|
58
68
|
|
69
|
+
|
59
70
|
class URIPattern
|
60
71
|
def initialize(pattern)
|
61
72
|
@pattern = pattern.is_a?(Addressable::URI) ? pattern : WebMock::Util::URI.normalize_uri(pattern)
|
62
73
|
end
|
74
|
+
end
|
75
|
+
|
76
|
+
class URIRegexpPattern < URIPattern
|
77
|
+
|
78
|
+
def matches?(uri)
|
79
|
+
WebMock::Util::URI.variations_of_uri_as_strings(uri).any? { |u| u.match(@pattern) } &&
|
80
|
+
(@query_params.nil? || @query_params == uri.query_values)
|
81
|
+
end
|
82
|
+
|
83
|
+
def to_s
|
84
|
+
str = @pattern.inspect
|
85
|
+
str += " with query params #{@query_params.inspect}" if @query_params
|
86
|
+
str
|
87
|
+
end
|
88
|
+
|
89
|
+
def add_query_params(query_params)
|
90
|
+
@query_params = query_params.is_a?(Hash) ? query_params : Addressable::URI.parse('?' + query_params).query_values
|
91
|
+
end
|
92
|
+
|
93
|
+
end
|
63
94
|
|
95
|
+
class URIStringPattern < URIPattern
|
64
96
|
def matches?(uri)
|
65
97
|
if @pattern.is_a?(Addressable::URI)
|
66
|
-
##TODO : do I need to normalize again??
|
67
98
|
uri === @pattern
|
68
|
-
elsif @pattern.is_a?(Regexp)
|
69
|
-
WebMock::Util::URI.variations_of_uri_as_strings(uri).any? { |u| u.match(@pattern) }
|
70
99
|
else
|
71
100
|
false
|
72
101
|
end
|
73
102
|
end
|
74
103
|
|
104
|
+
def add_query_params(query_params)
|
105
|
+
if !query_params.is_a?(Hash)
|
106
|
+
query_params = Addressable::URI.parse('?' + query_params).query_values
|
107
|
+
end
|
108
|
+
@pattern.query_values = (@pattern.query_values || {}).merge(query_params)
|
109
|
+
end
|
110
|
+
|
75
111
|
def to_s
|
76
112
|
WebMock::Util::URI.strip_default_port_from_uri_string(@pattern.to_s)
|
77
113
|
end
|
114
|
+
|
78
115
|
end
|
79
116
|
|
117
|
+
|
80
118
|
class BodyPattern
|
119
|
+
|
120
|
+
BODY_FORMATS = {
|
121
|
+
'text/xml' => :xml,
|
122
|
+
'application/xml' => :xml,
|
123
|
+
'application/json' => :json,
|
124
|
+
'text/json' => :json,
|
125
|
+
'application/javascript' => :json,
|
126
|
+
'text/javascript' => :json,
|
127
|
+
'text/html' => :html,
|
128
|
+
'application/x-yaml' => :yaml,
|
129
|
+
'text/yaml' => :yaml,
|
130
|
+
'text/plain' => :plain
|
131
|
+
}
|
132
|
+
|
81
133
|
def initialize(pattern)
|
82
134
|
@pattern = pattern
|
135
|
+
if (@pattern).is_a?(Hash)
|
136
|
+
@pattern = normalize_hash(@pattern)
|
137
|
+
end
|
83
138
|
end
|
84
139
|
|
85
|
-
def matches?(
|
86
|
-
|
87
|
-
|
88
|
-
|
140
|
+
def matches?(body, content_type = "")
|
141
|
+
if (@pattern).is_a?(Hash)
|
142
|
+
return true if @pattern.empty?
|
143
|
+
|
144
|
+
case BODY_FORMATS[content_type]
|
145
|
+
when :json then
|
146
|
+
Crack::JSON.parse(body) == @pattern
|
147
|
+
when :xml then
|
148
|
+
Crack::XML.parse(body) == @pattern
|
149
|
+
else
|
150
|
+
Addressable::URI.parse('?' + body).query_values == @pattern
|
151
|
+
end
|
152
|
+
else
|
153
|
+
empty_string?(@pattern) && empty_string?(body) ||
|
154
|
+
@pattern == body ||
|
155
|
+
@pattern === body
|
156
|
+
end
|
89
157
|
end
|
90
|
-
|
158
|
+
|
91
159
|
def to_s
|
92
|
-
@pattern.
|
160
|
+
@pattern.inspect
|
93
161
|
end
|
94
162
|
|
95
163
|
private
|
@@ -97,6 +165,11 @@ module WebMock
|
|
97
165
|
def empty_string?(string)
|
98
166
|
string.nil? || string == ""
|
99
167
|
end
|
168
|
+
|
169
|
+
def normalize_hash(hash)
|
170
|
+
JSON.parse(JSON.generate(hash))
|
171
|
+
end
|
172
|
+
|
100
173
|
end
|
101
174
|
|
102
175
|
class HeadersPattern
|
@@ -115,7 +188,7 @@ module WebMock
|
|
115
188
|
true
|
116
189
|
end
|
117
190
|
end
|
118
|
-
|
191
|
+
|
119
192
|
def to_s
|
120
193
|
WebMock::Util::Headers.sorted_headers_string(@pattern)
|
121
194
|
end
|
data/lib/webmock/response.rb
CHANGED
@@ -14,7 +14,7 @@ module WebMock
|
|
14
14
|
end
|
15
15
|
end
|
16
16
|
end
|
17
|
-
|
17
|
+
|
18
18
|
class Response
|
19
19
|
def initialize(options = {})
|
20
20
|
if options.is_a?(IO) || options.is_a?(String)
|
@@ -27,7 +27,7 @@ module WebMock
|
|
27
27
|
def headers
|
28
28
|
@headers
|
29
29
|
end
|
30
|
-
|
30
|
+
|
31
31
|
def headers=(headers)
|
32
32
|
@headers = headers
|
33
33
|
if @headers && !@headers.is_a?(Proc)
|
@@ -38,7 +38,7 @@ module WebMock
|
|
38
38
|
def body
|
39
39
|
@body || ''
|
40
40
|
end
|
41
|
-
|
41
|
+
|
42
42
|
def body=(body)
|
43
43
|
@body = body
|
44
44
|
stringify_body!
|
@@ -56,8 +56,16 @@ module WebMock
|
|
56
56
|
@exception
|
57
57
|
end
|
58
58
|
|
59
|
+
def exception=(exception)
|
60
|
+
@exception = case exception
|
61
|
+
when String then StandardError.new(exception)
|
62
|
+
when Class then exception.new('Exception from WebMock')
|
63
|
+
when Exception then exception
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
59
67
|
def raise_error_if_any
|
60
|
-
raise @exception
|
68
|
+
raise @exception if @exception
|
61
69
|
end
|
62
70
|
|
63
71
|
def should_timeout
|
@@ -68,7 +76,7 @@ module WebMock
|
|
68
76
|
self.headers = options[:headers]
|
69
77
|
self.status = options[:status]
|
70
78
|
self.body = options[:body]
|
71
|
-
|
79
|
+
self.exception = options[:exception]
|
72
80
|
@should_timeout = options[:should_timeout]
|
73
81
|
end
|
74
82
|
|
@@ -105,8 +113,8 @@ module WebMock
|
|
105
113
|
raw_response.close
|
106
114
|
raw_response = string
|
107
115
|
end
|
108
|
-
socket = Net::BufferedIO.new(raw_response)
|
109
|
-
response = Net::HTTPResponse.read_new(socket)
|
116
|
+
socket = ::Net::BufferedIO.new(raw_response)
|
117
|
+
response = ::Net::HTTPResponse.read_new(socket)
|
110
118
|
transfer_encoding = response.delete('transfer-encoding') #chunks were already read by curl
|
111
119
|
response.reading_body(socket, true) {}
|
112
120
|
|
data/lib/webmock/util/uri.rb
CHANGED
@@ -11,15 +11,26 @@ module WebMock
|
|
11
11
|
module Util
|
12
12
|
|
13
13
|
class URI
|
14
|
+
ADDRESSABLE_URIS = Hash.new do |hash, key|
|
15
|
+
hash[key] = Addressable::URI.heuristic_parse(key)
|
16
|
+
end
|
17
|
+
|
18
|
+
NORMALIZED_URIS = Hash.new do |hash, uri|
|
19
|
+
normalized_uri = WebMock::Util::URI.heuristic_parse(uri)
|
20
|
+
normalized_uri.query_values = sort_query_values(normalized_uri.query_values) if normalized_uri.query_values
|
21
|
+
normalized_uri = normalized_uri.normalize #normalize! is slower
|
22
|
+
normalized_uri.port = normalized_uri.inferred_port unless normalized_uri.port
|
23
|
+
hash[uri] = normalized_uri
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.heuristic_parse(uri)
|
27
|
+
ADDRESSABLE_URIS[uri].dup
|
28
|
+
end
|
14
29
|
|
15
30
|
def self.normalize_uri(uri)
|
16
31
|
return uri if uri.is_a?(Regexp)
|
17
32
|
uri = 'http://' + uri unless uri.match('^https?://') if uri.is_a?(String)
|
18
|
-
|
19
|
-
normalized_uri.query_values = sort_query_values(normalized_uri.query_values) if normalized_uri.query_values
|
20
|
-
normalized_uri = normalized_uri.normalize #normalize! is slower
|
21
|
-
normalized_uri.port = normalized_uri.inferred_port unless normalized_uri.port
|
22
|
-
normalized_uri
|
33
|
+
NORMALIZED_URIS[uri].dup
|
23
34
|
end
|
24
35
|
|
25
36
|
def self.variations_of_uri_as_strings(uri_object)
|
@@ -35,7 +46,7 @@ module WebMock
|
|
35
46
|
if normalized_uri.port == Addressable::URI.port_mapping[normalized_uri.scheme]
|
36
47
|
uris = uris_with_inferred_port_and_without(uris)
|
37
48
|
end
|
38
|
-
|
49
|
+
|
39
50
|
if normalized_uri.scheme == "http"
|
40
51
|
uris = uris_with_scheme_and_without(uris)
|
41
52
|
end
|
@@ -58,7 +69,7 @@ module WebMock
|
|
58
69
|
private
|
59
70
|
|
60
71
|
def self.sort_query_values(query_values)
|
61
|
-
Hash[*query_values.sort.inject([]) { |values, pair| values + pair}]
|
72
|
+
Hash[*query_values.sort.inject([]) { |values, pair| values + pair}]
|
62
73
|
end
|
63
74
|
|
64
75
|
def self.uris_with_inferred_port_and_without(uris)
|
data/lib/webmock/webmock.rb
CHANGED
@@ -59,8 +59,16 @@ module WebMock
|
|
59
59
|
WebMock::RequestRegistry.instance.reset_webmock
|
60
60
|
end
|
61
61
|
|
62
|
+
def reset_callbacks
|
63
|
+
WebMock::CallbackRegistry.reset
|
64
|
+
end
|
65
|
+
|
62
66
|
def assertion_failure(message)
|
63
67
|
raise message
|
64
68
|
end
|
69
|
+
|
70
|
+
def after_request(options={}, &block)
|
71
|
+
CallbackRegistry.add_callback(options, block)
|
72
|
+
end
|
65
73
|
|
66
74
|
end
|
data/spec/benchmark.rb
ADDED
@@ -0,0 +1,61 @@
|
|
1
|
+
# require 'rubygems'
|
2
|
+
require 'benchmark'
|
3
|
+
require 'net/http'
|
4
|
+
# require 'fakeweb'
|
5
|
+
require 'spec/spec_helper'
|
6
|
+
|
7
|
+
def http_request
|
8
|
+
res = Net::HTTP.get_response(URI.parse('http://example.com'))
|
9
|
+
raise "Body should be 'Hello'" unless res.body == 'Hello'
|
10
|
+
end
|
11
|
+
|
12
|
+
def fakeweb
|
13
|
+
FakeWeb.register_uri(:get, 'http://example.com', :body => 'Hello')
|
14
|
+
yield
|
15
|
+
ensure
|
16
|
+
FakeWeb.clean_registry
|
17
|
+
end
|
18
|
+
|
19
|
+
def webmock
|
20
|
+
WebMock.stub_request(:get, 'http://example.com').to_return(:body => 'Hello')
|
21
|
+
yield
|
22
|
+
ensure
|
23
|
+
WebMock.reset_webmock
|
24
|
+
end
|
25
|
+
|
26
|
+
def perform_benchmark(name)
|
27
|
+
puts "\n\nBenchmarking #{name}:"
|
28
|
+
Benchmark.benchmark do |b|
|
29
|
+
%w(webmock).each do |type|
|
30
|
+
b.report(type) do
|
31
|
+
# this is a bit convoluted, but we want to ensure that each benchmark runs without the other library loaded,
|
32
|
+
# so we fork off a sub-process before requiring the libraries.
|
33
|
+
Process.fork do
|
34
|
+
require type
|
35
|
+
yield type
|
36
|
+
end
|
37
|
+
Process.wait
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
n = 5000
|
44
|
+
perform_benchmark("Single setup/teardown") do |type|
|
45
|
+
send(type) { n.times { http_request } }
|
46
|
+
end
|
47
|
+
|
48
|
+
perform_benchmark("Setup/teardown for each http request") do |type|
|
49
|
+
n.times { send(type) { http_request } }
|
50
|
+
end
|
51
|
+
|
52
|
+
# Output on my machine:
|
53
|
+
#
|
54
|
+
# Benchmarking Single setup/teardown:
|
55
|
+
# webmock 0.000000 0.000000 18.830000 ( 19.350281)
|
56
|
+
# fakeweb 0.000000 0.000000 1.930000 ( 2.049569)
|
57
|
+
#
|
58
|
+
#
|
59
|
+
# Benchmarking Setup/teardown for each http request:
|
60
|
+
# webmock 0.000000 0.000000 21.350000 ( 21.795557)
|
61
|
+
# fakeweb 0.000000 0.000000 2.490000 ( 2.707574)
|