Skip to content

Commit

Permalink
Merge branch 'master' into uri_parsing_caching
Browse files Browse the repository at this point in the history
  • Loading branch information
Bartosz Blimke committed Jun 19, 2010
2 parents 5f8d6a8 + fa4ab29 commit 7b4fe5a
Show file tree
Hide file tree
Showing 21 changed files with 895 additions and 80 deletions.
66 changes: 62 additions & 4 deletions README.md
Expand Up @@ -13,10 +13,13 @@ Features
* Smart matching of the same headers in different representations.
* Support for Test::Unit
* Support for RSpec 1.x and RSpec 2.x
* Support for Net::HTTP and other http libraries based on Net::HTTP (i.e RightHttpConnection, rest-client, HTTParty)
* Support for HTTPClient library (both sync and async requests)
* Support for Patron library
* Easy to extend to other HTTP libraries

Supported HTTP libraries
------------------------

* Net::HTTP and libraries based on Net::HTTP (i.e RightHttpConnection, rest-client, HTTParty)
* HTTPClient
* Patron

Installation
------------
Expand Down Expand Up @@ -80,6 +83,20 @@ You can also use WebMock without RSpec or Test::Unit support:
res = Net::HTTP.start(uri.host, uri.port) {|http|
http.request(req, 'hello world')
} # ===> Success

### Matching requests with URL-Encoded, JSON or XML body against hash

stub_http_request(:post, "www.example.com").
with(:body => {:data => {:a => '1', :b => 'five'}})

RestClient.post('www.example.com', "data[a]=1&data[b]=five",
:content_type => 'application/x-www-form-urlencoded') # ===> Success

RestClient.post('www.example.com', '{"data":{"a":"1","b":"five"}}',
:content_type => 'application/json') # ===> Success

RestClient.post('www.example.com', '<data a="1" b="five" />',
:content_type => 'application/xml' ) # ===> Success

### Matching custom request headers

Expand Down Expand Up @@ -121,7 +138,13 @@ You can also use WebMock without RSpec or Test::Unit support:
stub_request(:any, /.*example.*/)

Net::HTTP.get('www.example.com', '/') # ===> Success
### Matching query params using hash

stub_http_request(:get, "www.example.com").with(:query => {"a" => ["b", "c"]})
RestClient.get("http://www.example.com/?a[]=b&a[]=c") # ===> Success
### Stubbing with custom response

stub_request(:any, "www.example.com").to_return(:body => "abc", :status => 200, :headers => { 'Content-Length' => 3 } )
Expand Down Expand Up @@ -179,9 +202,19 @@ You can also use WebMock without RSpec or Test::Unit support:

### Raising errors

#### Exception declared by class

stub_request(:any, 'www.example.net').to_raise(StandardError)

RestClient.post('www.example.net', 'abc') # ===> StandardError

#### or by exception instance

stub_request(:any, 'www.example.net').to_raise(StandardError.new("some error"))

#### or by string

stub_request(:any, 'www.example.net').to_raise("some error")

### Raising timeout errors

Expand Down Expand Up @@ -282,6 +315,11 @@ You can also use WebMock without RSpec or Test::Unit support:
WebMock.should_not have_requested(:get, "www.something.com")

WebMock.should have_requested(:post, "www.example.com").with { |req| req.body == "abc" }

WebMock.should have_requested(:get, "www.example.com").with(:query => {"a" => ["b", "c"]})

WebMock.should have_requested(:get, "www.example.com").
with(:body => {"a" => ["b", "c"]}, :headers => 'Content-Type' => 'application/json')

### Different way of setting expectations in RSpec

Expand All @@ -293,6 +331,11 @@ You can also use WebMock without RSpec or Test::Unit support:

request(:post, "www.example.com").with { |req| req.body == "abc" }.should have_been_made

request(:get, "www.example.com").with(:query => {"a" => ["b", "c"]}).should have_been_made

request(:post, "www.example.com").
with(:body => {"a" => ["b", "c"]}, :headers => 'Content-Type' => 'application/json').should have_been_made

## Clearing stubs and request history

If you want to reset all current stubs and history of requests use `WebMock.reset_webmock`
Expand Down Expand Up @@ -402,6 +445,20 @@ i.e the following two sets of headers are equal:

To record your application's real HTTP interactions and replay them later in tests you can use [VCR](http://github.com/myronmarston/vcr) with WebMock.

## Request callbacks

####WebMock can invoke a callback for each, stubbed or real, request:

WebMock.after_request do |request_signature, response|
Log.info("Request #{request_signature} was made and #{response} was returned")
end

#### invoke callbacks for real requests only and except requests made with Patron

WebMock.after_request(:except => [:patron], :real_requests_only => true) do |request_signature, response|
Log.info("Request #{request_signature} was made and #{response} was returned")
end

