From c5f83ee201e12df7550bb49604ce1e6f1a3ed0ed Mon Sep 17 00:00:00 2001 From: David Varvel Date: Tue, 20 May 2014 15:11:22 -0700 Subject: [PATCH] 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/'}) } ``` --- Gemfile.lock | 4 + lib/rspec_api_documentation.rb | 1 + .../http_test_client.rb | 153 ++++++++++++++++++ rspec_api_documentation.gemspec | 1 + spec/http_test_client_spec.rb | 126 +++++++++++++++ spec/support/external_test_app.rb | 33 ++++ 6 files changed, 318 insertions(+) create mode 100644 lib/rspec_api_documentation/http_test_client.rb create mode 100644 spec/http_test_client_spec.rb create mode 100644 spec/support/external_test_app.rb diff --git a/Gemfile.lock b/Gemfile.lock index ff1e82fd..ade3c2eb 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -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) @@ -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) @@ -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) diff --git a/lib/rspec_api_documentation.rb b/lib/rspec_api_documentation.rb index e51b752f..8f8972d9 100644 --- a/lib/rspec_api_documentation.rb +++ b/lib/rspec_api_documentation.rb @@ -18,6 +18,7 @@ module RspecApiDocumentation autoload :Index autoload :ClientBase autoload :Headers + autoload :HttpTestClient end autoload :DSL diff --git a/lib/rspec_api_documentation/http_test_client.rb b/lib/rspec_api_documentation/http_test_client.rb new file mode 100644 index 00000000..8aad0bfa --- /dev/null +++ b/lib/rspec_api_documentation/http_test_client.rb @@ -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 diff --git a/rspec_api_documentation.gemspec b/rspec_api_documentation.gemspec index 2187c4e2..f4be1f47 100644 --- a/rspec_api_documentation.gemspec +++ b/rspec_api_documentation.gemspec @@ -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" diff --git a/spec/http_test_client_spec.rb b/spec/http_test_client_spec.rb new file mode 100644 index 00000000..8cb79faf --- /dev/null +++ b/spec/http_test_client_spec.rb @@ -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@example.com" } } + + 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 diff --git a/spec/support/external_test_app.rb b/spec/support/external_test_app.rb new file mode 100644 index 00000000..1c862a48 --- /dev/null +++ b/spec/support/external_test_app.rb @@ -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 + + "World" + end +end + +StubApp.run!