about

rss

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!

<<

Fuzzycom le tumblelog de Vincent Hellot