From afa51dc4775b25002f1fe310c5c7692d8d6acdc8 Mon Sep 17 00:00:00 2001 From: Hugo Vacher Date: Tue, 28 Oct 2025 10:39:18 -0400 Subject: [PATCH 1/3] Add test coverage for #call_adyen_api --- spec/client_spec.rb | 126 +++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 119 insertions(+), 7 deletions(-) diff --git a/spec/client_spec.rb b/spec/client_spec.rb index bdc761c..32b8a19 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,117 @@ expect(error.msg).to eq('Not found error') end end - + + + describe '#call_adyen_api' do + it 'successfully makes a POST request and returns AdyenResult' do + client = Adyen::Client.new(api_key: 'test_key', env: :test) + mock_faraday_connection = double(Faraday::Connection) + 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_message_chain(:headers, :[]=) + 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 + client = Adyen::Client.new(api_key: 'test_key', env: :test) + mock_faraday_connection = double(Faraday::Connection) + 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/companies', anything) + .and_return(mock_faraday_connection) + .and_yield(mock_faraday_connection) + allow(mock_faraday_connection).to receive(:adapter) + allow(mock_faraday_connection).to receive_message_chain(:headers, :[]=) + allow(mock_faraday_connection).to receive(:get).and_return(mock_response) + + result = client.call_adyen_api('Management', { url: 'companies', 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 + client = Adyen::Client.new(api_key: 'test_key', env: :test) + mock_faraday_connection = double(Faraday::Connection) + mock_response = Faraday::Response.new(status: 200, body: '{}', response_headers: {}) + + headers_spy = {} + expect(Faraday).to receive(:new) + .with('https://checkout-test.adyen.com/v71/payments', anything) + .and_return(mock_faraday_connection) + .and_yield(mock_faraday_connection) + allow(mock_faraday_connection).to receive(:adapter) + 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', 'payments', {}, 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 + client = Adyen::Client.new(api_key: 'test_key', env: :test) + mock_faraday_connection = double(Faraday::Connection) + + expect(Faraday).to receive(:new) + .with('https://checkout-test.adyen.com/v71/payments', anything) + .and_return(mock_faraday_connection) + .and_yield(mock_faraday_connection) + allow(mock_faraday_connection).to receive_message_chain(:headers, :[]=) + allow(mock_faraday_connection).to receive(:adapter) + allow(mock_faraday_connection).to receive(:post).and_raise(Faraday::ConnectionFailed.new('Connection failed')) + + expect { + client.call_adyen_api('Checkout', 'payments', {}, {}, '71') + }.to raise_error(Faraday::ConnectionFailed, /Connection to .* failed/) + end + + it 'converts request data to JSON' do + client = Adyen::Client.new(api_key: 'test_key', env: :test) + mock_faraday_connection = double(Faraday::Connection) + 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) + allow(mock_faraday_connection).to receive(:adapter) + allow(mock_faraday_connection).to receive_message_chain(:headers, :[]=) + + 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 + end end From d2445b60dde22982101d4a61b2eb79f4a38697d5 Mon Sep 17 00:00:00 2001 From: Hugo Vacher Date: Tue, 28 Oct 2025 10:41:07 -0400 Subject: [PATCH 2/3] Introduce Client#call_adyen_api_url method This method allows passing in a full URL for users that do not want to use the whole concept of services and actions. --- lib/adyen/client.rb | 77 ++++++++----------- spec/client_spec.rb | 176 +++++++++++++++++++++++++++++++++++++------- 2 files changed, 182 insertions(+), 71 deletions(-) diff --git a/lib/adyen/client.rb b/lib/adyen/client.rb index b79e2f1..dbb43ec 100644 --- a/lib/adyen/client.rb +++ b/lib/adyen/client.rb @@ -144,8 +144,19 @@ 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) + + # make sure valid authentication has been provided + validate_auth_type(service, request_data) + + 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_type(nil, request_data) # initialize Faraday connection object conn = Faraday.new(url, @connection_options) do |faraday| @@ -154,7 +165,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 +184,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 +315,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 +335,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? diff --git a/spec/client_spec.rb b/spec/client_spec.rb index 32b8a19..bfcb0d4 100644 --- a/spec/client_spec.rb +++ b/spec/client_spec.rb @@ -452,9 +452,15 @@ 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 - client = Adyen::Client.new(api_key: 'test_key', env: :test) - mock_faraday_connection = double(Faraday::Connection) response_body = { pspReference: '123456789', resultCode: 'Authorised' }.to_json mock_response = Faraday::Response.new( status: 200, @@ -465,7 +471,6 @@ 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_message_chain(:headers, :[]=) allow(mock_faraday_connection).to receive(:post).and_return(mock_response) result = client.call_adyen_api('Checkout', 'payments', { amount: { value: 1000 } }, {}, '71') @@ -476,43 +481,42 @@ end it 'successfully makes a GET request' do - client = Adyen::Client.new(api_key: 'test_key', env: :test) - mock_faraday_connection = double(Faraday::Connection) 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/companies', anything) + .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(:adapter) - allow(mock_faraday_connection).to receive_message_chain(:headers, :[]=) allow(mock_faraday_connection).to receive(:get).and_return(mock_response) - result = client.call_adyen_api('Management', { url: 'companies', method: 'get' }, {}, {}, '1') + 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 - client = Adyen::Client.new(api_key: 'test_key', env: :test) - mock_faraday_connection = double(Faraday::Connection) mock_response = Faraday::Response.new(status: 200, body: '{}', response_headers: {}) - headers_spy = {} + expect(Faraday).to receive(:new) - .with('https://checkout-test.adyen.com/v71/payments', anything) + .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(:adapter) 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', 'payments', {}, custom_headers, '71') + 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') @@ -520,33 +524,24 @@ end it 'handles connection failures' do - client = Adyen::Client.new(api_key: 'test_key', env: :test) - mock_faraday_connection = double(Faraday::Connection) - expect(Faraday).to receive(:new) - .with('https://checkout-test.adyen.com/v71/payments', anything) + .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_message_chain(:headers, :[]=) - allow(mock_faraday_connection).to receive(:adapter) allow(mock_faraday_connection).to receive(:post).and_raise(Faraday::ConnectionFailed.new('Connection failed')) expect { - client.call_adyen_api('Checkout', 'payments', {}, {}, '71') + client.call_adyen_api('Checkout', 'paymentLinks/PL61C53A8B97E6924D', {}, {}, '71') }.to raise_error(Faraday::ConnectionFailed, /Connection to .* failed/) end it 'converts request data to JSON' do - client = Adyen::Client.new(api_key: 'test_key', env: :test) - mock_faraday_connection = double(Faraday::Connection) 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) - allow(mock_faraday_connection).to receive(:adapter) - allow(mock_faraday_connection).to receive_message_chain(:headers, :[]=) request_body_sent = nil allow(mock_faraday_connection).to receive(:post) do |&block| @@ -562,4 +557,133 @@ expect(request_body_sent).to eq(hash_data.to_json) 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 From 4b03d8fefb113b38b2dce2c7df2b9778c20eb6ec Mon Sep 17 00:00:00 2001 From: Hugo Vacher Date: Tue, 28 Oct 2025 12:31:55 -0400 Subject: [PATCH 3/3] Remove invalid service and simplify authentication validation The service specific validation for `PaymentSetupAndVerification` was useless as this service is not a valid service in `service_url_base`. Removing it means that we where able to simplify the validation as it is not service dependent anymore. --- lib/adyen/client.rb | 12 +++--------- spec/client_spec.rb | 8 ++++++++ 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/lib/adyen/client.rb b/lib/adyen/client.rb index dbb43ec..3aa0434 100644 --- a/lib/adyen/client.rb +++ b/lib/adyen/client.rb @@ -145,9 +145,7 @@ def call_adyen_api(service, action, request_data, headers, version, _with_applic # get URL for requested endpoint url = service_url(service, action.is_a?(String) ? action : action.fetch(:url), version) - # make sure valid authentication has been provided - validate_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) @@ -156,7 +154,7 @@ def call_adyen_api(service, action, request_data, headers, version, _with_applic # 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_type(nil, request_data) + validate_auth(request_data) # initialize Faraday connection object conn = Faraday.new(url, @connection_options) do |faraday| @@ -343,7 +341,7 @@ def auth_type "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( @@ -351,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 bfcb0d4..357462c 100644 --- a/spec/client_spec.rb +++ b/spec/client_spec.rb @@ -556,6 +556,14 @@ 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