## Bugs and Issues

Please submit them here [http://github.com/bblimke/webmock/issues](http://github.com/bblimke/webmock/issues)
Expand Down Expand Up @@ -439,6 +496,7 @@ People who submitted patches and new features or suggested improvements. Many th
* Jose Angel Cortinas
* Razic
* Steve Tooke
* Nathaniel Bibler

## Background

Expand Down
1 change: 1 addition & 0 deletions Rakefile
Expand Up @@ -11,6 +11,7 @@ begin
gem.homepage = "http://github.com/bblimke/webmock"
gem.authors = ["Bartosz Blimke"]
gem.add_dependency "addressable", ">= 2.1.1"
gem.add_dependency "crack", ">=0.1.7"
gem.add_development_dependency "rspec", ">= 1.2.9"
gem.add_development_dependency "httpclient", ">= 2.1.5.2"
gem.add_development_dependency "patron", ">= 0.4.5" unless RUBY_PLATFORM =~ /java/
Expand Down
2 changes: 2 additions & 0 deletions lib/webmock.rb
@@ -1,6 +1,7 @@
require 'singleton'

require 'addressable/uri'
require 'crack'

require 'webmock/http_lib_adapters/net_http'
require 'webmock/http_lib_adapters/httpclient'
Expand All @@ -20,5 +21,6 @@

require 'webmock/request_execution_verifier'
require 'webmock/config'
require 'webmock/callback_registry'
require 'webmock/request_registry'
require 'webmock/webmock'
35 changes: 35 additions & 0 deletions lib/webmock/callback_registry.rb
@@ -0,0 +1,35 @@
module WebMock
class CallbackRegistry
@@callbacks = []

def self.add_callback(options, block)
@@callbacks << {:options => options, :block => block}
end

def self.callbacks
@@callbacks
end

def self.invoke_callbacks(options, request_signature, response)
return if @@callbacks.empty?
CallbackRegistry.callbacks.each do |callback|
except = callback[:options][:except]
real_only = callback[:options][:real_requests_only]
unless except && except.include?(options[:lib])
if !real_only || options[:real_request]
callback[:block].call(request_signature, response)
end
end
end
end

def self.reset
@@callbacks = []
end

def self.any_callbacks?
!@@callbacks.empty?
end

end
end
35 changes: 31 additions & 4 deletions lib/webmock/http_lib_adapters/httpclient.rb
Expand Up @@ -18,15 +18,27 @@ def do_get_with_webmock(req, proxy, conn, stream = false, &block)
if WebMock.registered_request?(request_signature)
webmock_response = WebMock.response_for_request(request_signature)
response = build_httpclient_response(webmock_response, stream, &block)
conn.push(response)
res = conn.push(response)
WebMock::CallbackRegistry.invoke_callbacks(
{:lib => :http_client}, request_signature, webmock_response)
res
elsif WebMock.net_connect_allowed?(request_signature.uri)
if stream
res = if stream
do_get_stream_without_webmock(req, proxy, conn, &block)
else
do_get_block_without_webmock(req, proxy, conn, &block)
end
res = conn.pop
conn.push(res)
if WebMock::CallbackRegistry.any_callbacks?
webmock_response = build_webmock_response(res)
WebMock::CallbackRegistry.invoke_callbacks(
{:lib => :http_client, :real_request => true}, request_signature,
webmock_response)
end
res
else
raise NetConnectNotAllowedError.new(request_signature)
raise WebMock::NetConnectNotAllowedError.new(request_signature)
end
end

Expand All @@ -37,7 +49,7 @@ def do_request_async_with_webmock(method, uri, query, body, extheader)
if WebMock.registered_request?(request_signature) || WebMock.net_connect_allowed?(request_signature.uri)
do_request_async_without_webmock(method, uri, query, body, extheader)
else
raise NetConnectNotAllowedError.new(request_signature)
raise WebMock::NetConnectNotAllowedError.new(request_signature)
end
end

Expand Down Expand Up @@ -65,6 +77,21 @@ def build_httpclient_response(webmock_response, stream = false, &block)
response
end
end

def build_webmock_response(httpclient_response)
webmock_response = WebMock::Response.new
webmock_response.status = [httpclient_response.status, httpclient_response.reason]
webmock_response.headers = httpclient_response.header.all
if httpclient_response.content.respond_to?(:read)
webmock_response.body = httpclient_response.content.read
body = HTTP::Message::Body.new
body.init_response(StringIO.new(webmock_response.body))
httpclient_response.body = body
else
webmock_response.body = httpclient_response.content
end
webmock_response
end

def build_request_signature(req)
uri = WebMock::Util::URI.heuristic_parse(req.header.request_uri.to_s)
Expand Down
24 changes: 23 additions & 1 deletion lib/webmock/http_lib_adapters/net_http.rb
@@ -1,6 +1,7 @@
require 'net/http'
require 'net/https'
require 'stringio'
require File.join(File.dirname(__FILE__), 'net_http_response')

class StubSocket #:nodoc:

Expand Down Expand Up @@ -60,10 +61,20 @@ def request_with_webmock(request, body = nil, &block)
if WebMock.registered_request?(request_signature)
@socket = Net::HTTP.socket_type.new
webmock_response = WebMock.response_for_request(request_signature)
WebMock::CallbackRegistry.invoke_callbacks(
{:lib => :net_http}, request_signature, webmock_response)
build_net_http_response(webmock_response, &block)
elsif WebMock.net_connect_allowed?(request_signature.uri)
connect_without_webmock
request_without_webmock(request, nil, &block)
response = request_without_webmock(request, nil)
if WebMock::CallbackRegistry.any_callbacks? && started?
webmock_response = build_webmock_response(response)
WebMock::CallbackRegistry.invoke_callbacks(
{:lib => :net_http, :real_request => true}, request_signature, webmock_response)
response.extend WebMock::Net::HTTPResponse
end
yield response if block_given?
response
else
raise WebMock::NetConnectNotAllowedError.new(request_signature)
end
Expand Down Expand Up @@ -99,11 +110,22 @@ def build_net_http_response(webmock_response, &block)

response
end

def build_webmock_response(net_http_response)
webmock_response = WebMock::Response.new
webmock_response.status = [
net_http_response.code.to_i,
net_http_response.message]
webmock_response.headers = net_http_response.to_hash
webmock_response.body = net_http_response.body
webmock_response
end

end

end


module WebMock
module NetHTTPUtility

Expand Down
49 changes: 49 additions & 0 deletions lib/webmock/http_lib_adapters/net_http_response.rb
@@ -0,0 +1,49 @@
# This code is entierly copied from VCR (http://github.com/myronmarston/vcr) by courtesy of Myron Marston

# A Net::HTTP response that has already been read raises an IOError when #read_body
# is called with a destination string or block.
#
# This causes a problem when VCR records a response--it reads the body before yielding
# the response, and if the code that is consuming the HTTP requests uses #read_body, it
# can cause an error.
#
# This is a bit of a hack, but it allows a Net::HTTP response to be "re-read"
# after it has aleady been read. This attemps to preserve the behavior of
# #read_body, acting just as if it had never been read.

module WebMock
module Net
module HTTPResponse
def self.extended(object)
body_object = object.instance_variable_get(:@body)
object.instance_variable_set(:@__orig_body__,
case body_object
when String then body_object
else raise ArgumentError.new("Unexpected body object: #{body_object}")
end
)
end

def read_body(dest = nil, &block)
if @__orig_body__
if dest && block
raise ArgumentError.new("both arg and block given for HTTP method")
elsif dest
dest << @__orig_body__
elsif block
@body = ::Net::ReadAdapter.new(block)
@body << @__orig_body__
@body
else
@body = @__orig_body__
end
else
super
end
ensure
# allow subsequent calls to #read_body to proceed as normal, without our hack...
@__orig_body__ = nil
end
end
end
end
23 changes: 21 additions & 2 deletions lib/webmock/http_lib_adapters/patron.rb
Expand Up @@ -11,9 +11,19 @@ def handle_request_with_webmock(req)
if WebMock.registered_request?(request_signature)
webmock_response = WebMock.response_for_request(request_signature)
handle_file_name(req, webmock_response)
build_patron_response(webmock_response)
res = build_patron_response(webmock_response)
WebMock::CallbackRegistry.invoke_callbacks(
{:lib => :patron}, request_signature, webmock_response)
res
elsif WebMock.net_connect_allowed?(request_signature.uri)
handle_request_without_webmock(req)
res = handle_request_without_webmock(req)
if WebMock::CallbackRegistry.any_callbacks?
webmock_response = build_webmock_response(res)
WebMock::CallbackRegistry.invoke_callbacks(
{:lib => :patron, :real_request => true}, request_signature,
webmock_response)
end
res
else
raise WebMock::NetConnectNotAllowedError.new(request_signature)
end
Expand Down Expand Up @@ -74,6 +84,15 @@ def build_patron_response(webmock_response)
res.instance_variable_set(:@headers, webmock_response.headers)
res
end

def build_webmock_response(patron_response)
webmock_response = WebMock::Response.new
reason = patron_response.status_line.scan(%r(\AHTTP/(\d+\.\d+)\s+(\d\d\d)\s*([^\r\n]+)?\r?\z))[0][2]
webmock_response.status = [patron_response.status, reason]
webmock_response.body = patron_response.body
webmock_response.headers = patron_response.headers
webmock_response
end

end
end
Expand Down

0 comments on commit 7b4fe5a

Please sign in to comment.