diff --git a/CHANGELOG.md b/CHANGELOG.md index 65104263..28c6c5e1 100644 --- a/CHANGELOG.md +++ b/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) diff --git a/features/middleware/rack.feature b/features/middleware/rack.feature new file mode 100644 index 00000000..c68f6dbc --- /dev/null +++ b/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" + diff --git a/lib/vcr.rb b/lib/vcr.rb index ceea610d..bceaf7ab 100644 --- a/lib/vcr.rb +++ b/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 diff --git a/lib/vcr/middleware/cassette_arguments.rb b/lib/vcr/middleware/cassette_arguments.rb new file mode 100644 index 00000000..83fc0a63 --- /dev/null +++ b/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 diff --git a/lib/vcr/middleware/rack.rb b/lib/vcr/middleware/rack.rb new file mode 100644 index 00000000..e83d0a44 --- /dev/null +++ b/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 diff --git a/spec/middleware/cassette_arguments_spec.rb b/spec/middleware/cassette_arguments_spec.rb new file mode 100644 index 00000000..9e517c7c --- /dev/null +++ b/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 + diff --git a/spec/middleware/rack_spec.rb b/spec/middleware/rack_spec.rb new file mode 100644 index 00000000..daf3c374 --- /dev/null +++ b/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