Skip to content

Commit c5f83ee

Browse files
author
David Varvel
committed
Adds an HTTP test client
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/'}) } ```
1 parent d5ebfa2 commit c5f83ee

File tree

6 files changed

+318
-0
lines changed

6 files changed

+318
-0
lines changed

Gemfile.lock

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ PATH
33
specs:
44
rspec_api_documentation (3.0.0)
55
activesupport (>= 3.0.0)
6+
faraday (>= 0.9.0)
67
i18n (>= 0.1.0)
78
json (>= 1.4.6)
89
mustache (>= 0.99.4)
@@ -44,6 +45,8 @@ GEM
4445
multi_test (>= 0.0.2)
4546
diff-lcs (1.2.5)
4647
fakefs (0.4.3)
48+
faraday (0.9.0)
49+
multipart-post (>= 1.2, < 3)
4750
ffi (1.9.3)
4851
gherkin (2.12.2)
4952
multi_json (~> 1.3)
@@ -61,6 +64,7 @@ GEM
6164
minitest (4.7.5)
6265
multi_json (1.8.2)
6366
multi_test (0.0.2)
67+
multipart-post (2.0.0)
6468
mustache (0.99.5)
6569
nokogiri (1.6.0)
6670
mini_portile (~> 0.5.0)

lib/rspec_api_documentation.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ module RspecApiDocumentation
1818
autoload :Index
1919
autoload :ClientBase
2020
autoload :Headers
21+
autoload :HttpTestClient
2122
end
2223

2324
autoload :DSL
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
require 'faraday'
2+
3+
class RequestSaver < Faraday::Middleware
4+
def self.last_request
5+
@@last_request
6+
end
7+
8+
def self.last_request=(request_env)
9+
@@last_request = request_env
10+
end
11+
12+
def self.last_response
13+
@@last_response
14+
end
15+
16+
def self.last_response=(response_env)
17+
@@last_response = response_env
18+
end
19+
20+
def call(env)
21+
RequestSaver.last_request = env
22+
23+
@app.call(env).on_complete do |env|
24+
RequestSaver.last_response = env
25+
end
26+
end
27+
end
28+
29+
Faraday::Request.register_middleware :request_saver => lambda { RequestSaver }
30+
31+
module RspecApiDocumentation
32+
class HttpTestClient < ClientBase
33+
34+
def request_headers
35+
env_to_headers(last_request.request_headers)
36+
end
37+
38+
def response_headers
39+
last_response.response_headers
40+
end
41+
42+
def query_string
43+
last_request.url.query
44+
end
45+
46+
def status
47+
last_response.status
48+
end
49+
50+
def response_body
51+
last_response.body
52+
end
53+
54+
def request_content_type
55+
last_request.request_headers["CONTENT_TYPE"]
56+
end
57+
58+
def response_content_type
59+
last_response.request_headers["CONTENT_TYPE"]
60+
end
61+
62+
def do_request(method, path, params, request_headers)
63+
http_test_session.send(method, path, params, headers(method, path, params, request_headers))
64+
end
65+
66+
protected
67+
68+
def query_hash(query_string)
69+
Faraday::Utils.parse_query(query_string)
70+
end
71+
72+
def headers(*args)
73+
headers_to_env(super)
74+
end
75+
76+
def handle_multipart_body(request_headers, request_body)
77+
parsed_parameters = Rack::Request.new({
78+
"CONTENT_TYPE" => request_headers["Content-Type"],
79+
"rack.input" => StringIO.new(request_body)
80+
}).params
81+
82+
clean_out_uploaded_data(parsed_parameters,request_body)
83+
end
84+
85+
def document_example(method, path)
86+
return unless metadata[:document]
87+
88+
req_method = last_request.method
89+
if req_method == :post || req_method == :put
90+
request_body =last_request.body
91+
else
92+
request_body = ""
93+
end
94+
95+
request_metadata = {}
96+
request_body = "" if request_body == "null" || request_body == "\"\""
97+
98+
if request_content_type =~ /multipart\/form-data/ && respond_to?(:handle_multipart_body, true)
99+
request_body = handle_multipart_body(request_headers, request_body)
100+
end
101+
102+
request_metadata[:request_method] = method
103+
request_metadata[:request_path] = path
104+
request_metadata[:request_body] = request_body.empty? ? nil : request_body
105+
request_metadata[:request_headers] = last_request.request_headers
106+
request_metadata[:request_query_parameters] = query_hash(query_string)
107+
request_metadata[:request_content_type] = request_content_type
108+
request_metadata[:response_status] = status
109+
request_metadata[:response_status_text] = Rack::Utils::HTTP_STATUS_CODES[status]
110+
request_metadata[:response_body] = response_body.empty? ? nil : response_body
111+
request_metadata[:response_headers] = response_headers
112+
request_metadata[:response_content_type] = response_content_type
113+
request_metadata[:curl] = Curl.new(method, path, request_body, request_headers)
114+
115+
metadata[:requests] ||= []
116+
metadata[:requests] << request_metadata
117+
end
118+
119+
private
120+
121+
def clean_out_uploaded_data(params,request_body)
122+
params.each do |_, value|
123+
if value.is_a?(Hash)
124+
if value.has_key?(:tempfile)
125+
data = value[:tempfile].read
126+
request_body = request_body.gsub(data, "[uploaded data]")
127+
else
128+
request_body = clean_out_uploaded_data(value,request_body)
129+
end
130+
end
131+
end
132+
request_body
133+
end
134+
135+
136+
def http_test_session
137+
::Faraday.new(:url => options[:host]) do |faraday|
138+
faraday.request :request_saver # save the request and response
139+
faraday.request :url_encoded # form-encode POST params
140+
faraday.response :logger # log requests to STDOUT
141+
faraday.adapter Faraday.default_adapter # make requests with Net::HTTP
142+
end
143+
end
144+
145+
def last_request
146+
RequestSaver.last_request
147+
end
148+
149+
def last_response
150+
RequestSaver.last_response
151+
end
152+
end
153+
end

