Skip to content

Commit

Permalink
Adds an HTTP test client
Browse files Browse the repository at this point in the history
This commit adds RspecApiDocumentation::HttpTestClient, which can be used to
test *any* website, either on your local development box or out on the
internet somewhere.

To use, just add the following line to the top of your spec:

```
let(:client) { RspecApiDocumentation::HttpTestClient.new(self, {host: 'http://base.url.of.the.site.you.want.to.test.com/'}) }
```
  • Loading branch information
David Varvel committed May 20, 2014
1 parent d5ebfa2 commit c5f83ee
Show file tree
Hide file tree
Showing 6 changed files with 318 additions and 0 deletions.
4 changes: 4 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ PATH
specs:
rspec_api_documentation (3.0.0)
activesupport (>= 3.0.0)
faraday (>= 0.9.0)
i18n (>= 0.1.0)
json (>= 1.4.6)
mustache (>= 0.99.4)
Expand Down Expand Up @@ -44,6 +45,8 @@ GEM
multi_test (>= 0.0.2)
diff-lcs (1.2.5)
fakefs (0.4.3)
faraday (0.9.0)
multipart-post (>= 1.2, < 3)
ffi (1.9.3)
gherkin (2.12.2)
multi_json (~> 1.3)
Expand All @@ -61,6 +64,7 @@ GEM
minitest (4.7.5)
multi_json (1.8.2)
multi_test (0.0.2)
multipart-post (2.0.0)
mustache (0.99.5)
nokogiri (1.6.0)
mini_portile (~> 0.5.0)
Expand Down
1 change: 1 addition & 0 deletions lib/rspec_api_documentation.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ module RspecApiDocumentation
autoload :Index
autoload :ClientBase
autoload :Headers
autoload :HttpTestClient
end

autoload :DSL
Expand Down
153 changes: 153 additions & 0 deletions lib/rspec_api_documentation/http_test_client.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
require 'faraday'

class RequestSaver < Faraday::Middleware
def self.last_request
@@last_request
end

def self.last_request=(request_env)
@@last_request = request_env
end

def self.last_response
@@last_response
end

def self.last_response=(response_env)
@@last_response = response_env
end

def call(env)
RequestSaver.last_request = env

@app.call(env).on_complete do |env|
RequestSaver.last_response = env
end
end
end

Faraday::Request.register_middleware :request_saver => lambda { RequestSaver }

module RspecApiDocumentation
class HttpTestClient < ClientBase

def request_headers
env_to_headers(last_request.request_headers)
end

def response_headers
last_response.response_headers
end

def query_string
last_request.url.query
end

def status
last_response.status
end

def response_body
last_response.body
end

def request_content_type
last_request.request_headers["CONTENT_TYPE"]
end

def response_content_type
last_response.request_headers["CONTENT_TYPE"]
end

def do_request(method, path, params, request_headers)
http_test_session.send(method, path, params, headers(method, path, params, request_headers))
end

protected

def query_hash(query_string)
Faraday::Utils.parse_query(query_string)
end

def headers(*args)
headers_to_env(super)
end

def handle_multipart_body(request_headers, request_body)
parsed_parameters = Rack::Request.new({
"CONTENT_TYPE" => request_headers["Content-Type"],
"rack.input" => StringIO.new(request_body)
}).params

clean_out_uploaded_data(parsed_parameters,request_body)
end

def document_example(method, path)
return unless metadata[:document]

req_method = last_request.method
if req_method == :post || req_method == :put
request_body =last_request.body
else
request_body = ""
end

request_metadata = {}
request_body = "" if request_body == "null" || request_body == "\"\""

if request_content_type =~ /multipart\/form-data/ && respond_to?(:handle_multipart_body, true)
request_body = handle_multipart_body(request_headers, request_body)
end

request_metadata[:request_method] = method
request_metadata[:request_path] = path
request_metadata[:request_body] = request_body.empty? ? nil : request_body
request_metadata[:request_headers] = last_request.request_headers
request_metadata[:request_query_parameters] = query_hash(query_string)
request_metadata[:request_content_type] = request_content_type
request_metadata[:response_status] = status
request_metadata[:response_status_text] = Rack::Utils::HTTP_STATUS_CODES[status]
request_metadata[:response_body] = response_body.empty? ? nil : response_body
request_metadata[:response_headers] = response_headers
request_metadata[:response_content_type] = response_content_type
request_metadata[:curl] = Curl.new(method, path, request_body, request_headers)

metadata[:requests] ||= []
metadata[:requests] << request_metadata
end

private

def clean_out_uploaded_data(params,request_body)
params.each do |_, value|
if value.is_a?(Hash)
if value.has_key?(:tempfile)
data = value[:tempfile].read
request_body = request_body.gsub(data, "[uploaded data]")
else
request_body = clean_out_uploaded_data(value,request_body)
end
end
end
request_body
end


def http_test_session
::Faraday.new(:url => options[:host]) do |faraday|
faraday.request :request_saver # save the request and response
faraday.request :url_encoded # form-encode POST params
faraday.response :logger # log requests to STDOUT
faraday.adapter Faraday.default_adapter # make requests with Net::HTTP
end
end

