Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 31 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ Features

* Stubbing HTTP requests at low http client lib level (no need to change tests when you change HTTP library)
* Setting and verifying expectations on HTTP requests
* Matching requests based on method, URI, headers and body
* Matching requests based on method, URI, headers, body and proxy
* Smart matching of the same URIs in different representations (also encoded and non encoded forms)
* Smart matching of the same headers in different representations.
* Support for Test::Unit
Expand Down Expand Up @@ -213,6 +213,35 @@ req.add_field('Accept', 'image/jpeg')
Net::HTTP.start("www.example.com") {|http| http.request(req) } # ===> Success
```

### Matching requests based on proxy

```ruby
stub_request(:get, "www.example.com").
with(proxy: { "host" => "proxy.example.com", "port" => 8080 })

http = Net::HTTP.new("www.example.com", 80, "proxy.example.com", 8080)
http.start { |h| h.get("/") } # ===> Success
```

Proxy pattern supports Hash (partial matching), String (URI comparison), and Regexp:

```ruby
# Match by proxy URI string
stub_request(:get, "www.example.com").
with(proxy: "http://proxy.example.com:8080")

# Match by proxy URI regexp
stub_request(:get, "www.example.com").
with(proxy: /proxy\.example/)

# Match requests with no proxy
stub_request(:get, "www.example.com").
with(proxy: nil)
```

Proxy matching is supported by Net::HTTP, Curb, Excon, Patron, and Typhoeus.
Other adapters do not extract proxy information and will always have a nil proxy.

### Matching requests against provided block

```ruby
Expand Down Expand Up @@ -796,6 +825,7 @@ An executed request matches stubbed request if it passes following criteria:
- And request method is the same as stubbed request method or stubbed request method is :any
- And request body is the same as stubbed request body or stubbed request body is not specified
- And request headers match stubbed request headers, or stubbed request headers match a subset of request headers, or stubbed request headers are not specified
- And request proxy matches stubbed request proxy pattern (Hash, String, Regexp, or nil), or stubbed request proxy is not specified
- And request matches provided block or block is not provided

## Precedence of stubs
Expand Down
16 changes: 15 additions & 1 deletion lib/webmock/http_lib_adapters/curb_adapter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -122,11 +122,25 @@ def build_request_signature
method,
uri.to_s,
body: request_body,
headers: headers
headers: headers,
proxy: proxy_from_curb
)
request_signature
end

def proxy_from_curb
return nil unless self.proxy_url
proxy_uri = URI.parse(self.proxy_url)
proxy = {
"host" => proxy_uri.host,
"port" => proxy_uri.port
}
proxy["username"] = proxy_uri.user if proxy_uri.user
proxy["password"] = proxy_uri.password if proxy_uri.password
proxy["scheme"] = proxy_uri.scheme if proxy_uri.scheme && proxy_uri.scheme != "http"
proxy
end

def headers_as_hash(headers)
if headers.is_a?(Array)
headers.inject({}) {|hash, header|
Expand Down
27 changes: 26 additions & 1 deletion lib/webmock/http_lib_adapters/excon_adapter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -111,10 +111,35 @@ def self.build_request(params)
params = params.dup
params.delete(:user)
params.delete(:password)
proxy = proxy_from_excon(params.delete(:proxy))
method = (params.delete(:method) || :get).to_s.downcase.to_sym
params[:query] = to_query(params[:query]) if params[:query].is_a?(Hash)
uri = Addressable::URI.new(params).to_s
WebMock::RequestSignature.new method, uri, body: body_from(params), headers: params[:headers]
WebMock::RequestSignature.new method, uri, body: body_from(params), headers: params[:headers], proxy: proxy
end

def self.proxy_from_excon(proxy_data)
return nil if proxy_data.nil?
if proxy_data.is_a?(String)
proxy_uri = URI.parse(proxy_data)
proxy = {
"host" => proxy_uri.host,
"port" => proxy_uri.port
}
proxy["username"] = proxy_uri.user if proxy_uri.user
proxy["password"] = proxy_uri.password if proxy_uri.password
proxy["scheme"] = proxy_uri.scheme if proxy_uri.scheme && proxy_uri.scheme != "http"
proxy
elsif proxy_data.is_a?(Hash)
proxy = {
"host" => proxy_data[:host],
"port" => proxy_data[:port]
}
proxy["username"] = proxy_data[:user] if proxy_data[:user]
proxy["password"] = proxy_data[:password] if proxy_data[:password]
proxy["scheme"] = proxy_data[:scheme] if proxy_data[:scheme] && proxy_data[:scheme] != "http"
proxy
end
end

def self.body_from(params)
Expand Down
13 changes: 12 additions & 1 deletion lib/webmock/http_lib_adapters/net_http.rb
Original file line number Diff line number Diff line change
Expand Up @@ -282,7 +282,18 @@ def self.request_signature_from_request(net_http, request, body = nil)
request.set_body_internal body
end

WebMock::RequestSignature.new(method, uri, body: request.body, headers: headers)
WebMock::RequestSignature.new(method, uri, body: request.body, headers: headers, proxy: proxy_from_net_http(net_http))
end

def self.proxy_from_net_http(net_http)
return nil unless net_http.proxy_address
proxy = {
"host" => net_http.proxy_address,
"port" => net_http.proxy_port
}
proxy["username"] = net_http.proxy_user if net_http.proxy_user
proxy["password"] = net_http.proxy_pass if net_http.proxy_pass
proxy
end

def self.get_uri(net_http, path = nil)
Expand Down
16 changes: 15 additions & 1 deletion lib/webmock/http_lib_adapters/patron_adapter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -94,11 +94,25 @@ def self.build_request_signature(req)
req.action,
uri.to_s,
body: request_body,
headers: headers
headers: headers,
proxy: proxy_from_patron(req)
)
request_signature
end