rspec_api_documentation.gemspec

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ Gem::Specification.new do |s|
1919
s.add_runtime_dependency "i18n", ">= 0.1.0"
2020
s.add_runtime_dependency "mustache", ">= 0.99.4"
2121
s.add_runtime_dependency "json", ">= 1.4.6"
22+
s.add_runtime_dependency "faraday", ">= 0.9.0"
2223

2324
s.add_development_dependency "fakefs"
2425
s.add_development_dependency "sinatra"

spec/http_test_client_spec.rb

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
require 'spec_helper'
2+
require 'rack/test'
3+
4+
describe RspecApiDocumentation::HttpTestClient do
5+
before(:all) do
6+
WebMock.allow_net_connect!
7+
8+
$external_test_app_pid = spawn("ruby ./spec/support/external_test_app.rb")
9+
Process.detach $external_test_app_pid
10+
sleep 3 #Wait until the test app is up
11+
end
12+
13+
after(:all) do
14+
WebMock.disable_net_connect!
15+
16+
Process.kill('TERM', $external_test_app_pid)
17+
end
18+
19+
let(:client_context) { double(example: example, app_root: 'nowhere') }
20+
let(:target_host) { 'http://localhost:4567' }
21+
let(:test_client) { RspecApiDocumentation::HttpTestClient.new(client_context, {host: target_host}) }
22+
23+
subject { test_client }
24+
25+
it { should be_a(RspecApiDocumentation::HttpTestClient) }
26+
27+
its(:context) { should equal(client_context) }
28+
its(:example) { should equal(example) }
29+
its(:metadata) { should equal(example.metadata) }
30+
31+
describe "xml data", :document => true do
32+
before do
33+
test_client.get "/xml"
34+
end
35+
36+
it "should handle xml data" do
37+
test_client.response_headers["Content-Type"].should =~ /application\/xml/
38+
end
39+
40+
it "should log the request" do
41+
example.metadata[:requests].first[:response_body].should be_present
42+
end
43+
end
44+
45+
describe "#query_string" do
46+
before do
47+
test_client.get "/?query_string=true"
48+
end
49+
50+
it 'should contain the query_string' do
51+
test_client.query_string.should == "query_string=true"
52+
end
53+
end
54+
55+
describe "#request_headers" do
56+
before do
57+
test_client.get "/", {}, { "Accept" => "application/json", "Content-Type" => "application/json" }
58+
end
59+
60+
it "should contain all the headers" do
61+
test_client.request_headers.should eq({
62+
"Accept" => "application/json",
63+
"Content-Type" => "application/json"
64+
})
65+
end
66+
end
67+
68+
context "when doing request without parameter value" do
69+
before do
70+
test_client.post "/greet?query=&other=exists"
71+
end
72+
73+
context "when examples should be documented", :document => true do
74+
it "should still argument the metadata" do
75+
metadata = example.metadata[:requests].first
76+
metadata[:request_query_parameters].should == {'query' => "", 'other' => 'exists'}
77+
end
78+
end
79+
end
80+
81+
context "after a request is made" do
82+
before do
83+
test_client.post "/greet?query=test+query", post_data, headers
84+
end
85+
86+
let(:post_data) { { :target => "nurse" }.to_json }
87+
let(:headers) { { "Content-Type" => "application/json;charset=utf-8", "X-Custom-Header" => "custom header value" } }
88+
89+
context "when examples should be documented", :document => true do
90+
it "should augment the metadata with information about the request" do
91+
metadata = example.metadata[:requests].first
92+
metadata[:request_method].should eq("POST")
93+
metadata[:request_path].should eq("/greet?query=test+query")
94+
metadata[:request_body].should be_present
95+
metadata[:request_headers].should include({'CONTENT_TYPE' => 'application/json;charset=utf-8'})
96+
metadata[:request_headers].should include({'HTTP_X_CUSTOM_HEADER' => 'custom header value'})
97+
metadata[:request_query_parameters].should == {"query" => "test query"}
98+
metadata[:request_content_type].should match(/application\/json/)
99+
metadata[:response_status].should eq(200)
100+
metadata[:response_body].should be_present
101+
metadata[:response_headers]['Content-Type'].should match(/application\/json/)
102+
metadata[:response_headers]['Content-Length'].should == '18'
103+
metadata[:response_content_type].should match(/application\/json/)
104+
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"}))
105+
end
106+
107+
context "when post data is not json" do
108+
let(:post_data) { { :target => "nurse", :email => "[email protected]" } }
109+
110+
it "should not nil out request_body" do
111+
body = example.metadata[:requests].first[:request_body]
112+
body.should =~ /target=nurse/
113+
body.should =~ /email=email%40example\.com/
114+
end
115+
end
116+
117+
context "when post data is nil" do
118+
let(:post_data) { }
119+
120+
it "should nil out request_body" do
121+
example.metadata[:requests].first[:request_body].should be_nil
122+
end
123+
end
124+
end
125+
end
126+
end

spec/support/external_test_app.rb

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
require 'sinatra/base'
2+
require 'json'
3+
4+
class StubApp < Sinatra::Base
5+
set :logging, false
6+
7+
get "/" do
8+
content_type :json
9+
10+
{ :hello => "world" }.to_json
11+
end
12+
13+
post "/greet" do
14+
content_type :json
15+
16+
request.body.rewind
17+
begin
18+
data = JSON.parse request.body.read
19+
rescue JSON::ParserError
20+
request.body.rewind
21+
data = request.body.read
22+
end
23+
data.to_json
24+
end
25+
26+
get "/xml" do
27+
content_type :xml
28+
29+
"<hello>World</hello>"
30+
end
31+
end
32+
33+
StubApp.run!

0 commit comments

Comments
 (0)