Skip to content

Instantly share code, notes, and snippets.

@tsnow
Last active December 28, 2015 20:19
Show Gist options
  • Save tsnow/7556209 to your computer and use it in GitHub Desktop.
Save tsnow/7556209 to your computer and use it in GitHub Desktop.
Rack VCR proxy

VCR-Proxy

This is a sinatra server which might proxy requests to a backend, and VCR that shit so you can rerun further integration tests later against the same data.

Oh. And it has some pretty limited support for mocking responses too.

bundle
ruby vcr-proxy.rb

Currently just an idea.

source 'https://rubygems.org'
gem 'faraday'
gem 'rack-proxy'
gem 'sinatra'
gem 'vcr'
gem 'json'
gem 'activerecord'
gem 'nokogiri'
GEM
remote: https://rubygems.org/
specs:
activemodel (3.2.13)
activesupport (= 3.2.13)
builder (~> 3.0.0)
activerecord (3.2.13)
activemodel (= 3.2.13)
activesupport (= 3.2.13)
arel (~> 3.0.2)
tzinfo (~> 0.3.29)
activesupport (3.2.13)
i18n (= 0.6.1)
multi_json (~> 1.0)
arel (3.0.2)
builder (3.0.4)
faraday (0.8.8)
multipart-post (~> 1.2.0)
i18n (0.6.1)
json (1.7.7)
mini_portile (0.5.2)
multi_json (1.8.2)
multipart-post (1.2.0)
nokogiri (1.6.0)
mini_portile (~> 0.5.0)
rack (1.5.2)
rack-protection (1.5.1)
rack
rack-proxy (0.5.9)
rack
sinatra (1.4.4)
rack (~> 1.4)
rack-protection (~> 1.4)
tilt (~> 1.3, >= 1.3.4)
tilt (1.4.1)
tzinfo (0.3.37)
vcr (2.7.0)
PLATFORMS
ruby
DEPENDENCIES
activerecord
faraday
json
nokogiri
rack-proxy
sinatra
vcr
# -*- coding: utf-8 -*-
require 'active_record'
require 'json'
require 'nokogiri'
ActiveRecord::Base.establish_connection({
'host' => 'localhost',
'user' => 'root'
'password' => '',
})
class Mockery < ActiveRecord::Base
class QueryArg <ActiveRecord::Base
belongs_to :mockery
end
class JSONPath <ActiveRecord::Base
belongs_to :mockery
end
class XPath < ActiveRecord::Base
belongs_to :mockery
end
class Header < ActiveRecord::Base
belongs_to :mockery
end
class FormField < ActiveRecord::Base
belongs_to :mockery
end
end
# Mockery.create(:http_method => 'GET|POST', :path => '/[0-9]+/',
# :http_code => '500', :body => '{some_json:"1"}', :headers_json =>
# '{"content-type": "application/json"}')
class Mockery < ActiveRecord::Base
has_many :query_args
has_many :json_paths
has_many :x_paths
has_many :headers
has_many :form_fields
def self.match(env)
search_path = [:http_method_and_path, :query_args, :json, :xml, :form, :headers]
base_query = self.includes([:query_args,:json_paths,:x_paths,:headers,:form_fields])
scope = search_path.inject(base_query) do |acc,field|
self.send("#{field}_scope", env, acc, field)
end
scope.last
end
def response
headers = {}
begin
headers = JSON.parse(headers_json) if headers_json
rescue JSON::ParseError => e
ActiveRecord::Base.logger.error(["headers_json", self.id,e.class.to_s, e.message, e.backtrace].to_s)
end
[http_code, body, headers]
end
def self.http_method_and_path_scope(env,scope, field)
found = scope.where(:http_method => env['request.http_method'], :path => nil)
return found.all unless found.empty?
# I wonder if this works.
found = scope.where(['? rlike http_method and'+
'? rlike path',
env['request.http_method'],
env['request.path']
])
found.all
end
def self.query_args_scope(env,scope,field)
args = Hash[URI.decode_www_form(env['query_string'])]
#ary = ::decode_www_form(“a=1&a=2&b=3”)
#p ary #=> [[‘a’, ‘1’], [‘a’, ‘2’], [‘b’, ‘3’]]
#p ary.assoc(‘a’).last
#=> ‘1’
#p ary.assoc(‘b’).last #=> ‘3’
#p ary.rassoc(‘a’).last #=> ‘2’
#p Hash[ary] # => {“a”=>“2”, “b”=>“3”}
scope.select{|i|
i.query_args.all?{|arg| args.has_key?(arg.name)}
}
end
# JSONPath.create({:path => 'a.b.1.d', :value => '"some_json_value"'})
def self.json_scope(env,scope,field)
return scope unless env['content-type'] =~ %r{application/json}
json = nil
begin
json = JSON.parse(Rack::Request.new(env).body)
rescue JSON::ParseError => e
ActiveRecord::Base.logger.error([e.class.to_s, e.message, e.backtrace].to_s)
end
return scope unless json
scope.select do |i|
i.json_paths.all? do |json_path|
value = nil
begin
value = JSON.parse(json_path.value)
rescue JSON::ParseError => e
ActiveRecord::Base.logger.error(["json_path:",json_path.id,e.class.to_s, e.message, e.backtrace].to_s)
end
curr = json;
json_path.path.split('.').all? do |j|
has = curr.has_key?(j);
next false unless has;
curr = curr[j]
next true
end &&
curr == value
end
end
end
# XPath.create(:path => '//some[shit]', :value => "<SomeXML/>")
def self.xml_scope(env,scope,field)
return scope unless env['content-type'] =~ %r{application/xml}
xml = nil
begin
xml = Nokogiri::XML.parse(Rack::Request.new(env).body)
rescue Nokogiri::SyntaxError => e
ActiveRecord::Base.logger.error(["nokogiri:",Rack::Request.new(env).body,e.class.to_s, e.message, e.backtrace].to_s)
end
return scope unless xml
scope.select do |i|
i.x_paths.all? do |x_path|
xml.search(x_path.path).to_xml == x_path.value
end
end
end
# FormField.create(:field => 'some[crap]', :value => 'some other crap')
def self.form_scope(env,scope,field)
req = Rack::Request.new(env)
return scope unless req.form_data?
form = URI::decode_www_form(req.body)
scope.select do | i|
i.form_fields.all? do |form_field|
form[form_field.field] == form_field.value
end
end
end
# Header.create(:name => 'Authorization', :value => "YourMom WW91ciBtb3RoZXIgaXMgcm90dW5kLg==")
def self.headers_scope(env,scope,field)
scope.select do |i|
i.headers.all? do |header|
env[header.name] == header.value
end
end
end
end
require './vcr_session'
require './vcr_callbacks'
require './vcr-session-base'
require './mockery'
handlers do
def wildcards(env)
mock = Mockery.match(env)
return super(env) unless mock
mock.response
end
end
get '/always_errors' do
env['path'] = '/some_error_page'
VCRSession.proxy_request(env,VCRCallbacks,VCRSession.find_or_create("error"))
end
BASE = '/:locale/services/mobile/:app/:app_version/:device_type/:os_version/:device_identifier/:brand_code'
post "#{BASE}/some_new_method" do
[200, JSON.generate({"hey" => "some new neverbefore seen shit"})]
end
self.vcr_session_base
require 'sinatra'
def self.default_handler=(handler)
@default_handler = handler
end
def self.default_handler
@default_handler
end
self.default_handler=::VCRCallbacks
def self.vcr_session_base
helpers do
def ask_vcr(env,handler=self.default_handler)
VCRSession.proxy_request(env,handler)
end
def wildcards(env)
ask_vcr(env)
end
end
get '/' do
if params[:vcr_sessions]
next(erb :home)
else
wildcards(env)
end
end
get '/vcr_sessions' do
erb :home
end
post '/vcr_sessions/create' do
VCRSession.create
cassette = VCRSession.current
next "#{cassette.cassette} created. cassette=#{cassette.cassette}"
end
post '/vcr_sessions/finish' do
next "None in play" unless VCRSession.current
cassette = VCRSession.finish_current
next "#{cassette.cassette} finished. cassette=#{cassette.cassette}"
end
post '/vcr_sessions/use' do #cassette=e6942845-bd79-4a60-33b2-6f4d41fb12e8
cassette = VCRSession.load(params[:cassette])
next "None in play" unless cassette
next "Using #{cassette.cassette}. cassette=#{cassette.cassette}"
end
get '/*' do
wildcards(env)
end
post '/*' do
wildcards(env)
end
put '/*' do
wildcards(env)
end
delete '/*' do
wildcards(env)
end
end
__END__
@@home
<html>
<body>
<h1>VCR Test Sessions</h1>
<div>
<h2>Recorded Sesssions</h2>
</div>
<ul>
<% VCRSession.all.each do |cassette| %>
<li>
<%= cassette.cassette %>
<form action="/vcr_sessions/use">
<input type="hidden" name="cassette" value="<%= cassette.cassette %>" />
<button type="submit">Use</button>
</form>
</li>
<% end %>
</ul>
</div>
<div>
<form action="/vcr_sessions/create">
<button type="submit">Create New Session</button>
</form>
</div>
<div>
<form action="/vcr_sessions/finish">
<button type="submit">Finish Session</button>
</form>
</div>
</body>
</html>
class VCRCallbacks
def self.before_record(http,cassette)
end
def self.before_playback(http,cassette)
end
def self.before_http_request(request)
end
def self.after_http_request(request,response)
end
def self.around_http_request(request)
end
end
require 'vcr'
require 'rack-proxy'
require 'guid'
require 'faraday'
class VCRSession
class << self
attr_accessor :all, :cassette_dir, :current
end
@all = []
def self.cassette_dir=(cassette_dir)
VCR.configure do |c|
c.cassette_library_dir = cassette_dir
c.hook_into :faraday
c.before_record &VCRSession.method(:before_record)
c.before_playback &VCRSession.method(:before_playback)
c.before_http_request &VCRSession.method(:before_http_request)
c.after_http_request &VCRSession.method(:after_http_request)
c.around_http_request &VCRSession.method(:around_http_request)
c.debug_logger = $stderr
end
end
attr_reader :cassette
def self.create
cassettes = self.push(self.new.cassette)
self.current = cassettes.last
end
def self.push(cassette)
next_one = self.new(cassette)
next_one.start
@all.push(next_one)
@all
end
def self.finish_current
last = self.current
last.finish
self.current = nil
last
end
def self.find(cassette)
self.all.find{|i| i.cassette == cassette}
end
def self.find_or_create(cassette)
find(cassette) || push(cassette).last
end
def self.load(cassette)
found = find(cassette)
return nil unless found
self.current = found
found.cassette
end
def self.proxy_request(env, handler, cassette=self.current)
self.handler=handler
self.with_handler do
cassette.vcr do
Rack::Proxy.new(:backend => 'http://www.trunk.ridecharge.com').call(env)
end
end
end
def with_handler(handler)
self.handler = handler
out = yield
self.handler = nil
out
end
def self.before_record(http,cassette)
self.handler.before_record(http,cassette)
end
def self.before_playback(http,cassette)
self.handler.before_playback(http,cassette)
end
def self.before_http_request(http,cassette)
self.handler.before_http_request(http,cassette)
end
def self.after_http_request(http,cassette)
self.handler.after_http_request(http,cassette)
end
def self.around_http_request(http,cassette)
self.handler.around_http_request(http,cassette)
end
def initialize(cassette=Guid.new)
@cassette = cassette
end
def vcr
VCR.use_cassette(@cassette) do
yield
end
end
self.cassette_dir = "./proxy-cassettes"
end
@tsnow
Copy link
Author

tsnow commented Nov 20, 2013

@anachronistic I had some ideas in here. But they can only be discussed in hushed tones over beers.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment