Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add an HTTP test client #131

Merged
merged 1 commit into from
Oct 9, 2014
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should include this similar to how webmock is required.


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!