Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Add Faraday support.

  • Loading branch information...
commit 4f8e46720d8cd9ed703d2042df56c04f2faf5441 1 parent 2e4655c
@myronmarston authored
View
1  CHANGELOG.md
@@ -14,6 +14,7 @@
* Fixed bug with `ignore_localhost` config option. Previously, an error would
be raised if it was set before the `stub_with` option.
* Added VCR::Middleware::Rack (see features/middleware/rack.feature for usage).
+* Added support for Faraday (see features/middleware/faraday.feature for usage).
## 1.3.3 (November 21, 2010)
View
89 features/middleware/faraday.feature
@@ -0,0 +1,89 @@
+Feature: Faraday middleware
+
+ VCR provides middleware that can be used with Faraday. You can use this as
+ an alternative to Faraday's built-in test adapter.
+
+ To use VCR with Faraday, you should configure VCR to stub with faraday and
+ use the provided middleware. The middleware should come before the Faraday
+ HTTP adapter. You should provide the middleware with a block where you set
+ the cassette name and options. If your block accepts two arguments, the
+ env hash will be yielded, allowing you to dynamically set the cassette name
+ and options based on the request environment.
+
+ Background:
+ Given a file named "env_setup.rb" with:
+ """
+ require 'vcr_cucumber_helpers'
+
+ request_count = 0
+ start_sinatra_app(:port => 7777) do
+ get('/:path') { "Hello #{params[:path]} #{request_count += 1}" }
+ end
+
+ require 'vcr'
+
+ VCR.config do |c|
+ c.cassette_library_dir = 'cassettes'
+ c.stub_with :faraday
+ end
+ """
+
+ Scenario Outline: Use Faraday middleware
+ Given a file named "faraday_example.rb" with:
+ """
+ require 'env_setup'
+
+ conn = Faraday::Connection.new(:url => 'http://localhost:7777') do |builder|
+ builder.use VCR::Middleware::Faraday do |cassette|
+ cassette.name 'faraday_example'
+ cassette.options :record => :new_episodes
+ end
+
+ builder.adapter :<adapter>
+ end
+
+ puts "Response 1: #{conn.get('/foo').body}"
+ puts "Response 2: #{conn.get('/foo').body}"
+ """
+ When I run "ruby faraday_example.rb"
+ Then the output should contain:
+ """
+ Response 1: Hello foo 1
+ Response 2: Hello foo 1
+ """
+ And the file "cassettes/faraday_example.yml" should contain "body: Hello foo 1"
+
+ Examples:
+ | adapter |
+ | net_http |
+ | typhoeus |
+
+ Scenario: Set cassette name based on faraday env
+ Given a file named "faraday_example.rb" with:
+ """
+ require 'env_setup'
+
+ conn = Faraday::Connection.new(:url => 'http://localhost:7777') do |builder|
+ builder.use VCR::Middleware::Faraday do |cassette, env|
+ cassette.name env[:url].path.sub(/^\//, '')
+ cassette.options :record => :new_episodes
+ end
+
+ builder.adapter :net_http
+ end
+
+ puts "Response 1: #{conn.get('/foo').body}"
+ puts "Response 2: #{conn.get('/foo').body}"
+ puts "Response 3: #{conn.get('/bar').body}"
+ puts "Response 4: #{conn.get('/bar').body}"
+ """
+ When I run "ruby faraday_example.rb"
+ Then the output should contain:
+ """
+ Response 1: Hello foo 1
+ Response 2: Hello foo 1
+ Response 3: Hello bar 2
+ Response 4: Hello bar 2
+ """
+ And the file "cassettes/foo.yml" should contain "body: Hello foo 1"
+ And the file "cassettes/bar.yml" should contain "body: Hello bar 2"
View
2  lib/vcr.rb
@@ -22,6 +22,7 @@ class TurnedOffError < StandardError; end
module Middleware
autoload :CassetteArguments, 'vcr/middleware/cassette_arguments'
autoload :Common, 'vcr/middleware/common'
+ autoload :Faraday, 'vcr/middleware/faraday'
autoload :Rack, 'vcr/middleware/rack'
end
@@ -79,6 +80,7 @@ def http_stubbing_adapter
when :fakeweb; HttpStubbingAdapters::FakeWeb
when :webmock; HttpStubbingAdapters::WebMock
when :typhoeus; HttpStubbingAdapters::Typhoeus
+ when :faraday; HttpStubbingAdapters::Faraday
else raise ArgumentError.new("#{lib.inspect} is not a supported HTTP stubbing library.")
end
end
View
1  lib/vcr/http_stubbing_adapters/common.rb
@@ -1,6 +1,7 @@
module VCR
module HttpStubbingAdapters
autoload :FakeWeb, 'vcr/http_stubbing_adapters/fakeweb'
+ autoload :Faraday, 'vcr/http_stubbing_adapters/faraday'
autoload :MultiObjectProxy, 'vcr/http_stubbing_adapters/multi_object_proxy'
autoload :Typhoeus, 'vcr/http_stubbing_adapters/typhoeus'
autoload :WebMock, 'vcr/http_stubbing_adapters/webmock'
View
80 lib/vcr/http_stubbing_adapters/faraday.rb
@@ -0,0 +1,80 @@
+require 'faraday'
+
+module VCR
+ module HttpStubbingAdapters
+ module Faraday
+ include Common
+ extend self
+
+ MINIMUM_VERSION = '0.5.3'
+ MAXIMUM_VERSION = '0.5'
+
+ attr_writer :http_connections_allowed, :ignore_localhost
+
+ def http_connections_allowed?
+ !!@http_connections_allowed
+ end
+
+ def ignore_localhost?
+ !!@ignore_localhost
+ end
+
+ def stub_requests(http_interactions, match_attributes)
+ grouped_responses(http_interactions, match_attributes).each do |request_matcher, responses|
+ queue = stub_queues[request_matcher]
+ responses.each { |res| queue << res }
+ end
+ end
+
+ def create_stubs_checkpoint(checkpoint_name)
+ checkpoints[checkpoint_name] = stub_queue_dup
+ end
+
+ def restore_stubs_checkpoint(checkpoint_name)
+ @stub_queues = checkpoints.delete(checkpoint_name)
+ end
+
+ def stubbed_response_for(request_matcher)
+ queue = stub_queues[request_matcher]
+ return queue.shift if queue.size > 1
+ queue.first
+ end
+
+ def reset!
+ instance_variables.each do |ivar|
+ remove_instance_variable(ivar)
+ end
+ end
+
+ private
+
+ def version
+ ::Faraday::VERSION
+ end
+
+ def checkpoints
+ @checkpoints ||= {}
+ end
+
+ def stub_queues
+ @stub_queues ||= hash_of_arrays
+ end
+
+ def stub_queue_dup
+ dup = hash_of_arrays
+
+ stub_queues.each do |k, v|
+ dup[k] = v.dup
+ end
+
+ dup
+ end
+
+ def hash_of_arrays
+ Hash.new { |h, k| h[k] = [] }
+ end
+ end
+ end
+end
+
+VCR::HttpStubbingAdapters::Common.add_vcr_info_to_exception_message(VCR::Middleware::Faraday::HttpConnectionNotAllowedError)
View
72 lib/vcr/middleware/faraday.rb
@@ -0,0 +1,72 @@
+require 'faraday'
+
+module VCR
+ module Middleware
+ class Faraday < ::Faraday::Middleware
+ include Common
+
+ class HttpConnectionNotAllowedError < StandardError; end
+
+ # TODO: disable typhoeus/webmock/net_http adapters in here so that we don't record multiple times.
+ def call(env)
+ VCR.use_cassette(*cassette_arguments(env)) do |cassette|
+ request = request_for(env)
+ request_matcher = request.matcher(cassette.match_requests_on)
+
+ if VCR::HttpStubbingAdapters::Faraday.ignore_localhost? && VCR::LOCALHOST_ALIASES.include?(URI.parse(request.uri).host)
+ @app.call(env)
+ elsif response = VCR::HttpStubbingAdapters::Faraday.stubbed_response_for(request_matcher)
+ env.update(
+ :status => response.status.code,
+ :response_headers => correctly_cased_headers(response.headers),
+ :body => response.body
+ )
+
+ env[:response].finish(env)
+ elsif VCR::HttpStubbingAdapters::Faraday.http_connections_allowed?
+ response = @app.call(env)
+ VCR.record_http_interaction(VCR::HTTPInteraction.new(request, response_for(env)))
+ response
+ else
+ raise HttpConnectionNotAllowedError.new(
+ "Real HTTP connections are disabled. Request: #{request.method.inspect} #{request.uri}"
+ )
+ end
+ end
+ end
+
+ private
+
+ def request_for(env)
+ VCR::Request.new(
+ env[:method],
+ env[:url].to_s,
+ env[:body],
+ env[:request_headers]
+ )
+ end
+
+ def response_for(env)
+ response = env[:response]
+
+ VCR::Response.new(
+ VCR::ResponseStatus.new(response.status, nil),
+ response.headers,
+ response.body,
+ '1.1'
+ )
+ end
+
+ def correctly_cased_headers(headers)
+ correctly_cased_hash = {}
+
+ headers.each do |key, value|
+ key = key.to_s.split('-').map { |segment| segment.capitalize }.join("-")
+ correctly_cased_hash[key] = value
+ end
+
+ correctly_cased_hash
+ end
+ end
+ end
+end
View
86 spec/http_stubbing_adapters/faraday_spec.rb
@@ -0,0 +1,86 @@
+require 'spec_helper'
+
+describe VCR::HttpStubbingAdapters::Faraday do
+ without_monkey_patches :all
+ without_webmock_callbacks
+ without_typhoeus_callbacks
+
+ it_behaves_like 'an http stubbing adapter',
+ %w[ faraday-typhoeus faraday-net_http faraday-patron ],
+ [:method, :uri, :host, :path, :body, :headers],
+ :status_message_not_exposed, :does_not_support_rotating_responses
+
+ it_performs('version checking',
+ :valid => %w[ 0.5.3 0.5.10 ],
+ :too_low => %w[ 0.5.2 0.4.99 ],
+ :too_high => %w[ 0.6.0 1.0.0 ]
+ ) do
+ disable_warnings
+ before(:each) { @orig_version = Faraday::VERSION }
+ after(:each) { Faraday::VERSION = @orig_version }
+
+ # Cannot be regular method def as that raises a "dynamic constant assignment" error
+ define_method :stub_version do |version|
+ ::Faraday::VERSION = version
+ end
+ end
+
+ context 'when some request have been stubbed' do
+ subject { described_class }
+ let(:request_1) { VCR::Request.new(:get, 'http://foo.com') }
+ let(:request_2) { VCR::Request.new(:get, 'http://bazz.com') }
+ let(:match_attributes) { [:method, :uri] }
+
+ def stubbed_response_for(request)
+ matcher = VCR::RequestMatcher.new(request, match_attributes)
+ subject.stubbed_response_for(matcher)
+ end
+
+ before(:each) do
+ subject.stub_requests(
+ [
+ VCR::HTTPInteraction.new(request_1, :response_1),
+ VCR::HTTPInteraction.new(request_1, :response_2),
+ ], match_attributes
+ )
+ end
+
+ def test_stubbed_responses
+ stubbed_response_for(request_1).should == :response_1
+ stubbed_response_for(request_1).should == :response_2
+ stubbed_response_for(request_1).should == :response_2
+ stubbed_response_for(request_1).should == :response_2
+ end
+
+ describe '.stubbed_response_for' do
+ it 'returns nil when there is no matching response' do
+ stubbed_response_for(request_2).should be_nil
+ end
+
+ it 'dequeues each response and continues to return the last one' do
+ test_stubbed_responses
+ end
+ end
+
+ describe '.restore_stubs_checkpoints' do
+ before(:each) do
+ subject.create_stubs_checkpoint(:checkpoint_1)
+ end
+
+ it 'restores the queues to the checkpoint state when a queue has additional responses' do
+ subject.stub_requests( [
+ VCR::HTTPInteraction.new(request_1, :response_3),
+ ], match_attributes)
+
+ subject.restore_stubs_checkpoint(:checkpoint_1)
+ test_stubbed_responses
+ end
+
+ it 'restores the queues to the checkpoint state when a queue has been used' do
+ stubbed_response_for(request_1)
+ subject.restore_stubs_checkpoint(:checkpoint_1)
+ test_stubbed_responses
+ end
+ end
+ end
+end
View
52 spec/middleware/faraday_spec.rb
@@ -0,0 +1,52 @@
+require 'spec_helper'
+
+describe VCR::Middleware::Faraday do
+ describe '.new' do
+ it 'raises an error if no cassette arguments block is provided' do
+ expect {
+ described_class.new(lambda { |env| })
+ }.to raise_error(ArgumentError)
+ end
+ end
+
+ describe '#call' do
+ let(:env_hash) { { :url => 'http://localhost:3000/' } }
+
+ before(:each) do
+ VCR::HttpStubbingAdapters::Faraday.ignore_localhost = true
+ end
+
+ it 'uses a cassette when the app is called' do
+ VCR.current_cassette.should be_nil
+ app = lambda { |env| VCR.current_cassette.should_not be_nil }
+ instance = described_class.new(app) { |c| c.name 'cassette_name' }
+ instance.call(env_hash)
+ VCR.current_cassette.should be_nil
+ end
+
+ it 'sets the cassette name based on the provided block' do
+ app = lambda { |env| VCR.current_cassette.name.should == 'rack_cassette' }
+ instance = described_class.new(app) { |c| c.name 'rack_cassette' }
+ instance.call(env_hash)
+ end
+
+ it 'sets the cassette options based on the provided block' do
+ app = lambda { |env| VCR.current_cassette.erb.should == { :foo => :bar } }
+ instance = described_class.new(app) do |c|
+ c.name 'c'
+ c.options :erb => { :foo => :bar }
+ end
+
+ instance.call(env_hash)
+ end
+
+ it 'yields the env to the provided block when the block accepts 2 arguments' do
+ instance = described_class.new(lambda { |env| }) do |c, env|
+ env.should == env_hash
+ c.name 'c'
+ end
+
+ instance.call(env_hash)
+ end
+ end
+end
View
1  spec/monkey_patches.rb
@@ -95,6 +95,7 @@ def realias(klass, method, alias_extension)
require 'patron'
require 'em-http-request'
require 'curb'
+ require 'typhoeus'
end
# The FakeWeb adapter must be required after WebMock's so
View
3  spec/spec_helper.rb
@@ -19,6 +19,7 @@
config.extend DisableWarnings
config.extend MonkeyPatches::RSpecMacros
config.extend WebMockMacros
+ config.extend TyphoeusMacros
config.color_enabled = true
config.debug = RUBY_INTERPRETER == :mri
@@ -35,6 +36,8 @@
FakeWeb.allow_net_connect = true
FakeWeb.clean_registry
+
+ VCR::HttpStubbingAdapters::Faraday.reset!
end
config.filter_run :focus => true
View
77 spec/support/http_library_adapters.rb
@@ -112,6 +112,62 @@ def make_http_request(method, url, body = nil, headers = {})
end
end
+%w[ net_http typhoeus patron ].each do |_faraday_adapter|
+ HTTP_LIBRARY_ADAPTERS["faraday-#{_faraday_adapter}"] = Module.new do
+ class << self; self; end.class_eval do
+ define_method(:http_library_name) do
+ "Faraday (#{_faraday_adapter})"
+ end
+ end
+
+ define_method(:faraday_adapter) { _faraday_adapter.to_sym }
+
+ def get_body_string(response)
+ response.body
+ end
+
+ def get_header(header_key, response)
+ response.headers[header_key]
+ end
+
+ def make_http_request(method, url, body = nil, headers = {})
+ url_root, url_rest = split_url(url)
+
+ faraday_connection(url_root).send(method) do |req|
+ req.url url_rest
+ headers.each { |k, v| req[k] = v }
+ req.body = body if body
+ end
+ end
+
+ def split_url(url)
+ uri = URI.parse(url)
+ url_root = "#{uri.scheme}://#{uri.host}:#{uri.port}"
+ rest = url.sub(url_root, '')
+
+ [url_root, rest]
+ end
+
+ def faraday_connection(url_root)
+ Faraday::Connection.new(:url => url_root) do |builder|
+ builder.use VCR::Middleware::Faraday do |cassette|
+ cassette.name 'faraday_example'
+
+ if respond_to?(:match_requests_on)
+ cassette.options :match_requests_on => match_requests_on
+ end
+
+ if respond_to?(:record_mode)
+ cassette.options :record => record_mode
+ end
+ end
+
+ builder.adapter faraday_adapter
+ end
+ end
+ end
+end
+
NET_CONNECT_NOT_ALLOWED_ERROR = /You can use VCR to automatically record this request and replay it later/
module HttpLibrarySpecs
@@ -139,7 +195,9 @@ def self.matching_on(attribute, valid, invalid, &block)
supported_request_match_attributes = @supported_request_match_attributes
describe ":#{attribute}" do
- let(:perform_stubbing) { subject.stub_requests(interactions, [attribute]) }
+ let(:perform_stubbing) { subject.stub_requests(interactions, match_requests_on) }
+ let(:match_requests_on) { [attribute] }
+ let(:record_mode) { :none }
if supported_request_match_attributes.include?(attribute)
before(:each) { perform_stubbing }
@@ -199,7 +257,7 @@ def make_http_request(headers)
end
end
- def self.test_real_http_request(http_allowed)
+ def self.test_real_http_request(http_allowed, *other)
let(:url) { "http://localhost:#{VCR::SinatraApp.port}/foo" }
if http_allowed
@@ -238,7 +296,7 @@ def self.test_real_http_request(http_allowed)
it 'records the response status message' do
recorded_interaction.response.status.message.should == 'OK'
- end
+ end unless other.include?(:status_message_not_exposed)
it 'records the response body' do
recorded_interaction.response.body.should == 'FOO!'
@@ -249,6 +307,8 @@ def self.test_real_http_request(http_allowed)
end
end
else
+ let(:record_mode) { :none }
+
it 'does not allow real HTTP requests or record them' do
VCR.should_receive(:record_http_interaction).never
expect { make_http_request(:get, url) }.to raise_error(NET_CONNECT_NOT_ALLOWED_ERROR)
@@ -273,11 +333,12 @@ def test_request_stubbed(method, url, expected)
subject.http_connections_allowed?.should == http_allowed
end
- test_real_http_request(http_allowed)
+ test_real_http_request(http_allowed, *other)
unless http_allowed
describe '.ignore_localhost =' do
localhost_response = "Localhost response"
+ let(:record_mode) { :none }
VCR::LOCALHOST_ALIASES.each do |localhost_alias|
describe 'when set to true' do
@@ -320,7 +381,7 @@ def test_request_stubbed(method, url, expected)
it 'gets the stubbed responses when requests are made to http://example.com/foo, and does not record them' do
VCR.should_receive(:record_http_interaction).never
- get_body_string(make_http_request(:get, 'http://example.com/foo')).should == 'example.com get response 1 with path=foo'
+ get_body_string(make_http_request(:get, 'http://example.com/foo')).should =~ /example\.com get response \d with path=foo/
end
it 'rotates through multiple responses for the same request' do
@@ -330,16 +391,16 @@ def test_request_stubbed(method, url, expected)
# subsequent requests keep getting the last one
get_body_string(make_http_request(:get, 'http://example.com/foo')).should == 'example.com get response 2 with path=foo'
get_body_string(make_http_request(:get, 'http://example.com/foo')).should == 'example.com get response 2 with path=foo'
- end
+ end unless other.include?(:does_not_support_rotating_responses)
it "correctly handles stubbing multiple values for the same header" do
- get_header('Set-Cookie', make_http_request(:get, 'http://example.com/two_set_cookie_headers')).should =~ ['bar=bazz', 'foo=bar']
+ get_header('Set-Cookie', make_http_request(:get, 'http://example.com/two_set_cookie_headers')).should =~ ['bar=bazz', 'foo=bar']
end
context 'when we restore our previous check point' do
before(:each) { subject.restore_stubs_checkpoint(:my_checkpoint) }
- test_real_http_request(http_allowed)
+ test_real_http_request(http_allowed, *other)
if other.include?(:needs_net_http_extension)
it 'returns false from #request_stubbed?' do
View
12 spec/support/typhoeus_macros.rb
@@ -0,0 +1,12 @@
+module TyphoeusMacros
+ def without_typhoeus_callbacks
+ before(:all) do
+ @original_typhoeus_callbacks = ::Typhoeus::Hydra.global_hooks.dup
+ ::Typhoeus::Hydra.clear_global_hooks
+ end
+
+ after(:all) do
+ ::Typhoeus::Hydra.global_hooks = @original_typhoeus_callbacks
+ end
+ end
+end
View
3  spec/vcr_spec.rb
@@ -138,7 +138,8 @@ def insert_cassette
{
:fakeweb => VCR::HttpStubbingAdapters::FakeWeb,
- :webmock => VCR::HttpStubbingAdapters::WebMock
+ :webmock => VCR::HttpStubbingAdapters::WebMock,
+ :faraday => VCR::HttpStubbingAdapters::Faraday
}.each do |symbol, klass|
it "returns #{klass} for :#{symbol}" do
VCR::Config.stub_with symbol

0 comments on commit 4f8e467

Please sign in to comment.
Something went wrong with that request. Please try again.