diff --git a/lib/adyen/client.rb b/lib/adyen/client.rb index b79e2f1..3aa0434 100644 --- a/lib/adyen/client.rb +++ b/lib/adyen/client.rb @@ -144,8 +144,17 @@ def service_url(service, action, version) def call_adyen_api(service, action, request_data, headers, version, _with_application_info: false) # get URL for requested endpoint url = service_url(service, action.is_a?(String) ? action : action.fetch(:url), version) - - auth_type = auth_type(service, request_data) + + # get method from action or default to post + method = action.is_a?(String) ? 'post' : action.fetch(:method) + + call_adyen_api_url(url, method, request_data, headers) + end + + # send request to adyen API with a given full URL + def call_adyen_api_url(url, method, request_data, headers) + # make sure valid authentication has been provided, without a specific service + validate_auth(request_data) # initialize Faraday connection object conn = Faraday.new(url, @connection_options) do |faraday| @@ -154,7 +163,7 @@ def call_adyen_api(service, action, request_data, headers, version, _with_applic faraday.headers['User-Agent'] = "#{Adyen::NAME}/#{Adyen::VERSION}" # set header based on auth_type and service - auth_header(auth_type, faraday) + add_auth_header(faraday) # add optional headers if specified in request # will overwrite default headers if overlapping @@ -173,49 +182,27 @@ def call_adyen_api(service, action, request_data, headers, version, _with_applic # convert to json request_data = request_data.to_json - if action.is_a?(::Hash) - if action.fetch(:method) == 'get' - begin - response = conn.get - rescue Faraday::ConnectionFailed => e - raise e, "Connection to #{url} failed" - end - end - if action.fetch(:method) == 'delete' - begin - response = conn.delete - rescue Faraday::ConnectionFailed => e - raise e, "Connection to #{url} failed" - end - end - if action.fetch(:method) == 'patch' - begin - response = conn.patch do |req| + begin + response = case method + when 'get' + conn.get + when 'delete' + conn.delete + when 'patch' + conn.patch do |req| req.body = request_data end - rescue Faraday::ConnectionFailed => e - raise e, "Connection to #{url} failed" - end - end - if action.fetch(:method) == 'post' - # post request to Adyen - begin - response = conn.post do |req| + when 'post' + conn.post do |req| req.body = request_data end - rescue Faraday::ConnectionFailed => e - raise e, "Connection to #{url} failed" - end - end - else - begin - response = conn.post do |req| - req.body = request_data + else + raise ArgumentError, "Invalid HTTP method: #{method}" end - rescue Faraday::ConnectionFailed => e - raise e, "Connection to #{url} failed" - end + rescue Faraday::ConnectionFailed => e + raise e, "Connection to #{url} failed" end + # check for API errors case response.status when 400 @@ -326,10 +313,10 @@ def balance_control @balance_control ||= Adyen::BalanceControl.new(self) end - + private - def auth_header(auth_type, faraday) + def add_auth_header(faraday) case auth_type when "basic" if Gem::Version.new(Faraday::VERSION) >= Gem::Version.new('2.0') @@ -346,9 +333,7 @@ def auth_header(auth_type, faraday) end end - def auth_type(service, request_data) - # make sure valid authentication has been provided - validate_auth_type(service, request_data) + def auth_type # Will prioritize authentication methods in this order: # api-key, oauth, basic return "api-key" unless @api_key.nil? @@ -356,7 +341,7 @@ def auth_type(service, request_data) "basic" end - def validate_auth_type(service, request_data) + def validate_auth(request_data) # ensure authentication has been provided if @api_key.nil? && @oauth_token.nil? && (@ws_password.nil? || @ws_user.nil?) raise Adyen::AuthenticationError.new( @@ -364,10 +349,6 @@ def validate_auth_type(service, request_data) request_data ) end - if service == "PaymentSetupAndVerification" && @api_key.nil? && @oauth_token.nil? && @ws_password.nil? && @ws_user.nil? - raise Adyen::AuthenticationError.new('Checkout service requires API-key or oauth_token', request_data), - 'Checkout service requires API-key or oauth_token' - end end # build the error message from the response payload diff --git a/spec/client_spec.rb b/spec/client_spec.rb index bdc761c..357462c 100644 --- a/spec/client_spec.rb +++ b/spec/client_spec.rb @@ -118,8 +118,8 @@ skip "Only runs on Ruby >= 3.2" unless RUBY_VERSION >= '3.2' connection_options = Faraday::ConnectionOptions.new( request: { - open_timeout: 5, - timeout: 10 + open_timeout: 5, + timeout: 10 } ) expect(Faraday::ConnectionOptions).not_to receive(:new) @@ -278,7 +278,7 @@ .to eq('https://terminal-api-test.adyen.com/connectedTerminals') end - + it 'checks the initialization of the terminal region' do client = Adyen::Client.new(api_key: 'api_key', env: :test, terminal_region: 'eu') expect(client.service_url('TerminalCloudAPI', 'connectedTerminals', nil)) @@ -301,7 +301,7 @@ client = Adyen::Client.new(env: :test) expect(client.service_url_base('Disputes')) .to eq('https://ca-test.adyen.com/ca/services/DisputesService') - end + end it 'checks the creation of SessionAuthentication url for the test env' do client = Adyen::Client.new(env: :test) @@ -375,7 +375,7 @@ message: "Return URL is missing.", errorType: "validation", pspReference: "8816118280275544" - } + } mock_response = Faraday::Response.new(status: 422, body: error_body) allow(Faraday).to receive(:new).and_return(mock_faraday_connection) @@ -435,7 +435,7 @@ it 'raises NotFoundError on 404 response with an invalid JSON body' do client = Adyen::Client.new(api_key: 'api_key', env: :test) mock_faraday_connection = double(Faraday::Connection) - error_body = "this is an error message" + error_body = "this is an error message" mock_response = Faraday::Response.new(status: 404, body: error_body) allow(Faraday).to receive(:new).and_return(mock_faraday_connection) @@ -449,5 +449,249 @@ expect(error.msg).to eq('Not found error') end end - + + + describe '#call_adyen_api' do + let(:client) { Adyen::Client.new(api_key: 'test_key', env: :test) } + let(:mock_faraday_connection) { double(Faraday::Connection) } + + before do + allow(mock_faraday_connection).to receive_message_chain(:headers, :[]=) + allow(mock_faraday_connection).to receive(:adapter) + end + + it 'successfully makes a POST request and returns AdyenResult' do + response_body = { pspReference: '123456789', resultCode: 'Authorised' }.to_json + mock_response = Faraday::Response.new( + status: 200, + body: response_body, + response_headers: { 'content-type' => 'application/json' } + ) + + expect(Faraday).to receive(:new) + .with('https://checkout-test.adyen.com/v71/payments', anything) + .and_return(mock_faraday_connection) + allow(mock_faraday_connection).to receive(:post).and_return(mock_response) + + result = client.call_adyen_api('Checkout', 'payments', { amount: { value: 1000 } }, {}, '71') + + expect(result).to be_a(Adyen::AdyenResult) + expect(result.status).to eq(200) + expect(result.response["pspReference"]).to eq('123456789') + end + + it 'successfully makes a GET request' do + response_body = { data: [{ id: '1' }] }.to_json + mock_response = Faraday::Response.new(status: 200, body: response_body, response_headers: {}) + + expect(Faraday).to receive(:new) + .with('https://management-test.adyen.com/v1/merchants/MyMerchantID/paymentsApps', anything) + .and_return(mock_faraday_connection) + .and_yield(mock_faraday_connection) + allow(mock_faraday_connection).to receive(:get).and_return(mock_response) + + result = client.call_adyen_api( + 'Management', + { url: 'merchants/MyMerchantID/paymentsApps', method: 'get' }, + {}, + {}, + '1' + ) + + expect(result).to be_a(Adyen::AdyenResult) + expect(result.status).to eq(200) + end + + it 'sets correct headers including custom headers' do + mock_response = Faraday::Response.new(status: 200, body: '{}', response_headers: {}) + headers_spy = {} + + expect(Faraday).to receive(:new) + .with('https://checkout-test.adyen.com/v71/storedPaymentMethods', anything) + .and_return(mock_faraday_connection) + .and_yield(mock_faraday_connection) + allow(mock_faraday_connection).to receive_message_chain(:headers, :[]=) do |key, value| + headers_spy[key] = value + end + allow(mock_faraday_connection).to receive(:post).and_return(mock_response) + + custom_headers = { 'Idempotency-Key' => 'test-123' } + client.call_adyen_api('Checkout', 'storedPaymentMethods', {}, custom_headers, '71') + + expect(headers_spy['Content-Type']).to eq('application/json') + expect(headers_spy['x-api-key']).to eq('test_key') + expect(headers_spy['Idempotency-Key']).to eq('test-123') + end + + it 'handles connection failures' do + expect(Faraday).to receive(:new) + .with('https://checkout-test.adyen.com/v71/paymentLinks/PL61C53A8B97E6924D', anything) + .and_return(mock_faraday_connection) + .and_yield(mock_faraday_connection) + allow(mock_faraday_connection).to receive(:post).and_raise(Faraday::ConnectionFailed.new('Connection failed')) + + expect { + client.call_adyen_api('Checkout', 'paymentLinks/PL61C53A8B97E6924D', {}, {}, '71') + }.to raise_error(Faraday::ConnectionFailed, /Connection to .* failed/) + end + + it 'converts request data to JSON' do + mock_response = Faraday::Response.new(status: 200, body: '{}', response_headers: {}) + + expect(Faraday).to receive(:new) + .with('https://checkout-test.adyen.com/v71/payments', anything) + .and_return(mock_faraday_connection) + .and_yield(mock_faraday_connection) + + request_body_sent = nil + allow(mock_faraday_connection).to receive(:post) do |&block| + req = double('request') + allow(req).to receive(:body=) { |body| request_body_sent = body } + block.call(req) + mock_response + end + + hash_data = { amount: { value: 1000, currency: 'EUR' } } + client.call_adyen_api('Checkout', 'payments', hash_data, {}, '71') + + expect(request_body_sent).to eq(hash_data.to_json) + end + + it 'validates authentication is provided' do + client_without_auth = Adyen::Client.new(env: :test) + + expect { + client_without_auth.call_adyen_api('Checkout', 'payments', {}, {}, '71') + }.to raise_error(Adyen::AuthenticationError, /No authentication found/) + end + end + + describe '#call_adyen_api_url' do + let(:client) { Adyen::Client.new(api_key: 'test_key', env: :test) } + let(:mock_faraday_connection) { double(Faraday::Connection) } + + before do + allow(mock_faraday_connection).to receive_message_chain(:headers, :[]=) + allow(mock_faraday_connection).to receive(:adapter) + end + + it 'successfully makes a POST request with full URL' do + response_body = { pspReference: '987654321', resultCode: 'Authorised' }.to_json + mock_response = Faraday::Response.new( + status: 200, + body: response_body, + response_headers: { 'content-type' => 'application/json' } + ) + + expect(Faraday).to receive(:new) + .with('https://balanceplatform-api-test.adyen.com/capital/v1/grants', anything) + .and_return(mock_faraday_connection) + .and_yield(mock_faraday_connection) + allow(mock_faraday_connection).to receive(:post).and_return(mock_response) + + result = client.call_adyen_api_url( + 'https://balanceplatform-api-test.adyen.com/capital/v1/grants', + 'post', + { amount: { value: 2000 } }, + {} + ) + + expect(result).to be_a(Adyen::AdyenResult) + expect(result.status).to eq(200) + expect(result.response["pspReference"]).to eq('987654321') + end + + it 'successfully makes a GET request' do + response_body = { data: [{ id: 'comp-1' }] }.to_json + mock_response = Faraday::Response.new(status: 200, body: response_body, response_headers: {}) + + expect(Faraday).to receive(:new) + .with('https://management-test.adyen.com/v3/merchants/MyMerchantID/stores', anything) + .and_return(mock_faraday_connection) + .and_yield(mock_faraday_connection) + allow(mock_faraday_connection).to receive(:get).and_return(mock_response) + + result = client.call_adyen_api_url( + 'https://management-test.adyen.com/v3/merchants/MyMerchantID/stores', + 'get', + {}, + {} + ) + + expect(result).to be_a(Adyen::AdyenResult) + expect(result.status).to eq(200) + end + + it 'sets correct headers' do + mock_response = Faraday::Response.new(status: 200, body: '{}', response_headers: {}) + headers_spy = {} + + expect(Faraday).to receive(:new) + .with('https://custom.adyen.com/api/endpoint', anything) + .and_return(mock_faraday_connection) + .and_yield(mock_faraday_connection) + allow(mock_faraday_connection).to receive_message_chain(:headers, :[]=) do |key, value| + headers_spy[key] = value + end + allow(mock_faraday_connection).to receive(:post).and_return(mock_response) + + custom_headers = { 'X-Custom-Header' => 'custom-value' } + client.call_adyen_api_url('https://custom.adyen.com/api/endpoint', 'post', {}, custom_headers) + + expect(headers_spy['Content-Type']).to eq('application/json') + expect(headers_spy['x-api-key']).to eq('test_key') + expect(headers_spy['X-Custom-Header']).to eq('custom-value') + end + + it 'handles connection failures' do + expect(Faraday).to receive(:new) + .with('https://management-test.adyen.com/v3/stores', anything) + .and_return(mock_faraday_connection) + .and_yield(mock_faraday_connection) + allow(mock_faraday_connection).to receive(:post).and_raise(Faraday::ConnectionFailed.new('Connection failed')) + + expect { + client.call_adyen_api_url('https://management-test.adyen.com/v3/stores', 'post', {}, {}) + }.to raise_error(Faraday::ConnectionFailed, /Connection to .* failed/) + end + + it 'supports different HTTP methods' do + mock_response = Faraday::Response.new(status: 200, body: '{"status":"updated"}', response_headers: {}) + + expect(Faraday).to receive(:new) + .with('https://checkout-test.adyen.com/v71/paymentLinks/PL61C53A8B97E6915A', anything) + .and_return(mock_faraday_connection) + .and_yield(mock_faraday_connection) + allow(mock_faraday_connection).to receive(:patch).and_return(mock_response) + + result = client.call_adyen_api_url( + 'https://checkout-test.adyen.com/v71/paymentLinks/PL61C53A8B97E6915A', + 'patch', + { status: 'active' }, + {} + ) + + expect(result).to be_a(Adyen::AdyenResult) + expect(result.status).to eq(200) + end + + it 'raises for invalid HTTP method' do + expect { + client.call_adyen_api_url('https://management-test.adyen.com/v1/something', 'invalid', {}, {}) + }.to raise_error(ArgumentError, "Invalid HTTP method: invalid") + end + + it 'validates authentication is provided' do + client_without_auth = Adyen::Client.new(env: :test) + + expect { + client_without_auth.call_adyen_api_url( + 'https://checkout-test.adyen.com/v71/paymentMethods/balance', + 'post', + {}, + {} + ) + }.to raise_error(Adyen::AuthenticationError, /No authentication found/) + end + end end