diff --git a/lib/ruby_api_pack_cloudways/connection/cw_connect.rb b/lib/ruby_api_pack_cloudways/connection/cw_connect.rb index 6b304c6..eb62cca 100644 --- a/lib/ruby_api_pack_cloudways/connection/cw_connect.rb +++ b/lib/ruby_api_pack_cloudways/connection/cw_connect.rb @@ -60,7 +60,7 @@ def handle_response(response) # Parse response from Cloudways API def parse_response(response) - content_type = response.headers['content-type'] + content_type = response.headers&.fetch('content-type', nil) raise "Unexpected response: #{response.body}" unless content_type&.include?('application/json') Oj.load(response.body, mode: :strict) diff --git a/spec/ruby_api_pack_cloudways/connection/cw_connect_spec.rb b/spec/ruby_api_pack_cloudways/connection/cw_connect_spec.rb index 1ad5de8..c42e558 100644 --- a/spec/ruby_api_pack_cloudways/connection/cw_connect_spec.rb +++ b/spec/ruby_api_pack_cloudways/connection/cw_connect_spec.rb @@ -3,7 +3,14 @@ RSpec.describe RubyApiPackCloudways::Connection::CwConnect do let(:connection) { described_class.new('https://api.cloudways.com/api/v1', '/some_path') } let(:token_instance) { instance_double(RubyApiPackCloudways::Connection::CwToken, cw_api_token: 'fake_token') } - let(:http_response) { instance_double(HTTParty::Response, code: 200, body: '{"data":"value"}') } + let(:http_response) do + instance_double( + HTTParty::Response, + code: 200, + body: '{"data":"value"}', + headers: { 'content-type' => 'application/json' } + ) + end let(:post_params) { { key: 'value' } } before do @@ -29,7 +36,14 @@ end context 'when the response code is not 200' do - let(:error_response) { instance_double(HTTParty::Response, code: 500, body: '{"error":"Server error"}') } + let(:error_response) do + instance_double( + HTTParty::Response, + code: 500, + body: '{"error":"Server error"}', + headers: { 'content-type' => 'application/json' } + ) + end before do allow(HTTParty).to receive(:get).and_return(error_response) @@ -41,7 +55,14 @@ end context 'when parsing the response fails' do - let(:faulty_response) { instance_double(HTTParty::Response, code: 200, body: 'invalid_json') } + let(:faulty_response) do + instance_double( + HTTParty::Response, + code: 200, + body: 'invalid_json', + headers: { 'content-type' => 'application/json' } + ) + end before do allow(HTTParty).to receive(:get).and_return(faulty_response) @@ -52,6 +73,41 @@ expect { connection.cloudways_api_connection }.to raise_error(/Error parsing response: Unexpected character/) end end + + context 'when rate limit is exceeded' do + let(:rate_limit_error) { RuntimeError.new('Rate limit exceeded') } + + before do + attempts = 0 + allow(HTTParty).to receive(:get).and_wrap_original do |original_method, *args| + attempts += 1 + raise rate_limit_error if attempts == 1 + + instance_double( + HTTParty::Response, + code: 200, + body: '{"data":"value"}', + headers: { 'content-type' => 'application/json' } + ) + end + end + + it 'retries after a rate limit error and succeeds' do + response = connection.cloudways_api_connection + expect(response['data']).to eq('value') + expect(HTTParty).to have_received(:get).twice # Confirms retry occurred + end + end + + context 'when an unexpected runtime error occurs' do + before do + allow(HTTParty).to receive(:get).and_raise(RuntimeError, 'Unexpected error') + end + + it 'raises the unexpected runtime error' do + expect { connection.cloudways_api_connection }.to raise_error(RuntimeError, 'Unexpected error') + end + end end describe '#cloudways_api_post_connection' do @@ -76,7 +132,14 @@ end context 'when the POST response code is not 200' do - let(:error_response) { instance_double(HTTParty::Response, code: 500, body: '{"error":"Server error"}') } + let(:error_response) do + instance_double( + HTTParty::Response, + code: 500, + body: '{"error":"Server error"}', + headers: { 'content-type' => 'application/json' } + ) + end before do allow(HTTParty).to receive(:post).and_return(error_response) @@ -88,7 +151,14 @@ end context 'when parsing the POST response fails' do - let(:faulty_response) { instance_double(HTTParty::Response, code: 200, body: 'invalid_json') } + let(:faulty_response) do + instance_double( + HTTParty::Response, + code: 200, + body: 'invalid_json', + headers: { 'content-type' => 'application/json' } + ) + end before do allow(HTTParty).to receive(:post).and_return(faulty_response) @@ -102,4 +172,67 @@ end end end + + describe '#parse_response' do + let(:valid_response) do + instance_double( + HTTParty::Response, + body: '{"key":"value"}', + headers: { 'content-type' => 'application/json' } + ) + end + + let(:invalid_response_missing_content_type) do + instance_double( + HTTParty::Response, + body: '{"key":"value"}', + headers: nil + ) + end + + let(:invalid_response_wrong_content_type) do + instance_double( + HTTParty::Response, + body: '{"key":"value"}', + headers: { 'content-type' => 'text/html' } + ) + end + + let(:invalid_json_response) do + instance_double( + HTTParty::Response, + body: 'invalid_json', + headers: { 'content-type' => 'application/json' } + ) + end + + it 'parses the response body successfully when content-type is application/json' do + result = connection.send(:parse_response, valid_response) + expect(result).to eq({ 'key' => 'value' }) + end + + it 'raises an error when content-type is missing' do + invalid_response_missing_content_type = instance_double( + HTTParty::Response, + body: '{"key":"value"}', + headers: nil + ) + expect do + connection.send(:parse_response, invalid_response_missing_content_type) + end.to raise_error(/Unexpected response/) + end + + it 'raises an error when content-type does not include application/json' do + expect do + connection.send(:parse_response, invalid_response_wrong_content_type) + end.to raise_error(/Unexpected response/) + end + + it 'raises a parsing error when Oj fails to parse the response body' do + allow(Oj).to receive(:load).and_raise(Oj::ParseError, 'Unexpected character') + expect do + connection.send(:parse_response, invalid_json_response) + end.to raise_error(/Error parsing response: Unexpected character/) + end + end end diff --git a/spec/ruby_api_pack_cloudways/connection/cw_token_spec.rb b/spec/ruby_api_pack_cloudways/connection/cw_token_spec.rb index 7fa160a..f549aaf 100644 --- a/spec/ruby_api_pack_cloudways/connection/cw_token_spec.rb +++ b/spec/ruby_api_pack_cloudways/connection/cw_token_spec.rb @@ -5,8 +5,22 @@ RSpec.describe RubyApiPackCloudways::Connection::CwToken do let(:token) { described_class.new } - let(:response) { instance_double(HTTParty::Response, body: '{"access_token":"fake_token"}') } - let(:invalid_json_response) { instance_double(HTTParty::Response, body: 'invalid_json') } + let(:valid_response) do + instance_double( + HTTParty::Response, + code: 200, + body: '{"access_token":"fake_token", "expires_in": 3600}', # Includes "expires_in" + headers: { 'content-type' => 'application/json' } + ) + end + let(:error_response) do + instance_double( + HTTParty::Response, + code: 500, + body: '{"error":"Invalid request"}', + headers: { 'content-type' => 'application/json' } + ) + end before do RubyApiPackCloudways.configure do |config| @@ -15,37 +29,64 @@ config.api_email = 'test@example.com' config.api_key = 'test_key' end - allow(HTTParty).to receive(:post).and_return(response) + + allow(HTTParty).to receive(:post).and_return(valid_response) end describe '#cw_api_token' do - it 'returns an access token' do - expect(token.cw_api_token).to eq('fake_token') + context 'when a valid cached token exists' do + before do + allow(token).to receive(:valid_cached_token?).and_return(true) + token.instance_variable_set(:@cached_token, 'cached_token') + end + + it 'returns the cached token without making an HTTP request' do + expect(HTTParty).not_to receive(:post) + expect(token.cw_api_token).to eq('cached_token') + end end - it 'calls the correct endpoint' do - token.cw_api_token - expect_httparty_post_with_correct_params + context 'when no cached token exists or it is expired' do + it 'fetches a new token and returns it' do + expect(token.cw_api_token).to eq('fake_token') + end end - context 'when the response body is invalid JSON' do + context 'when the response code is not 200' do before do - allow(Oj).to receive(:load).and_raise(Oj::ParseError, 'Invalid JSON') + allow(HTTParty).to receive(:post).and_return(error_response) end - it 'raises a parsing error' do - expect do - token.send(:parse_response, invalid_json_response) - end.to raise_error(RuntimeError, 'Error parsing response: Invalid JSON') + it 'raises an error with the response code' do + expect { token.cw_api_token }.to raise_error(RuntimeError, /Failed to fetch token: 500/) + end + end + + context 'when the cached token is expired' do + before do + token.instance_variable_set(:@cached_token, 'expired_token') + token.instance_variable_set(:@token_expiry, Time.now - 3600) # Token expired an hour ago + end + + it 'fetches a new token' do + expect(token.cw_api_token).to eq('fake_token') end end end - def expect_httparty_post_with_correct_params - expect(HTTParty).to have_received(:post).with( - 'https://api.cloudways.com/api/v1/oauth/access_token', - headers: { 'Content-Type' => 'application/x-www-form-urlencoded' }, - body: { email: 'test@example.com', api_key: 'test_key' } - ) + describe '#parse_response' do + let(:valid_response) { instance_double(HTTParty::Response, body: '{"key":"value"}') } + let(:invalid_response) { instance_double(HTTParty::Response, body: 'invalid_json') } + + it 'parses the response body successfully' do + expect(token.send(:parse_response, valid_response)).to eq({ 'key' => 'value' }) + end + + it 'raises an error when parsing fails' do + allow(Oj).to receive(:load).and_raise(Oj::ParseError, 'Unexpected character') + expect do + token.send(:parse_response, invalid_response) + end.to raise_error(RuntimeError, /Error parsing response: Unexpected character/) + end end end