describe QuizApiClient::HttpClient do let(:uri) { 'http://api.quiz.docker' } let(:jwt) { JWT.encode({ jwt: 'payload' }, 'secret') } let(:default_request_data) { client.send(:default_request_data) } let(:config) { QuizApiClient::Config.new { |c| c.consumer_request_id = 'hi' } } subject(:client) { QuizApiClient::HttpClient.new(uri: uri, jwt: jwt, config: config) } def url_for_path(path) "http://api.quiz.docker#{path}" end def stub_quiz_api(path, item: 1, query: {}, headers: {}, status: 200) stub_request(:get, url_for_path(path)) .with(query: query) .to_return( body: [item].to_json, status: status, headers: headers.merge( 'Content-Type' => 'application/json' ) ) end def link_header(path, page, last_page) link_header = "; rel=\"last\"" link_header += ", ; rel=\"next\"" if page < last_page link_header end def mock_time(duration) mock = Time.now allow(Time).to receive(:now).and_return(mock, mock + duration) mock end def mock_metrics(mock_time) mock = instance_double(QuizApiClient::HttpRequest::Metrics) expect(mock).to receive(:increment) expect(mock).to receive(:duration).with(mock_time, mock_time + 10) mock end def expect_metrics_calls(config, method, url, code) expect(QuizApiClient::HttpRequest::Metrics).to receive(:new) .with(config, method, url, code) .and_return(mock_metrics(mock_time(10))) end def expect_raise_error_call(config, method, url, response, current_error) mock_failure = instance_double(QuizApiClient::HttpRequest::Failure) expect(QuizApiClient::HttpRequest::Failure).to receive(:new).with(config).and_return(mock_failure) expect(mock_failure).to( receive(:raise_error) .with(method, url, response: response, current_error: current_error) .and_raise(QuizApiClient::HttpClient::RequestFailed.new(:context)) ) end it { is_expected.to be_a HTTParty } describe 'get' do it 'makes a get request' do path = '/api/quizzes' stub_quiz_api path, query: { sort: 'alpha' } expect_metrics_calls(client.config, :get, url_for_path(path), 200) response = client.get(path, all: false, query: { sort: 'alpha' }) expect(response.parsed_response).to eq [1] end describe 'all' do it 'retrieves single page when all is true' do stub_quiz_api '/api/quizzes' expect(client.get('/api/quizzes', all: true)).to eq [1] end context 'link pagination' do it 'retrieves subsequent pages when all is true' do path = '/api/quizzes' stub_quiz_api path, headers: { link: link_header(path, 1, 2) } stub_quiz_api path, item: 2, query: { page: 2 }, headers: { link: link_header(path, 2, 2) } expect(client.get('/api/quizzes', query: { page: 2 }, all: true)).to eq [1, 2] end it 'sanitizies the query param if page is nil' do stub_quiz_api '/api/quizzes' expect(client.get('/api/quizzes', query: { page: nil }, all: true)).to eq [1] end it 'sanitizies the query param if per_page is nil' do stub_quiz_api '/api/quizzes' expect(client.get('/api/quizzes', query: { per_page: nil }, all: true)).to eq [1] end end context 'dynamo pagination' do let(:dynamo_headers) { { 'x-last-evaluated-hash-key' => 'foo', 'x-last-evaluated-range-key' => 'bar' } } let(:dynamo_params) { { last_evaluated_hash_key: 'foo', last_evaluated_range_key: 'bar' } } it 'retrieves subsequent pages when all is true' do path = '/api/quizzes' stub_quiz_api path, headers: dynamo_headers stub_quiz_api path, item: 2, query: dynamo_params expect(client.get('/api/quizzes', all: true)).to eq [1, 2] end it 'retrieves subsequent pages when all is true and query is present' do path = '/api/quizzes' stub_quiz_api path, query: { my_id: 12 }, headers: dynamo_headers stub_quiz_api path, item: 2, query: { my_id: 12, **dynamo_params } expect(client.get('/api/quizzes', query: { my_id: 12 }, all: true)).to eq [1, 2] end it 'retrieves subsequent pages when all is true and sanitizes query if page is nil' do path = '/api/quizzes' stub_quiz_api path, query: { my_id: 12 }, headers: dynamo_headers stub_quiz_api path, item: 2, query: { my_id: 12, **dynamo_params } expect(client.get('/api/quizzes', query: { my_id: 12, page: nil }, all: true)).to eq [1, 2] end it 'retrieves subsequent pages when all is true and sanitizes query if per_page is nil' do path = '/api/quizzes' stub_quiz_api path, query: { my_id: 12 }, headers: dynamo_headers stub_quiz_api path, item: 2, query: { my_id: 12, **dynamo_params } expect(client.get('/api/quizzes', query: { my_id: 12, per_page: nil }, all: true)).to eq [1, 2] end end end end describe 'post' do it 'makes a post request' do url = 'http://api.quiz.docker/api/quizzes' expect(client.class).to receive(:post).with( url, body: { title: 'ohai' }.to_json, **default_request_data ).and_return(instance_double('HTTParty::Response', success?: true, code: 200)) expect_metrics_calls(client.config, :post, url, 200) client.post('/api/quizzes', title: 'ohai') end end describe 'patch' do it 'makes a patch request' do url = 'http://api.quiz.docker/api/quizzes/1' expect(client.class).to receive(:patch).with( url, body: { title: 'new title' }.to_json, **default_request_data ).and_return(instance_double('HTTParty::Response', success?: true, code: 200)) expect_metrics_calls(client.config, :patch, url, 200) client.patch('/api/quizzes/1', title: 'new title') end end describe 'put' do it 'makes a put request' do url = 'http://api.quiz.docker/api/quizzes/1' expect(client.class).to receive(:put).with( url, body: { title: 'new title' }.to_json, **default_request_data ).and_return(instance_double('HTTParty::Response', success?: true, code: 200)) expect_metrics_calls(client.config, :put, url, 200) client.put('/api/quizzes/1', title: 'new title') end end describe 'delete' do it 'makes a delete request' do url = 'http://api.quiz.docker/api/quizzes/1' expect(client.class).to receive(:delete).with( url, **default_request_data ).and_return(instance_double('HTTParty::Response', success?: true, code: 200)) expect_metrics_calls(client.config, :delete, url, 200) client.delete('/api/quizzes/1') end end describe 'headers' do it 'includes the correct headers' do expect(default_request_data[:headers]).to eq( 'Authorization' => jwt, 'AuthType' => 'Signature', 'Accept' => 'application/json', 'Content-Type' => 'application/json', 'X-Consumer-Request-Id' => 'hi' ) end end describe '#successful_response?' do it 'returns true if the response is successful' do resp = instance_double('HTTParty::Response', success?: true) expect(client.send(:successful_response?, resp)).to be_truthy end it 'returns true if the response code is 401' do resp = instance_double('HTTParty::Response', success?: false, code: 401) expect(client.send(:successful_response?, resp)).to be_truthy end it 'returns false if the response is not successful' do resp = instance_double('HTTParty::Response', success?: false, code: 404) expect(client.send(:successful_response?, resp)).to be_falsey end end describe 'error handling' do context 'with :sentry_raven error handler' do let(:error_handler) { :sentry_raven } let(:path) { '/' } let(:url) { "http://api.quiz.docker#{path}" } let(:error_context) do { quiz_api_client: { request: { method: :get, url: url } } } end before do client.config.error_handler = error_handler end context 'with non-success responses' do let(:body) { '[2]' } let(:code) { 404 } let(:path) { '/api/quizzes' } let(:context_url) { url } let(:method) { :get } let(:error_context) do { quiz_api_client: { request: { method: method, url: context_url }, response: { body: body, code: code } } } end let(:mock_response) do instance_double( 'HTTParty::Response', success?: false, body: body, code: code ) end let(:success_response) do # rubocop:disable Metrics/LineLength instance_double( 'HTTParty::Response', body: body, code: 200, parsed_response: [1], headers: { 'link' => '; rel="last", ; rel="next"', 'content-type' => ['application/json'] } ) # rubocop:enable Metrics/LineLength end context ':get request' do it 'raises error' do expect(client).to receive(:successful_response?).with(mock_response).and_return(false) expect(client.class).to receive(:get).and_return(mock_response) expect_metrics_calls(client.config, method, url_for_path(path), code) expect_raise_error_call(client.config, method, url_for_path(path), mock_response, nil) expect { client.get(path, all: false, query: { sort: 'alpha' }) } .to raise_error(QuizApiClient::HttpClient::RequestFailed) end end context ':post request' do let(:method) { :post } it 'raises error' do expect(client).to receive(:successful_response?).with(mock_response).and_return(false) expect(client.class).to receive(:post).and_return(mock_response) expect_metrics_calls(client.config, method, url_for_path(path), code) expect_raise_error_call(client.config, method, url_for_path(path), mock_response, nil) expect { client.post(path) }.to raise_error(QuizApiClient::HttpClient::RequestFailed) end end context ':put request' do let(:method) { :put } it 'raises error' do expect(client).to receive(:successful_response?).with(mock_response).and_return(false) expect(client.class).to receive(:put).and_return(mock_response) expect_metrics_calls(client.config, method, url_for_path(path), code) expect_raise_error_call(client.config, method, url_for_path(path), mock_response, nil) expect { client.put(path) }.to raise_error(QuizApiClient::HttpClient::RequestFailed) end end context ':patch request' do let(:method) { :patch } it 'raises error' do expect(client).to receive(:successful_response?).with(mock_response).and_return(false) expect(client.class).to receive(:patch).and_return(mock_response) expect_metrics_calls(client.config, method, url_for_path(path), code) expect_raise_error_call(client.config, method, url_for_path(path), mock_response, nil) expect { client.patch(path) }.to raise_error(QuizApiClient::HttpClient::RequestFailed) end end context ':delete request' do let(:method) { :delete } it 'raises error' do expect(client).to receive(:successful_response?).with(mock_response).and_return(false) expect(client.class).to receive(:delete).and_return(mock_response) expect_metrics_calls(client.config, method, url_for_path(path), code) expect_raise_error_call(client.config, method, url_for_path(path), mock_response, nil) expect { client.delete(path) }.to raise_error(QuizApiClient::HttpClient::RequestFailed) end end context 'with pagination' do let(:context_url) { "#{url}?page=2" } it 'raises error if one of the linked pages does not return 200 response' do stub_quiz_api path, headers: { link: link_header(path, 1, 2) } stub_quiz_api path, item: 2, query: { page: 2 }, headers: { link: link_header(path, 2, 2) }, status: code allow(HTTParty::Response).to receive(:new).and_return(success_response, mock_response) expect(client).to receive(:successful_response?).and_return(true, false).twice expect_raise_error_call(client.config, method, context_url, mock_response, nil) expect { client.get('/api/quizzes', all: true) }.to raise_error(QuizApiClient::HttpClient::RequestFailed) end end end it 'handles an HTTParty::Error error' do stub_request(:get, url).to_raise(HTTParty::Error) expect_metrics_calls(client.config, :get, url_for_path(path), 0) expect_raise_error_call(client.config, :get, url_for_path(path), nil, kind_of(HTTParty::Error)) expect { client.get('/') }.to raise_error(QuizApiClient::HttpClient::RequestFailed) end it 'handles an Errno::ECONNREFUSED error' do stub_request(:get, url).to_raise(Errno::ECONNREFUSED) expect_metrics_calls(client.config, :get, url_for_path(path), 0) expect_raise_error_call(client.config, :get, url_for_path(path), nil, kind_of(Errno::ECONNREFUSED)) expect { client.get('/') }.to raise_error(QuizApiClient::HttpClient::RequestFailed) end it 'handles an Net::ReadTimeout error' do stub_request(:get, url).to_raise(Net::ReadTimeout) expect_metrics_calls(client.config, :get, url_for_path(path), 0) expect_raise_error_call(client.config, :get, url_for_path(path), nil, kind_of(Net::ReadTimeout)) expect { client.get('/') }.to raise_error(QuizApiClient::HttpClient::RequestFailed) end end end end