def self.proxy_from_patron(req)
return nil unless req.proxy && !req.proxy.empty?
proxy_uri = URI.parse(req.proxy)
proxy = {
"host" => proxy_uri.host,
"port" => proxy_uri.port
}
proxy["username"] = proxy_uri.user if proxy_uri.user
proxy["password"] = proxy_uri.password if proxy_uri.password
proxy["scheme"] = proxy_uri.scheme if proxy_uri.scheme && proxy_uri.scheme != "http"
proxy
end

def self.build_patron_response(webmock_response, default_response_charset)
raise ::Patron::TimeoutError if webmock_response.should_timeout
webmock_response.raise_error_if_any
Expand Down
19 changes: 18 additions & 1 deletion lib/webmock/http_lib_adapters/typhoeus_hydra_adapter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -72,14 +72,31 @@ def self.build_request_signature(req)
req.options[:method] || :get,
uri.to_s,
body: body,
headers: headers
headers: headers,
proxy: proxy_from_typhoeus(req)
)

req.instance_variable_set(:@__webmock_request_signature, request_signature)

request_signature
end

def self.proxy_from_typhoeus(req)
proxy_url = req.options[:proxy]
return nil unless proxy_url && !proxy_url.empty?
proxy_uri = URI.parse(proxy_url)
proxy = {
"host" => proxy_uri.host,
"port" => proxy_uri.port
}
if req.options[:proxyuserpwd]
user, pass = req.options[:proxyuserpwd].split(":", 2)
proxy["username"] = user if user
proxy["password"] = pass if pass
end
proxy["scheme"] = proxy_uri.scheme if proxy_uri.scheme && proxy_uri.scheme != "http"
proxy
end

def self.build_webmock_response(typhoeus_response)
webmock_response = WebMock::Response.new
Expand Down
50 changes: 48 additions & 2 deletions lib/webmock/request_pattern.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,14 @@ def rSpecHashExcludingMatcher?(matcher)

class RequestPattern

attr_reader :method_pattern, :uri_pattern, :body_pattern, :headers_pattern
attr_reader :method_pattern, :uri_pattern, :body_pattern, :headers_pattern, :proxy_pattern

def initialize(method, uri, options = {})
@method_pattern = MethodPattern.new(method)
@uri_pattern = create_uri_pattern(uri)
@body_pattern = nil
@headers_pattern = nil
@proxy_pattern = nil
@with_block = nil
assign_options(options)
end
Expand All @@ -39,6 +40,7 @@ def matches?(request_signature)
@uri_pattern.matches?(request_signature.uri) &&
(@body_pattern.nil? || @body_pattern.matches?(request_signature.body, content_type || "")) &&
(@headers_pattern.nil? || @headers_pattern.matches?(request_signature.headers)) &&
(@proxy_pattern.nil? || @proxy_pattern.matches?(request_signature.proxy)) &&
(@with_block.nil? || @with_block.call(request_signature))
end

Expand All @@ -47,6 +49,7 @@ def to_s
string << " #{@uri_pattern.to_s}"
string << " with body #{@body_pattern.to_s}" if @body_pattern
string << " with headers #{@headers_pattern.to_s}" if @headers_pattern
string << " with proxy #{@proxy_pattern.to_s}" if @proxy_pattern
string << " with given block" if @with_block
string
end
Expand All @@ -56,10 +59,11 @@ def to_s

