I recently found myself wanting to create a HTTP proxy to intercept certain REST requests and translate them into completely different REST requests while at the same time redirecting them to a new location.
You might not want to do EXACTLY what I wanted to do, but there are many similar tasks that either require a proxy or are greatly simplified by a proxy. In my case I essentially wanted to extend an existing application to a new purpose without actually changing any code in that application. This is the sort of thing proxies are great at. You have some existing application and you either don't want to change the code (risk breaking existing functionality/incur large testing overhead), or you don't even have any code for it. As long as you understand the protocol, you can use a proxy to intercept and modify this data thereby adapting an existing application to new purposes without actually touching the old application/code (with the small required exception of pointing it to your proxy).
If your code/application is communicating via HTTP here are some ideas of things that you might want to do with a proxy:
1 - Insert or remove a specific HTTP Header
2 - Redirect some (but not all) traffic to a different service
3 - Deny access to specific sites/resources
4 - Replace specific resources with custom resources
5 - Log or inspect traffic as it flows across the proxy (debugging)
If you want to do anything like this, you may find this proxy code useful:
A simple HTTP Proxy using Ruby and Sinatra
I have stripped out all of the code specific to my task, and what is left is a simple proxy that intercepts HTTP request and forwards them on to their new location. It is ready for you to drop in a few lines of code to accomplish your specific task.
For example:
If you wanted to add a specific header to your requests before passing it along you could do this in one line right after the headers are retrieved from the request and before being passed along:
headers = getHeadersFromRequest
#headers['yourcustomheader']='yourcustomvalue'
I am sure it isn't perfect, so if you do use it and come up with suggestions for improvement let me know in the comments.
Showing posts with label Sinatra. Show all posts
Showing posts with label Sinatra. Show all posts
Friday, July 24, 2015
Monday, June 29, 2015
Mock almost any web service with these 100 lines of ruby code
Many applications are now dependent upon web services to provide much of their functionality. Testing applications and their interactions with these web services can be pretty hard to automate without a mock service.
I recently needed to write some unit tests for a bit of code that communicated with a web service, and so I started reading up on Sinatra.
It turns out that Sinatra makes it very simple to generate just about any web service. It is especially trivial to mock data for a given web service end point. Take this example right out of the Sinatra README:
This seemed awesome, but I realized that I had maybe 50 endpoints that I wanted to mock. I guess this still isn't too bad, but I really wanted to generalize my ruby code in such a way that it could mock any endpoint without code modification.
So I wrote a Sinatra service that has only 4 end points using splat captures (*), which is basically a wildcard. I made one route for each of the main HTTP verbs (POST,PUT,GET,DELETE), and had each route match ANY endpoint that matched its verb, meaning:
get '*' do ...
put '*' do ...
post '*' do ...
delete '*' do ...
I then wrote each route to mirror a "fixtures" directory on the filesystem.
For example, say I had an endpoint to some API for getting user information:
GET /companies/company1/users/jdoe
The get '*' route handles this and browses the file on the filesystem at ./fixtures/companies/company1/users/jdoe.json, and returns the contents of this file in the body of the response:
get '*' do
path = Pathname.new("#{File.dirname(__FILE__)}/#{FIXTURESDIR}#{params[:splat].first()}")
If I wanted to iterate all users, I could instead request a get on a directory instead of a file:
GET /companies/company1/users
Now the get '*' route browses to the directory and iterates through all the ".json" files returning an array of JSON encoded user objects in the body.
If I want to add a new user I can simply do a POST:
POST /companies/company1/users/dsmith
with some JSON in the body, and this JSON will be written to ./fixtures/companies/company1/dsmith.json
Delete also works as you might expect, deleting the file in the fixtures directory.
All of this can be done with very little code thanks to the domain specific nature of Sinatra. The full ruby code can be found in one of my GitHub Gists's:
https://gist.github.com/jhnbwrs/6e940679a3dd1a23cbc0
Now, if you are unit testing a ruby application you can stub any requests to your api endpoint using webmock:
# spec/spec_helper.rb RSpec.configure
do |config|
config.before(:each) do
stub_request(:any, /api.company1.com/).to_rack(JSONFileServer)
end
end
If you aren't unit testing a ruby application, you should still be able to fire up this service during the unit test phase of your build, and redirect your API requests to localhost:4567.
I recently needed to write some unit tests for a bit of code that communicated with a web service, and so I started reading up on Sinatra.
It turns out that Sinatra makes it very simple to generate just about any web service. It is especially trivial to mock data for a given web service end point. Take this example right out of the Sinatra README:
get '/hello/:name' do # matches "GET /hello/foo" and "GET /hello/bar" # params['name'] is 'foo' or 'bar' "Hello #{params['name']}!" end
This seemed awesome, but I realized that I had maybe 50 endpoints that I wanted to mock. I guess this still isn't too bad, but I really wanted to generalize my ruby code in such a way that it could mock any endpoint without code modification.
So I wrote a Sinatra service that has only 4 end points using splat captures (*), which is basically a wildcard. I made one route for each of the main HTTP verbs (POST,PUT,GET,DELETE), and had each route match ANY endpoint that matched its verb, meaning:
get '*' do ...
put '*' do ...
post '*' do ...
delete '*' do ...
I then wrote each route to mirror a "fixtures" directory on the filesystem.
For example, say I had an endpoint to some API for getting user information:
GET /companies/company1/users/jdoe
The get '*' route handles this and browses the file on the filesystem at ./fixtures/companies/company1/users/jdoe.json, and returns the contents of this file in the body of the response:
get '*' do
path = Pathname.new("#{File.dirname(__FILE__)}/#{FIXTURESDIR}#{params[:splat].first()}")
pathPlusJson = Pathname.new("#{path}.json")
if path.directory?
response = get_directory_contents_array(path.to_path)
return response.to_json
elsif pathPlusJson.exist? and pathPlusJson.file?
response = JSON.parse(IO.read(pathPlusJson.to_path))
return response.to_json
else
return create_response( {"error" => "Not Found" }.to_json, 404 )
end
end
if path.directory?
response = get_directory_contents_array(path.to_path)
return response.to_json
elsif pathPlusJson.exist? and pathPlusJson.file?
response = JSON.parse(IO.read(pathPlusJson.to_path))
return response.to_json
else
return create_response( {"error" => "Not Found" }.to_json, 404 )
end
end
If I wanted to iterate all users, I could instead request a get on a directory instead of a file:
GET /companies/company1/users
Now the get '*' route browses to the directory and iterates through all the ".json" files returning an array of JSON encoded user objects in the body.
If I want to add a new user I can simply do a POST:
POST /companies/company1/users/dsmith
with some JSON in the body, and this JSON will be written to ./fixtures/companies/company1/dsmith.json
Delete also works as you might expect, deleting the file in the fixtures directory.
All of this can be done with very little code thanks to the domain specific nature of Sinatra. The full ruby code can be found in one of my GitHub Gists's:
https://gist.github.com/jhnbwrs/6e940679a3dd1a23cbc0
# spec/spec_helper.rb RSpec.configure
do |config|
config.before(:each) do
stub_request(:any, /api.company1.com/).to_rack(JSONFileServer)
end
end
If you aren't unit testing a ruby application, you should still be able to fire up this service during the unit test phase of your build, and redirect your API requests to localhost:4567.
Subscribe to:
Posts (Atom)