diff --git a/airborne.gemspec b/airborne.gemspec index 1750e41..71b358f 100644 --- a/airborne.gemspec +++ b/airborne.gemspec @@ -15,5 +15,7 @@ Gem::Specification.new do |s| s.add_runtime_dependency 'rack-test', '~> 0.6', '>= 0.6.2' s.add_runtime_dependency 'rack' s.add_runtime_dependency 'activesupport' + s.add_runtime_dependency 'faraday', '~> 0.13' s.add_development_dependency 'webmock', '~> 0' + s.add_development_dependency 'pry' end diff --git a/lib/airborne.rb b/lib/airborne.rb index a8d22a0..5de2c46 100644 --- a/lib/airborne.rb +++ b/lib/airborne.rb @@ -1,27 +1,29 @@ +require 'airborne/rspec_settings' require 'airborne/optional_hash_type_expectations' require 'airborne/path_matcher' require 'airborne/request_expectations' require 'airborne/rest_client_requester' require 'airborne/rack_test_requester' +require 'airborne/faraday_requester' require 'airborne/base' -RSpec.configure do |config| - config.add_setting :base_url - config.add_setting :match_expected - config.add_setting :match_actual - config.add_setting :match_expected_default, default: true - config.add_setting :match_actual_default, default: false - config.add_setting :headers - config.add_setting :rack_app - config.add_setting :requester_type - config.add_setting :requester_module - config.before do |example| - config.match_expected = example.metadata[:match_expected].nil? ? - Airborne.configuration.match_expected_default? : example.metadata[:match_expected] - config.match_actual = example.metadata[:match_actual].nil? ? - Airborne.configuration.match_actual_default? : example.metadata[:match_actual] + +module Airborne + class << self + def configuration + RSpec.configuration + end + + def configure + RSpec.configure do |config| + yield config + end + end end +end - # Include last since it depends on the configuration already being added - config.include Airborne +Airborne.configure do |config| + config.include Airborne::Base + config.filter_run_when_matching :focus + config.order = :random end diff --git a/lib/airborne/base.rb b/lib/airborne/base.rb index 7f900eb..f119f0a 100644 --- a/lib/airborne/base.rb +++ b/lib/airborne/base.rb @@ -1,82 +1,82 @@ require 'json' require 'active_support' require 'active_support/core_ext/hash/indifferent_access' +require 'base64' module Airborne - class InvalidJsonError < StandardError; end - - include RequestExpectations - - attr_reader :response, :headers, :body - - def self.configure - RSpec.configure do |config| - yield config + module Base + class InvalidJsonError < StandardError; end + + include RequestExpectations + + attr_reader :response, :headers, :body + + def self.included(base) + if Airborne.configuration.use_faraday + base.send(:include, Airborne::FaradayRequester) + elsif Airborne.configuration.requester_module + base.send(:include, Airborne.configuration.requester_module) + elsif Airborne.configuration.rack_app + base.send(:include, RackTestRequester) + else + base.send(:include, RestClientRequester) + end end - end - def self.included(base) - if !Airborne.configuration.requester_module.nil? - base.send(:include, Airborne.configuration.requester_module) - elsif !Airborne.configuration.rack_app.nil? - base.send(:include, RackTestRequester) - else - base.send(:include, RestClientRequester) + def get(path, body = nil, headers = nil, &block) + @response = make_request(:get, path, body, headers, &block) end - end - - def self.configuration - RSpec.configuration - end - def get(url, headers = nil) - @response = make_request(:get, url, headers: headers) - end + def post(path, post_body = nil, headers = nil, &block) + @response = make_request(:post, path, post_body, headers, &block) + end - def post(url, post_body = nil, headers = nil) - @response = make_request(:post, url, body: post_body, headers: headers) - end + def patch(path, patch_body = nil, headers = nil, &block) + @response = make_request(:patch, path, patch_body, headers, &block) + end - def patch(url, patch_body = nil, headers = nil) - @response = make_request(:patch, url, body: patch_body, headers: headers) - end + def put(path, put_body = nil, headers = nil, &block) + @response = make_request(:put, path, put_body, headers, &block) + end - def put(url, put_body = nil, headers = nil) - @response = make_request(:put, url, body: put_body, headers: headers) - end + def delete(path, delete_body = nil, headers = nil, &block) + @response = make_request(:delete, path, delete_body, headers, &block) + end - def delete(url, delete_body = nil, headers = nil) - @response = make_request(:delete, url, body: delete_body, headers: headers) - end + def head(path, body = nil, headers = nil, &block) + @response = make_request(:head, path, body, headers, &block) + end - def head(url, headers = nil) - @response = make_request(:head, url, headers: headers) - end + def options(path, body = nil, headers = nil, &block) + @response = make_request(:options, path, body, headers, &block) + end - def options(url, headers = nil) - @response = make_request(:options, url, headers: headers) - end + def response + @response + end - def response - @response - end + def headers + HashWithIndifferentAccess.new(response.headers) + end - def headers - HashWithIndifferentAccess.new(response.headers) - end + def basic_auth(login, pass) + val = Base64.encode64([login, pass].join(':')).gsub!("\n", '') + "Basic #{val}" + end - def body - response.body - end + def body + response.body + end - def json_body - JSON.parse(response.body, symbolize_names: true) rescue fail InvalidJsonError, 'Api request returned invalid json' - end + def json_body + JSON.parse(response.body, symbolize_names: true) rescue fail InvalidJsonError, 'Api request returned invalid json' + end - private + private - def get_url(url) - base = Airborne.configuration.base_url || '' - base + url + def get_url(url) + base = Airborne.configuration.base_url || '' + base + url + end end end diff --git a/lib/airborne/faraday_requester.rb b/lib/airborne/faraday_requester.rb new file mode 100644 index 0000000..a3b45bc --- /dev/null +++ b/lib/airborne/faraday_requester.rb @@ -0,0 +1,35 @@ +require 'faraday' + +module Airborne + module FaradayRequester + def conn + @conn ||= Faraday.new do |c| + c.request :url_encoded + c.adapter Faraday.default_adapter + end + end + + def make_request(method_name, path, body = {}, headers ={}, &block) + conn.send(method_name, path, body) do |req| + conn.url_prefix = base_url + req.path = path + req.headers.merge!(headers) + yield req, conn if block_given? + req.headers.merge!(conn.headers) + end + end + + private + def base_url + Airborne.configuration.base_url + end + end +end + +module FaradayResponse + def code + status + end +end + +Faraday::Response.send(:include, FaradayResponse) diff --git a/lib/airborne/rack_test_requester.rb b/lib/airborne/rack_test_requester.rb index abcb652..d308a61 100644 --- a/lib/airborne/rack_test_requester.rb +++ b/lib/airborne/rack_test_requester.rb @@ -2,13 +2,11 @@ module Airborne module RackTestRequester - def make_request(method, url, options = {}) - headers = options[:headers] || {} - base_headers = Airborne.configuration.headers || {} - headers = base_headers.merge(headers) + def make_request(method, url, body = {}, headers = {}, &block) + headers = (Airborne.configuration.headers || {}).merge(headers || {}) browser = Rack::Test::Session.new(Rack::MockSession.new(Airborne.configuration.rack_app)) headers.each { |name, value| browser.header(name, value) } - browser.send(method, url, options[:body] || {}, headers) + browser.send(method, url, body || {}, headers) Rack::MockResponse.class_eval do alias_method :code, :status end diff --git a/lib/airborne/rest_client_requester.rb b/lib/airborne/rest_client_requester.rb index 3ebe68f..fb52de9 100644 --- a/lib/airborne/rest_client_requester.rb +++ b/lib/airborne/rest_client_requester.rb @@ -2,12 +2,12 @@ module Airborne module RestClientRequester - def make_request(method, url, options = {}) - headers = base_headers.merge(options[:headers] || {}) + def make_request(method, url, body, headers, &block) + headers = base_headers.merge(headers || {}) res = if method == :post || method == :patch || method == :put begin - request_body = options[:body].nil? ? '' : options[:body] - request_body = request_body.to_json if options[:body].is_a?(Hash) + request_body = body || '' + request_body = request_body.to_json if request_body.is_a?(Hash) RestClient.send(method, get_url(url), request_body, headers) rescue RestClient::Exception => e e.response diff --git a/lib/airborne/rspec_settings.rb b/lib/airborne/rspec_settings.rb new file mode 100644 index 0000000..776a6bd --- /dev/null +++ b/lib/airborne/rspec_settings.rb @@ -0,0 +1,22 @@ +RSpec.configure do |config| + config.add_setting :base_url + config.add_setting :match_expected_default, default: true + config.add_setting :match_actual_default, default: false + config.add_setting :match_expected, default: config.match_expected_default + config.add_setting :match_actual, default: config.match_actual_default + config.add_setting :headers + config.add_setting :rack_app + config.add_setting :requester_type + config.add_setting :requester_module + config.add_setting :use_faraday, default: true + config.around(:example, match_expected: !config.match_expected_default) do |example| + config.match_expected = !config.match_expected_default + example.run + config.match_expected = config.match_expected_default + end + config.around(:example, match_actual: !config.match_actual_default) do |example| + config.match_actual = !config.match_actual_default + example.run + config.match_actual = config.match_actual_default + end +end diff --git a/spec/airborne/base_spec.rb b/spec/airborne/base_spec.rb index 63d1da2..aeed130 100644 --- a/spec/airborne/base_spec.rb +++ b/spec/airborne/base_spec.rb @@ -52,4 +52,66 @@ post '/simple_post', {} expect(json_body).to_not be(nil) end + + context 'when using faraday instead of RestClient' do + before do + Airborne.configure do |c| + c.use_faraday = true + end + end + + context 'when sucessful request is made' do + + before { mock_get('simple_get') } + + it 'response should be set' do + get '/simple_get' + expect(response).to_not be(nil) + end + + it 'headers should be set' do + get '/simple_get' + expect(headers).to_not be(nil) + end + + it 'headers should be hash with indifferent access' do + mock_get('simple_get', 'Content-Type' => 'application/json') + get '/simple_get' + expect(headers).to be_kind_of(Hash) + expect(headers[:content_type]).to eq('application/json') + expect(headers['content_type']).to eq('application/json') + end + + it 'body should be set' do + get '/simple_get' + expect(body).to_not be(nil) + end + + it 'json body should be symbolized hash' do + get '/simple_get' + expect(json_body).to be_kind_of(Hash) + expect(json_body.first[0]).to be_kind_of(Symbol) + end + end + + it 'should throw an InvalidJsonError when accessing json_body on invalid json' do + mock_get('invalid_json') + get '/invalid_json' + expect(body).to eq('invalid1234') + expect { json_body }.to raise_error(InvalidJsonError) + end + + it 'should handle a 500 error on get' do + mock_get('simple_get', {}, [500, 'Internal Server Error']) + get '/simple_get' + expect(json_body).to_not be(nil) + end + + it 'should handle a 500 error on post' do + mock_post('simple_post', {}, [500, 'Internal Server Error']) + post '/simple_post', {} + expect(json_body).to_not be(nil) + end + + end end diff --git a/spec/airborne/client_requester_spec.rb b/spec/airborne/client_requester_spec.rb index 3023801..d1dc616 100644 --- a/spec/airborne/client_requester_spec.rb +++ b/spec/airborne/client_requester_spec.rb @@ -3,11 +3,9 @@ describe 'client requester' do before do allow(RestClient).to receive(:send) - RSpec::Mocks.space.proxy_for(self).remove_stub_if_present(:get) end after do - allow(RestClient).to receive(:send).and_call_original Airborne.configure { |config| config.headers = {} } end @@ -19,7 +17,7 @@ end it 'should override headers with option[:headers]' do - get '/foo', { content_type: 'application/x-www-form-urlencoded' } + get '/foo', {} ,{ content_type: 'application/x-www-form-urlencoded' } expect(RestClient).to have_received(:send) .with(:get, 'http://www.example.com/foo', { content_type: 'application/x-www-form-urlencoded' }) diff --git a/spec/airborne/expectations/expect_header_contains_spec.rb b/spec/airborne/expectations/expect_header_contains_spec.rb index 32d61aa..f65c8c6 100644 --- a/spec/airborne/expectations/expect_header_contains_spec.rb +++ b/spec/airborne/expectations/expect_header_contains_spec.rb @@ -1,20 +1,22 @@ require 'spec_helper' describe 'expect header contains' do - it 'should ensure partial header match exists' do + before do mock_get('simple_get', 'Content-Type' => 'application/json') + end + + it 'should ensure partial header match exists' do get '/simple_get' expect_header_contains(:content_type, 'json') end it 'should ensure header is present' do - mock_get('simple_get', 'Content-Type' => 'application/json') get '/simple_get' expect { expect_header_contains(:foo, 'bar') }.to raise_error(ExpectationNotMetError) end + it 'should ensure partial header is present' do - mock_get('simple_get', 'Content-Type' => 'application/json') get '/simple_get' expect { expect_header_contains(:content_type, 'bar') }.to raise_error(ExpectationNotMetError) end diff --git a/spec/airborne/expectations/expect_json_regex_spec.rb b/spec/airborne/expectations/expect_json_regex_spec.rb index e66905b..a28e502 100644 --- a/spec/airborne/expectations/expect_json_regex_spec.rb +++ b/spec/airborne/expectations/expect_json_regex_spec.rb @@ -4,30 +4,30 @@ it 'should test against regex' do mock_get('simple_get') get '/simple_get' - expect_json(name: regex('^A')) + expect_json(name: /^A/) end it 'should raise an error if regex does not match' do mock_get('simple_get') get '/simple_get' - expect { expect_json(name: regex('^B')) }.to raise_error(ExpectationNotMetError) + expect { expect_json(name: /^B/) }.to raise_error(ExpectationNotMetError) end it 'should allow regex(Regexp) to be tested against a path' do mock_get('simple_nested_path') get '/simple_nested_path' - expect_json('address.city', regex('^R')) + expect_json('address.city', /^R/) end it 'should allow testing regex against numbers directly' do mock_get('simple_nested_path') get '/simple_nested_path' - expect_json('address.coordinates.lattitude', regex('^3')) + expect_json('address.coordinates.lattitude', /^3/) end it 'should allow testing regex against numbers in the hash' do mock_get('simple_nested_path') get '/simple_nested_path' - expect_json('address.coordinates', lattitude: regex('^3')) + expect_json('address.coordinates', lattitude: /^3/) end end diff --git a/spec/airborne/expectations/expect_status_spec.rb b/spec/airborne/expectations/expect_status_spec.rb index d7817f5..9872b31 100644 --- a/spec/airborne/expectations/expect_status_spec.rb +++ b/spec/airborne/expectations/expect_status_spec.rb @@ -1,12 +1,6 @@ require 'spec_helper' describe 'expect_status' do - it 'should verify correct status code' do - mock_get('simple_get') - get '/simple_get' - expect_status 200 - end - it 'should fail when incorrect status code is returned' do mock_get('simple_get') get '/simple_get' diff --git a/spec/airborne/faraday_requester_spec.rb b/spec/airborne/faraday_requester_spec.rb new file mode 100644 index 0000000..39023c0 --- /dev/null +++ b/spec/airborne/faraday_requester_spec.rb @@ -0,0 +1,109 @@ +require 'spec_helper' + +RSpec.describe Airborne::FaradayRequester do + before do + stub_request(:any, /www.example.com/) + allow(Airborne).to receive_message_chain(:configuration, :base_url).and_return 'https://www.example.com' + end + + class Test + include Airborne::FaradayRequester + end + + subject { Test.new } + + shared_examples 'faraday_get_delete_requests' do |kind_of_request| + describe "##{kind_of_request}" do + context 'when have only path' do + it "makes a simple #{kind_of_request} call" do + subject.make_request kind_of_request, '/asdasd' + expect(a_request(kind_of_request, "https://www.example.com/asdasd").with(body: {})).to have_been_made.once + end + end + + context 'when request has body' do + it 'makes calls with body specified in the body params' do + subject.make_request kind_of_request, '/asdasd', { jane: 'doe' } + expect(a_request(kind_of_request, "https://www.example.com/asdasd?jane=doe")).to have_been_made.once + end + end + + context 'when request also has headers' do + it 'makes calls with headers specified' do + subject.make_request kind_of_request, '/asdasd', { }, { a: '1' } + expect(a_request(kind_of_request, "https://www.example.com/asdasd").with({headers: {'A' => '1'}})).to have_been_made.once + end + end + + context 'when request has body and headers' do + it 'makes calls with body specified in the body params' do + subject.make_request kind_of_request, '/asdasd', { jane: 'doe' }, { a: '1' } + expect(a_request(kind_of_request, "https://www.example.com/asdasd?jane=doe").with({headers: {'A' => '1'}})).to have_been_made.once + end + end + + context 'when request has a different URL than the URL specified' do + before { stub_request(:any, /www.example.de/) } + + it 'makes calls with body specified in the body params' do + + subject.make_request kind_of_request, '/asdasd', { jane: 'doe' } do |k, c| + c.url_prefix = 'http://www.example.de' + c.basic_auth 'asdsa', 'asdsa' + k.headers.merge!({ a: '1' }) + end + expect(a_request(kind_of_request, "www.example.de/asdasd?jane=doe").with({headers: {'A' => '1', 'Authorization' => "Basic YXNkc2E6YXNkc2E="}})).to have_been_made.once + end + end + end + end + + shared_examples 'faraday_put_post_requests' do |kind_of_request| + describe "##{kind_of_request}" do + context 'when have only path' do + it "makes a simple #{kind_of_request} call" do + subject.make_request kind_of_request, '/asdasd' + expect(a_request(kind_of_request, "https://www.example.com/asdasd").with(body: {})).to have_been_made.once + end + end + + context 'when request has body' do + it 'makes calls with body specified in the body params' do + subject.make_request kind_of_request, '/asdasd', { jane: 'doe' } + expect(a_request(kind_of_request, "https://www.example.com/asdasd").with(body: 'jane=doe')).to have_been_made.once + end + end + + context 'when request also has headers' do + it 'makes calls with headers specified' do + subject.make_request kind_of_request, '/asdasd', { }, { a: '1' } + expect(a_request(kind_of_request, "https://www.example.com/asdasd").with({headers: {'A' => '1'}})).to have_been_made.once + end + end + + context 'when request has body and headers' do + it 'makes calls with body specified in the body params' do + subject.make_request kind_of_request, '/asdasd', { jane: 'doe' }, { a: '1' } + expect(a_request(kind_of_request, "https://www.example.com/asdasd").with({headers: {'A' => '1'}, body: 'jane=doe'})).to have_been_made.once + end + end + + context 'when request has a different URL than the URL specified' do + before { stub_request(:any, /www.example.de/) } + + it 'makes calls with body specified in the body params' do + subject.make_request kind_of_request, '/asdasd', { jane: 'doe' } do |k, c| + c.url_prefix = 'http://www.example.de' + k.headers.merge!({ a: '1' }) + end + expect(a_request(kind_of_request, "www.example.de/asdasd").with({headers: {'A' => '1'}, body: 'jane=doe'})).to have_been_made.once + end + end + end + end + + it_behaves_like 'faraday_get_delete_requests', :get + it_behaves_like 'faraday_get_delete_requests', :delete + it_behaves_like 'faraday_put_post_requests', :put + it_behaves_like 'faraday_put_post_requests', :post +end diff --git a/spec/airborne/post_spec.rb b/spec/airborne/post_spec.rb index 22bce3c..1c41240 100644 --- a/spec/airborne/post_spec.rb +++ b/spec/airborne/post_spec.rb @@ -11,7 +11,16 @@ it 'should allow testing on post requests' do url = 'http://www.example.com/simple_post' stub_request(:post, url) - post '/simple_post', 'hello', content_type: 'text/plain' - expect(WebMock).to have_requested(:post, url).with(body: 'hello', headers: { 'Content-Type' => 'text/plain' }) + post '/simple_post', 'hello', content_type: 'text/plain', 'Authorization' => basic_auth('shreyas', 'agarwal') + expect(WebMock).to have_requested(:post, url).with(body: 'hello', headers: { 'Content-Type' => 'text/plain', 'Authorization' => 'Basic c2hyZXlhczphZ2Fyd2Fs' }) + end + + context 'when using Faraday', :faraday do + it 'makes a post request to specfied URL with body and headers' do + url = 'http://www.example.com/simple_post' + stub_request(:post, url) + post '/simple_post', 'hello', content_type: 'text/plain', 'Authorization' => basic_auth('shreyasagarwaltesting', 'apiapplicationswithauthorisation') + expect(WebMock).to have_requested(:post, url).with(body: 'hello', headers: { 'Content-Type' => 'text/plain', 'Authorization' => 'Basic c2hyZXlhc2FnYXJ3YWx0ZXN0aW5nOmFwaWFwcGxpY2F0aW9uc3dpdGhhdXRob3Jpc2F0aW9u' }) + end end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 0801c56..44124ab 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -2,13 +2,15 @@ Coveralls.wear! require 'airborne' require 'stub_helper' +require 'pry' Airborne.configure do |config| config.base_url = 'http://www.example.com' config.include StubHelper + config.use_faraday = false end ExpectationNotMetError = RSpec::Expectations::ExpectationNotMetError ExpectationError = Airborne::ExpectationError -InvalidJsonError = Airborne::InvalidJsonError +InvalidJsonError = Airborne::Base::InvalidJsonError PathError = Airborne::PathError