Skip to content

Commit

Permalink
Add rack middleware feature.
Browse files Browse the repository at this point in the history
  • Loading branch information
myronmarston committed Nov 25, 2010
1 parent b845bb8 commit b58808d
Show file tree
Hide file tree
Showing 7 changed files with 235 additions and 2 deletions.
5 changes: 3 additions & 2 deletions CHANGELOG.md
Expand Up @@ -4,15 +4,16 @@


[Full Changelog](http://github.com/myronmarston/vcr/compare/v1.3.3...master) [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 want to use VCR for all of your test suite). There are a few ways to
enable this: enable this:
* In your `VCR.config` block, set `allow_http_connections_when_no_cassette` * In your `VCR.config` block, set `allow_http_connections_when_no_cassette`
to true to allow HTTP requests without a cassette. to true to allow HTTP requests without a cassette.
* You can temporarily turn off VCR using `VCR.turned_off { ... }`. * 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!`. * 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. 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) ## 1.3.3 (November 21, 2010)


Expand Down
95 changes: 95 additions & 0 deletions 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"

5 changes: 5 additions & 0 deletions lib/vcr.rb
Expand Up @@ -19,6 +19,11 @@ module VCR
class CassetteInUseError < StandardError; end class CassetteInUseError < StandardError; end
class TurnedOffError < StandardError; end class TurnedOffError < StandardError; end


module Middleware
autoload :CassetteArguments, 'vcr/middleware/cassette_arguments'
autoload :Rack, 'vcr/middleware/rack'
end

def current_cassette def current_cassette
cassettes.last cassettes.last
end end
Expand Down
18 changes: 18 additions & 0 deletions 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
28 changes: 28 additions & 0 deletions 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
32 changes: 32 additions & 0 deletions 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

54 changes: 54 additions & 0 deletions 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.