Using a scriptable proxy to test remote API without hurting them
Sometimes when you’re using an API provided by a partner/client, and even if your code is well tested, you want to be sure that the first runs won’t hurt the other side datas. In a REST context, it means that you can let GET requests query the API while keeping an eye on PUT, POST, DELETE requests. Still, it would be nice to have a response for those requests too.
Basically you need a scriptable proxy which will let go GET through and handle the other verbs without querying the API.
For this purpose I have used em-proxy, which provides a scritable proxy based on eventmachine.
For the impatients, here the code:
#! /usr/bin/env ruby
require 'set'
require 'rubygems'
require 'em-proxy'
require 'unicorn'
require "addressable/uri"
class MyFakeWeb
@@pairs = Set.new
@@counters = Hash.new(0)
def self.register_uri(verb, url, options = {})
@@pairs << MyFakeWebPair.new(verb, url, options[:body])
end
def self.answer_to(verb, url)
hash = @@pairs.classify{|p| [p.verb, p.url.path]}
parsed_url=Addressable::URI.parse(url)
key = [verb.downcase, parsed_url.path]
response = hash[key] ? hash[key].to_a[@@counters[key] % hash[key].size] : nil
@@counters[key] += 1
response ? response.body : ''
end
end
class MyFakeWebPair
attr_accessor(:verb,:body)
attr_reader :url
def initialize(string_verb, string_url, string_body)
self.verb = string_verb.downcase
self.url = string_url
self.body = string_body
end
def url=(string_url)
@url = Addressable::URI.parse string_url
end
end
MyFakeWeb.register_uri('post', 'http://production.com/bla', :body => 'hey1')
MyFakeWeb.register_uri('post', 'http://production.com/bla', :body => 'hey2')
MyFakeWeb.register_uri('post', 'http://production.com/bla', :body => 'hey3')
MyFakeWeb.register_uri('post', 'http://production.com/bla', :body => 'hey4')
Proxy.start(:host => "0.0.0.0", :port => 8080, :debug => true) do |conn|
conn.server :production, :host => 'production.com', :port => 80
conn.on_data do |data|
parser = Unicorn::HttpParser.new
headers = parser.headers({}, data.clone)
if %w(POST PUT DELETE).include? headers['REQUEST_METHOD']
http_string = StringIO.new
body = MyFakeWeb.answer_to(headers['REQUEST_METHOD'], headers['REQUEST_URI'])
Unicorn::HttpResponse.write(http_string, [200, {'Content-Length' => body.length}, body])
conn.send_data http_string.string
data = nil
else
data.gsub!(/Host: .*?\r\n/, "Host: production.com\r\n")
end
data
end
conn.on_response do |server, resp|
resp
end
end
MyFakeWeb and MyFakeWebPair classes are juste meant to encapsulate fake responses.
The all filtering process happens in the Proxy block.- It parses the request with the HttpParser provided by Unicorn
- determine the HTTP verb and queried path
- either let it go through to the remote API (just changing the Host header to be sure that the right server will handle it) or answer with a fake response.
That’s it!