Skip to content
Browse files

Add rack middleware feature.

  • Loading branch information...
1 parent b845bb8 commit b58808ddf8c8ebf7d6ac1489b9b65023685e7a90 @myronmarston committed Nov 25, 2010
View
5 CHANGELOG.md
@@ -4,15 +4,16 @@
[Full Changelog](http://github.com/myronmarston/vcr/compare/v1.3.3...master)
-* Add support for making HTTP requests without a cassette (i.e. if you don't
+* Added support for making HTTP requests without a cassette (i.e. if you don't
want to use VCR for all of your test suite). There are a few ways to
enable this:
* In your `VCR.config` block, set `allow_http_connections_when_no_cassette`
to true to allow HTTP requests without a cassette.
* You can temporarily turn off VCR using `VCR.turned_off { ... }`.
* You can toggle VCR off and on with `VCR.turn_off!` and `VCR.turn_on!`.
-* Fix bug with `ignore_localhost` config option. Previously, an error would
+* 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).
## 1.3.3 (November 21, 2010)
View
95 features/middleware/rack.feature
@@ -0,0 +1,95 @@
+Feature: rack middleware
+
+ VCR provides a rack middleware that uses a cassette for the duration of
+ a request. Simply provide VCR::Middleware::Rack with a block that sets
+ the cassette name and options. You can set these based on the rack env
+ if your block accepts two arguments.
+
+ There useful in a couple different ways:
+
+ * In a rails app, you could use this to log all HTTP API calls made by
+ the rails app (using the :all record mode). Of course, this will only
+ record HTTP API calls made in the request-response cycle--API calls that
+ are offloaded to a background job will not be logged.
+ * This can be used as middleware in a simple rack HTTP proxy, to record the
+ and replay the proxied requests.
+
+ Background:
+ Given a file named "remote_server.rb" with:
+ """
+ require 'vcr_cucumber_helpers'
+
+ request_count = 0
+ start_sinatra_app(:port => 7777) do
+ get('/:path') { "Hello #{params[:path]} #{request_count += 1}" }
+ end
+ """
+ And a file named "client.rb" with:
+ """
+ require 'remote_server'
+ require 'proxy_server'
+ require 'cgi'
+
+ url = URI.parse("http://localhost:8888?url=#{CGI.escape('http://localhost:7777/foo')}")
+
+ puts "Response 1: #{Net::HTTP.get_response(url).body}"
+ puts "Response 2: #{Net::HTTP.get_response(url).body}"
+ """
+ And the directory "cassettes" does not exist
+
+ Scenario: Use VCR rack middleware to record HTTP responses for a simple rack proxy app
+ Given a file named "proxy_server.rb" with:
+ """
+ require 'vcr'
+
+ start_sinatra_app(:port => 8888) do
+ use VCR::Middleware::Rack do |cassette|
+ cassette.name 'proxied'
+ cassette.options :record => :new_episodes
+ end
+
+ get('/') { Net::HTTP.get_response(URI.parse(params[:url])).body }
+ end
+
+ VCR.config do |c|
+ c.cassette_library_dir = 'cassettes'
+ c.stub_with :fakeweb
+ c.allow_http_connections_when_no_cassette = true
+ end
+ """
+ When I run "ruby client.rb"
+ Then the output should contain:
+ """
+ Response 1: Hello foo 1
+ Response 2: Hello foo 1
+ """
+ And the file "cassettes/proxied.yml" should contain "body: Hello foo 1"
+
+ Scenario: Set cassette name based on rack request env
+ Given a file named "proxy_server.rb" with:
+ """
+ require 'vcr'
+
+ start_sinatra_app(:port => 8888) do
+ use VCR::Middleware::Rack do |cassette, env|
+ cassette.name env['SERVER_NAME']
+ cassette.options :record => :new_episodes
+ end
+
+ get('/') { Net::HTTP.get_response(URI.parse(params[:url])).body }
+ end
+
+ VCR.config do |c|
+ c.cassette_library_dir = 'cassettes'
+ c.stub_with :fakeweb
+ c.allow_http_connections_when_no_cassette = true
+ end
+ """
+ When I run "ruby client.rb"
+ Then the output should contain:
+ """
+ Response 1: Hello foo 1
+ Response 2: Hello foo 1
+ """
+ And the file "cassettes/localhost.yml" should contain "body: Hello foo 1"
+
View
5 lib/vcr.rb
@@ -19,6 +19,11 @@ module VCR
class CassetteInUseError < StandardError; end
class TurnedOffError < StandardError; end
+ module Middleware
+ autoload :CassetteArguments, 'vcr/middleware/cassette_arguments'
+ autoload :Rack, 'vcr/middleware/rack'
+ end
+
def current_cassette
cassettes.last
end
View
18 lib/vcr/middleware/cassette_arguments.rb
@@ -0,0 +1,18 @@
+module VCR
+ module Middleware
+ class CassetteArguments
+ def initialize
+ @options = {}
+ end
+
+ def name(name = nil)
+ @name = name if name
+ @name
+ end
+
+ def options(options = {})
+ @options.merge!(options)
+ end
+ end
+ end
+end
View
28 lib/vcr/middleware/rack.rb
@@ -0,0 +1,28 @@
+module VCR
+ module Middleware
+ class Rack
+ def initialize(app, &block)
+ raise ArgumentError.new("You must provide a block to set the cassette options") unless block
+ @app, @cassette_arguments_block = app, block
+ end
+
+ def call(env)
+ VCR.use_cassette(*cassette_arguments(env)) do
+ @app.call(env)
+ end
+ end
+
+ private
+
+ def cassette_arguments(env)
+ arguments = CassetteArguments.new
+
+ block_args = [arguments]
+ block_args << env unless @cassette_arguments_block.arity == 1
+
+ @cassette_arguments_block.call(*block_args)
+ [arguments.name, arguments.options]
+ end
+ end
+ end
+end
View
32 spec/middleware/cassette_arguments_spec.rb
@@ -0,0 +1,32 @@
+require 'spec_helper'
+
+describe VCR::Middleware::CassetteArguments do
+ describe '#name' do
+ it 'initially returns nil' do
+ subject.name.should be_nil
+ end
+
+ it 'stores the given value, returning it when no arg is given' do
+ subject.name :value1
+ subject.name.should == :value1
+
+ subject.name :value2
+ subject.name.should == :value2
+ end
+ end
+
+ describe '#options' do
+ it 'initially returns an empty hash' do
+ subject.options.should == {}
+ end
+
+ it 'merges the given hash options, returning them when no arg is given' do
+ subject.options :record => :new_episodes
+ subject.options.should == { :record => :new_episodes }
+
+ subject.options :erb => true
+ subject.options.should == { :record => :new_episodes, :erb => true }
+ end
+ end
+end
+
View
54 spec/middleware/rack_spec.rb
@@ -0,0 +1,54 @@
+require 'spec_helper'
+
+describe VCR::Middleware::Rack 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) { { :env => :hash } }
+ it 'calls the provided rack app and returns its response' do
+ rack_app = mock
+ rack_app.should_receive(:call).with(env_hash).and_return(:response)
+ instance = described_class.new(rack_app) { |c| c.name 'cassette_name' }
+ instance.call(env_hash).should == :response
+ end
+
+ it 'uses a cassette when the rack app is called' do
+ VCR.current_cassette.should be_nil
+ rack_app = lambda { |env| VCR.current_cassette.should_not be_nil }
+ instance = described_class.new(rack_app) { |c| c.name 'cassette_name' }
+ instance.call({})
+ VCR.current_cassette.should be_nil
+ end
+
+ it 'sets the cassette name based on the provided block' do
+ rack_app = lambda { |env| VCR.current_cassette.name.should == 'rack_cassette' }
+ instance = described_class.new(rack_app) { |c| c.name 'rack_cassette' }
+ instance.call({})
+ end
+
+ it 'sets the cassette options based on the provided block' do
+ rack_app = lambda { |env| VCR.current_cassette.erb.should == { :foo => :bar } }
+ instance = described_class.new(rack_app) do |c|
+ c.name 'c'
+ c.options :erb => { :foo => :bar }
+ end
+
+ instance.call({})
+ end
+
+ it 'yields the rack 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

0 comments on commit b58808d

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