def last_request
RequestSaver.last_request
end

def last_response
RequestSaver.last_response
end
end
end
1 change: 1 addition & 0 deletions rspec_api_documentation.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ Gem::Specification.new do |s|
s.add_runtime_dependency "i18n", ">= 0.1.0"
s.add_runtime_dependency "mustache", ">= 0.99.4"
s.add_runtime_dependency "json", ">= 1.4.6"
s.add_runtime_dependency "faraday", ">= 0.9.0"

s.add_development_dependency "fakefs"
s.add_development_dependency "sinatra"
Expand Down
126 changes: 126 additions & 0 deletions spec/http_test_client_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
require 'spec_helper'
require 'rack/test'

describe RspecApiDocumentation::HttpTestClient do
before(:all) do
WebMock.allow_net_connect!

$external_test_app_pid = spawn("ruby ./spec/support/external_test_app.rb")
Process.detach $external_test_app_pid
sleep 3 #Wait until the test app is up
end

after(:all) do
WebMock.disable_net_connect!

Process.kill('TERM', $external_test_app_pid)
end

let(:client_context) { double(example: example, app_root: 'nowhere') }
let(:target_host) { 'http://localhost:4567' }
let(:test_client) { RspecApiDocumentation::HttpTestClient.new(client_context, {host: target_host}) }

subject { test_client }

it { should be_a(RspecApiDocumentation::HttpTestClient) }

its(:context) { should equal(client_context) }
its(:example) { should equal(example) }
its(:metadata) { should equal(example.metadata) }

describe "xml data", :document => true do
before do
test_client.get "/xml"
end

it "should handle xml data" do
test_client.response_headers["Content-Type"].should =~ /application\/xml/
end

it "should log the request" do
example.metadata[:requests].first[:response_body].should be_present
end
end

describe "#query_string" do
before do
test_client.get "/?query_string=true"
end

it 'should contain the query_string' do
test_client.query_string.should == "query_string=true"
end
end

describe "#request_headers" do
before do
test_client.get "/", {}, { "Accept" => "application/json", "Content-Type" => "application/json" }
end

it "should contain all the headers" do
test_client.request_headers.should eq({
"Accept" => "application/json",
"Content-Type" => "application/json"
})
end
end

context "when doing request without parameter value" do
before do
test_client.post "/greet?query=&other=exists"
end

context "when examples should be documented", :document => true do
it "should still argument the metadata" do
metadata = example.metadata[:requests].first
metadata[:request_query_parameters].should == {'query' => "", 'other' => 'exists'}
end
end
end

context "after a request is made" do
before do
test_client.post "/greet?query=test+query", post_data, headers
end

let(:post_data) { { :target => "nurse" }.to_json }
let(:headers) { { "Content-Type" => "application/json;charset=utf-8", "X-Custom-Header" => "custom header value" } }

context "when examples should be documented", :document => true do
it "should augment the metadata with information about the request" do
metadata = example.metadata[:requests].first
metadata[:request_method].should eq("POST")
metadata[:request_path].should eq("/greet?query=test+query")
metadata[:request_body].should be_present
metadata[:request_headers].should include({'CONTENT_TYPE' => 'application/json;charset=utf-8'})
metadata[:request_headers].should include({'HTTP_X_CUSTOM_HEADER' => 'custom header value'})
metadata[:request_query_parameters].should == {"query" => "test query"}
metadata[:request_content_type].should match(/application\/json/)
metadata[:response_status].should eq(200)
metadata[:response_body].should be_present
metadata[:response_headers]['Content-Type'].should match(/application\/json/)
metadata[:response_headers]['Content-Length'].should == '18'
metadata[:response_content_type].should match(/application\/json/)
metadata[:curl].should eq(RspecApiDocumentation::Curl.new("POST", "/greet?query=test+query", post_data, {"Content-Type" => "application/json;charset=utf-8", "X-Custom-Header" => "custom header value"}))
end

context "when post data is not json" do
let(:post_data) { { :target => "nurse", :email => "[email protected]" } }

it "should not nil out request_body" do
body = example.metadata[:requests].first[:request_body]
body.should =~ /target=nurse/
body.should =~ /email=email%40example\.com/
end
end

context "when post data is nil" do
let(:post_data) { }

it "should nil out request_body" do
example.metadata[:requests].first[:request_body].should be_nil
end
end
end
end
end
33 changes: 33 additions & 0 deletions spec/support/external_test_app.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
require 'sinatra/base'
require 'json'

class StubApp < Sinatra::Base
set :logging, false

get "/" do
content_type :json

{ :hello => "world" }.to_json
end

post "/greet" do
content_type :json

request.body.rewind
begin
data = JSON.parse request.body.read
rescue JSON::ParserError
request.body.rewind
data = request.body.read
end
data.to_json
end

get "/xml" do
content_type :xml

"<hello>World</hello>"
end
end

StubApp.run!

0 comments on commit c5f83ee

Please sign in to comment.