def assign_options(options)
options = WebMock::Util::HashKeysStringifier.stringify_keys!(options, deep: true)
HashValidator.new(options).validate_keys('body', 'headers', 'query', 'basic_auth')
HashValidator.new(options).validate_keys('body', 'headers', 'query', 'basic_auth', 'proxy')
set_basic_auth_as_headers!(options)
@body_pattern = BodyPattern.new(options['body']) if options.has_key?('body')
@headers_pattern = HeadersPattern.new(options['headers']) if options.has_key?('headers')
@proxy_pattern = ProxyPattern.new(options['proxy']) if options.has_key?('proxy')
@uri_pattern.add_query_params(options['query']) if options.has_key?('query')
end

Expand Down Expand Up @@ -425,4 +429,46 @@ def empty_headers?(headers)
end
end

class ProxyPattern
def initialize(pattern)
@pattern = pattern
end

def matches?(proxy)
case @pattern
when Hash
return false if proxy.nil?
normalized = normalize_keys(@pattern)
normalized.all? { |key, value| value === proxy[key] }
when String
proxy_to_uri_string(proxy) == @pattern
when Regexp
@pattern =~ proxy_to_uri_string(proxy).to_s
when NilClass
proxy.nil?
else
@pattern === proxy
end
end

def to_s
@pattern.inspect
end

private

def normalize_keys(hash)
hash.each_with_object({}) { |(k, v), h| h[k.to_s] = v }
end

def proxy_to_uri_string(proxy)
return nil if proxy.nil?
scheme = proxy["scheme"] || "http"
host = proxy["host"]
port = proxy["port"]
return nil unless host
port ? "#{scheme}://#{host}:#{port}" : "#{scheme}://#{host}"
end
end

end
6 changes: 5 additions & 1 deletion lib/webmock/request_signature.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ module WebMock

class RequestSignature

attr_accessor :method, :uri, :body
attr_accessor :method, :uri, :body, :proxy
attr_reader :headers

def initialize(method, uri, options = {})
Expand All @@ -20,6 +20,9 @@ def to_s
if headers && !headers.empty?
string << " with headers #{WebMock::Util::Headers.sorted_headers_string(headers)}"
end
if proxy && !proxy.empty?
string << " with proxy #{proxy.inspect}"
end
string
end

Expand Down Expand Up @@ -49,6 +52,7 @@ def json_headers?
def assign_options(options)
self.body = options[:body] if options.has_key?(:body)
self.headers = options[:headers] if options.has_key?(:headers)
self.proxy = options[:proxy] if options.has_key?(:proxy)
end

end
Expand Down
3 changes: 3 additions & 0 deletions lib/webmock/request_stub.rb
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,9 @@ def self.from_request_signature(signature)
if (signature.headers && !signature.headers.empty?)
stub.with(headers: signature.headers)
end
if (signature.proxy && !signature.proxy.empty?)
stub.with(proxy: signature.proxy)
end
stub
end
end
Expand Down
6 changes: 6 additions & 0 deletions lib/webmock/stub_request_snippet.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,12 @@ def to_s(with_response = true)

with << "\n headers: #{request_pattern.headers_pattern.pp_to_s}"
end

if (request_pattern.proxy_pattern)
with << "," unless with.empty?

with << "\n proxy: #{request_pattern.proxy_pattern.to_s}"
end
string << ".\n with(#{with})" unless with.empty?
if with_response
if request_pattern.headers_pattern && request_pattern.headers_pattern.matches?({ 'Accept' => "application/json" })
Expand Down
47 changes: 47 additions & 0 deletions spec/acceptance/curb/curb_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -544,4 +544,51 @@
end
end
end

describe "proxy matching" do
before(:each) do
WebMock.disable_net_connect!
WebMock.reset!
end

it "should match request with correct proxy" do
stub_request(:get, "www.example.com").with(
proxy: {"host" => "proxy.example.com", "port" => 8080}
).to_return(body: "proxied")

curl = Curl::Easy.new("http://www.example.com/")
curl.proxy_url = "http://proxy.example.com:8080"
curl.http_get
expect(curl.body_str).to eq("proxied")
end

it "should not match request with wrong proxy" do
stub_request(:get, "www.example.com").with(
proxy: {"host" => "other-proxy.example.com", "port" => 8080}
)

curl = Curl::Easy.new("http://www.example.com/")
curl.proxy_url = "http://proxy.example.com:8080"
expect {
curl.http_get
}.to raise_error(WebMock::NetConnectNotAllowedError)
end

it "should match request without proxy when proxy pattern is nil" do
stub_request(:get, "www.example.com").with(proxy: nil).to_return(body: "direct")

curl = Curl::Easy.new("http://www.example.com/")
curl.http_get
expect(curl.body_str).to eq("direct")
end

it "should match request with proxy when no proxy pattern is specified" do
stub_request(:get, "www.example.com").to_return(body: "any")

curl = Curl::Easy.new("http://www.example.com/")
curl.proxy_url = "http://proxy.example.com:8080"
curl.http_get
expect(curl.body_str).to eq("any")
end
end
end
Loading
Loading