From e4c8d4b3c18648e0fe8ba5d1e8c0aa59b63a96cf Mon Sep 17 00:00:00 2001 From: adrian-y1 <80251505+adrian-y1@users.noreply.github.com> Date: Thu, 27 Aug 2026 10:23:18 +1000 Subject: [PATCH 1/3] update client and add auto refresh service and specs --- .github/workflows/main.yml | 4 +- lib/airwallex/client.rb | 23 +-- lib/airwallex/middleware/auth_refresh.rb | 28 +++- .../airwallex/middleware/auth_refresh_spec.rb | 138 ++++++++++++++++++ 4 files changed, 175 insertions(+), 18 deletions(-) create mode 100644 spec/airwallex/middleware/auth_refresh_spec.rb diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 4c1f64d..533434c 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -3,7 +3,7 @@ name: Ruby on: push: branches: - - master + - main pull_request: @@ -14,7 +14,7 @@ jobs: strategy: matrix: ruby: - - '3.3.4' + - "3.3.4" steps: - uses: actions/checkout@v4 diff --git a/lib/airwallex/client.rb b/lib/airwallex/client.rb index b96c61a..860ad08 100644 --- a/lib/airwallex/client.rb +++ b/lib/airwallex/client.rb @@ -7,6 +7,8 @@ module Airwallex class Client + LOGIN_PATH = "/api/v1/authentication/login" + attr_reader :config, :access_token, :token_expires_at def initialize(config = Airwallex.configuration) @@ -19,11 +21,7 @@ def initialize(config = Airwallex.configuration) def connection @connection ||= Faraday.new(url: config.api_url) do |conn| - conn.request :json - conn.request :multipart - conn.request :retry, retry_options - conn.response :json, content_type: /\bjson$/ - conn.response :logger, config.logger, { headers: true, bodies: true } if config.logger + configure_middleware(conn) conn.headers["Content-Type"] = "application/json" conn.headers["User-Agent"] = user_agent @@ -55,7 +53,7 @@ def delete(path, params = {}, headers = {}) def authenticate! @token_mutex.synchronize do - response = connection.post("/api/v1/authentication/login") do |req| + response = connection.post(LOGIN_PATH) do |req| req.headers["x-client-id"] = config.client_id req.headers["x-api-key"] = config.api_key req.headers.delete("Authorization") @@ -85,12 +83,9 @@ def ensure_authenticated! private def request(method, path, data, headers) - ensure_authenticated! - response = connection.public_send(method) do |req| req.url(path) req.headers.merge!(headers) - req.headers["Authorization"] = "Bearer #{access_token}" case method when :get, :delete @@ -104,6 +99,16 @@ def request(method, path, data, headers) response.body end + def configure_middleware(conn) + conn.use Airwallex::Middleware::Idempotency + conn.request :json + conn.request :multipart + conn.request :retry, retry_options + conn.use Airwallex::Middleware::AuthRefresh, self + conn.response :json, content_type: /\bjson$/ + conn.response :logger, config.logger, { headers: true, bodies: true } if config.logger + end + def handle_response_errors(response) return if response.success? diff --git a/lib/airwallex/middleware/auth_refresh.rb b/lib/airwallex/middleware/auth_refresh.rb index d3f47e8..4da583d 100644 --- a/lib/airwallex/middleware/auth_refresh.rb +++ b/lib/airwallex/middleware/auth_refresh.rb @@ -9,24 +9,38 @@ def initialize(app, client) end def call(env) - # Skip authentication refresh for login endpoint - return @app.call(env) if env[:url].path.include?("/authentication/login") + # Skip authentication entirely for the login endpoint itself + return @app.call(env) if login_request?(env) - # Ensure token is valid before making request - @client.ensure_authenticated! unless env[:url].path.include?("/authentication/") + # Ensure token is valid before making the request, then attach it + @client.ensure_authenticated! unless authentication_request?(env) + authorize!(env) response = @app.call(env) # If we get a 401, try refreshing the token and retrying once - if response.status == 401 && !env[:request].fetch(:auth_retry, false) + if response.status == 401 @client.authenticate! - env[:request][:auth_retry] = true - env[:request_headers]["Authorization"] = "Bearer #{@client.access_token}" + authorize!(env) response = @app.call(env) end response end + + private + + def login_request?(env) + env[:url].path.include?(Client::LOGIN_PATH) + end + + def authentication_request?(env) + env[:url].path.include?("/authentication/") + end + + def authorize!(env) + env[:request_headers]["Authorization"] = "Bearer #{@client.access_token}" + end end end end diff --git a/spec/airwallex/middleware/auth_refresh_spec.rb b/spec/airwallex/middleware/auth_refresh_spec.rb new file mode 100644 index 0000000..533d9bd --- /dev/null +++ b/spec/airwallex/middleware/auth_refresh_spec.rb @@ -0,0 +1,138 @@ +# frozen_string_literal: true + +RSpec.describe Airwallex::Middleware::AuthRefresh do + let(:app) { double("app") } + + let(:client) do + Class.new do + attr_reader :access_token + + def initialize + @access_token = "token_initial" + @ensure_authenticated_calls = 0 + @authenticate_calls = 0 + end + + def ensure_authenticated! + @ensure_authenticated_calls += 1 + end + + def authenticate! + @authenticate_calls += 1 + @access_token = "token_refreshed" + end + + def ensure_authenticated_calls + @ensure_authenticated_calls + end + + def authenticate_calls + @authenticate_calls + end + end.new + end + + let(:middleware) { described_class.new(app, client) } + + def env_for(path) + { url: URI("https://api-demo.airwallex.com#{path}"), request_headers: {} } + end + + def response_double(status) + double("response", status: status) + end + + describe "#call" do + context "with the login endpoint" do + let(:env) { env_for("/api/v1/authentication/login") } + + it "does not ensure authentication or set the Authorization header" do + allow(app).to receive(:call).and_return(response_double(200)) + + middleware.call(env) + + expect(client.ensure_authenticated_calls).to eq(0) + expect(env[:request_headers]).not_to have_key("Authorization") + end + + it "passes the request straight through" do + expect(app).to receive(:call).with(env).and_return(response_double(200)) + + middleware.call(env) + end + end + + context "with a non-authentication endpoint" do + let(:env) { env_for("/api/v1/pa/payment_intents/create") } + + it "ensures the client is authenticated before the request" do + allow(app).to receive(:call).and_return(response_double(200)) + + middleware.call(env) + + expect(client.ensure_authenticated_calls).to eq(1) + end + + it "sets the Authorization header from the client's access token" do + expect(app).to receive(:call) do |passed_env| + expect(passed_env[:request_headers]["Authorization"]).to eq("Bearer token_initial") + response_double(200) + end + + middleware.call(env) + end + + it "does not re-authenticate on a successful response" do + allow(app).to receive(:call).and_return(response_double(200)) + + middleware.call(env) + + expect(client.authenticate_calls).to eq(0) + end + + it "re-authenticates and retries once on a 401 response" do + call_count = 0 + allow(app).to receive(:call) do |passed_env| + call_count += 1 + if call_count == 1 + response_double(401) + else + expect(passed_env[:request_headers]["Authorization"]).to eq("Bearer token_refreshed") + response_double(200) + end + end + + response = middleware.call(env) + + expect(client.authenticate_calls).to eq(1) + expect(app).to have_received(:call).twice + expect(response.status).to eq(200) + end + + it "does not retry more than once if the retried request also returns 401" do + allow(app).to receive(:call).and_return(response_double(401)) + + response = middleware.call(env) + + expect(client.authenticate_calls).to eq(1) + expect(app).to have_received(:call).twice + expect(response.status).to eq(401) + end + end + + context "with an authentication endpoint that is not login" do + let(:env) { env_for("/api/v1/authentication/refresh") } + + it "does not call ensure_authenticated! but still sets the Authorization header" do + expect(app).to receive(:call) do |passed_env| + expect(passed_env[:request_headers]["Authorization"]).to eq("Bearer token_initial") + response_double(200) + end + + middleware.call(env) + + expect(client.ensure_authenticated_calls).to eq(0) + end + end + end +end From ffe8fcbef372c039b0696ba065e1cbacaf7a1e10 Mon Sep 17 00:00:00 2001 From: adrian-y1 <80251505+adrian-y1@users.noreply.github.com> Date: Thu, 27 Aug 2026 10:23:39 +1000 Subject: [PATCH 2/3] Fix middleware registration and improve CI configuration - Ensure `Idempotency` and `AuthRefresh` middleware are properly registered in the Faraday connection, enabling automatic `request_id` generation and 401 retry behavior. - Corrected a bug in `AuthRefresh` related to the 401-retry guard. - Update CI configuration to trigger on pushes to `main` instead of `master`. - Refactor `Client#request` to delegate `Authorization` header management to the `AuthRefresh` middleware. - Update dependencies in `Gemfile.lock` for improved compatibility and performance. --- CHANGELOG.md | 12 +++ Gemfile.lock | 77 ++++++++++--------- spec/airwallex/api_operations/create_spec.rb | 15 +--- spec/airwallex/api_operations/delete_spec.rb | 15 +--- spec/airwallex/api_operations/list_spec.rb | 19 +---- .../airwallex/api_operations/retrieve_spec.rb | 15 +--- spec/airwallex/api_operations/update_spec.rb | 21 ++--- spec/airwallex/api_resource_spec.rb | 9 +-- spec/airwallex/client_spec.rb | 24 ++---- spec/airwallex/configuration_spec.rb | 2 +- spec/airwallex/list_object_spec.rb | 24 +----- .../airwallex/middleware/auth_refresh_spec.rb | 4 +- spec/airwallex/resources/balance_spec.rb | 22 ++---- .../resources/batch_transfer_spec.rb | 33 +++----- spec/airwallex/resources/beneficiary_spec.rb | 24 ++---- spec/airwallex/resources/conversion_spec.rb | 28 +++---- spec/airwallex/resources/customer_spec.rb | 36 +++------ spec/airwallex/resources/dispute_spec.rb | 49 +++++------- .../resources/payment_intent_spec.rb | 34 +++----- .../resources/payment_method_spec.rb | 34 +++----- spec/airwallex/resources/quote_spec.rb | 20 ++--- spec/airwallex/resources/rate_spec.rb | 20 ++--- spec/airwallex/resources/transfer_spec.rb | 24 ++---- spec/spec_helper.rb | 6 ++ spec/support/airwallex_test_helpers.rb | 21 +++++ 25 files changed, 213 insertions(+), 375 deletions(-) create mode 100644 spec/support/airwallex_test_helpers.rb diff --git a/CHANGELOG.md b/CHANGELOG.md index 7b682f6..83a7a1c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,17 @@ ## [Unreleased] +### Fixed +- `Idempotency` and `AuthRefresh` middleware are now actually registered on the Faraday connection. + Previously both classes existed but were never wired in, so the "automatic `request_id` generation" + and "retry once after a 401" behavior documented in the README did not happen at runtime. +- Fixed a bug in `AuthRefresh` where the 401-retry guard used `env[:request][:auth_retry]`, a key that + doesn't exist on `Faraday::RequestOptions` and would have raised `NoMethodError` the first time it ran. +- CI now triggers on pushes to `main` (previously configured for `master`, so pushes never ran CI). + +### Changed +- `Client#request` no longer manually manages the `Authorization` header or calls + `ensure_authenticated!` directly; this is now owned by the `AuthRefresh` middleware. + ## [0.3.0] - 2025-11-25 ### Added diff --git a/Gemfile.lock b/Gemfile.lock index 0a356c4..e0d6a0a 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -9,61 +9,63 @@ PATH GEM remote: https://rubygems.org/ specs: - addressable (2.8.7) - public_suffix (>= 2.0.2, < 7.0) + addressable (2.9.0) + public_suffix (>= 2.0.2, < 8.0) ast (2.4.3) - bigdecimal (3.3.1) + bigdecimal (4.1.2) crack (1.0.1) bigdecimal rexml - date (3.5.0) diff-lcs (1.6.2) docile (1.4.1) - erb (6.0.0) - faraday (2.14.0) + erb (6.0.7) + faraday (2.14.3) faraday-net_http (>= 2.0, < 3.5) json logger - faraday-multipart (1.1.1) + faraday-multipart (1.2.0) multipart-post (~> 2.0) - faraday-net_http (3.4.2) + faraday-net_http (3.4.4) net-http (~> 0.5) - faraday-retry (2.3.2) + faraday-retry (2.4.0) faraday (~> 2.0) hashdiff (1.2.1) - io-console (0.8.1) - irb (1.15.3) + io-console (0.9.2) + irb (1.18.0) pp (>= 0.6.0) + prism (>= 1.3.0) rdoc (>= 4.0.0) reline (>= 0.4.2) - json (2.16.0) - language_server-protocol (3.17.0.5) + json (2.21.2) + language_server-protocol (3.17.0.6) lint_roller (1.1.0) logger (1.7.0) multipart-post (2.4.1) - net-http (0.8.0) + net-http (0.9.1) uri (>= 0.11.1) - parallel (1.27.0) - parser (3.3.10.0) + parallel (1.28.0) + parser (3.3.12.0) ast (~> 2.4.1) racc - pp (0.6.3) + pp (0.6.4) prettyprint prettyprint (0.2.0) - prism (1.6.0) - psych (5.2.6) - date - stringio - public_suffix (6.0.2) + prism (1.9.0) + public_suffix (7.0.5) racc (1.8.1) rainbow (3.1.1) - rake (13.3.1) - rdoc (6.15.1) + rake (13.4.2) + rbs (4.1.3) + logger + prism (>= 1.6.0) + tsort + rdoc (8.0.0) erb - psych (>= 4.0.0) + prism (>= 1.6.0) + rbs (>= 4.0.0) tsort - regexp_parser (2.11.3) - reline (0.6.3) + regexp_parser (2.12.0) + reline (0.7.0) io-console (~> 0.5) rexml (3.4.4) rspec (3.13.2) @@ -75,24 +77,24 @@ GEM rspec-expectations (3.13.5) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.13.0) - rspec-mocks (3.13.7) + rspec-mocks (3.13.8) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.13.0) - rspec-support (3.13.6) - rubocop (1.81.7) - json (~> 2.3) + rspec-support (3.13.7) + rubocop (1.90.0) + json (>= 2.3) language_server-protocol (~> 3.17.0.2) lint_roller (~> 1.1.0) - parallel (~> 1.10) + parallel (>= 1.10) parser (>= 3.3.0.2) rainbow (>= 2.2.2, < 4.0) regexp_parser (>= 2.9.3, < 3.0) - rubocop-ast (>= 1.47.1, < 2.0) + rubocop-ast (>= 1.49.0, < 2.0) ruby-progressbar (~> 1.7) unicode-display_width (>= 2.4.0, < 4.0) - rubocop-ast (1.48.0) + rubocop-ast (1.50.0) parser (>= 3.3.7.2) - prism (~> 1.4) + prism (~> 1.7) ruby-progressbar (1.13.0) simplecov (0.22.0) docile (~> 1.1) @@ -100,13 +102,12 @@ GEM simplecov_json_formatter (~> 0.1) simplecov-html (0.13.2) simplecov_json_formatter (0.1.4) - stringio (3.1.8) tsort (0.2.0) unicode-display_width (3.2.0) unicode-emoji (~> 4.1) - unicode-emoji (4.1.0) + unicode-emoji (4.2.0) uri (1.1.1) - webmock (3.26.1) + webmock (3.26.3) addressable (>= 2.8.0) crack (>= 0.3.2) hashdiff (>= 0.4.0, < 2.0.0) diff --git a/spec/airwallex/api_operations/create_spec.rb b/spec/airwallex/api_operations/create_spec.rb index 832eec3..d7bb296 100644 --- a/spec/airwallex/api_operations/create_spec.rb +++ b/spec/airwallex/api_operations/create_spec.rb @@ -13,18 +13,7 @@ def self.resource_path end end - let(:auth_response) do - { - status: 200, - body: { token: "test_token" }.to_json, - headers: { "Content-Type" => "application/json" } - } - end - before do - stub_request(:post, "https://api-demo.airwallex.com/api/v1/authentication/login") - .to_return(auth_response) - stub_const("TestResource", test_class) end @@ -46,7 +35,7 @@ def self.resource_path end before do - stub_request(:post, "https://api-demo.airwallex.com/api/v1/test_resources/create") + stub_request(:post, "#{BASE_URL}/api/v1/test_resources/create") .with(body: hash_including(create_params)) .to_return( status: 200, @@ -67,7 +56,7 @@ def self.resource_path it "sends POST request to create endpoint" do TestResource.create(create_params) - expect(WebMock).to have_requested(:post, "https://api-demo.airwallex.com/api/v1/test_resources/create") + expect(WebMock).to have_requested(:post, "#{BASE_URL}/api/v1/test_resources/create") .with(body: hash_including(create_params)) end diff --git a/spec/airwallex/api_operations/delete_spec.rb b/spec/airwallex/api_operations/delete_spec.rb index 392ab7d..fc11130 100644 --- a/spec/airwallex/api_operations/delete_spec.rb +++ b/spec/airwallex/api_operations/delete_spec.rb @@ -13,24 +13,13 @@ def self.resource_path end end - let(:auth_response) do - { - status: 200, - body: { token: "test_token" }.to_json, - headers: { "Content-Type" => "application/json" } - } - end - before do - stub_request(:post, "https://api-demo.airwallex.com/api/v1/authentication/login") - .to_return(auth_response) - stub_const("TestResource", test_class) end describe ".delete" do before do - stub_request(:delete, "https://api-demo.airwallex.com/api/v1/test_resources/test_123") + stub_request(:delete, "#{BASE_URL}/api/v1/test_resources/test_123") .to_return( status: 200, body: {}.to_json, @@ -47,7 +36,7 @@ def self.resource_path it "sends DELETE request" do TestResource.delete("test_123") - expect(WebMock).to have_requested(:delete, "https://api-demo.airwallex.com/api/v1/test_resources/test_123") + expect(WebMock).to have_requested(:delete, "#{BASE_URL}/api/v1/test_resources/test_123") end end end diff --git a/spec/airwallex/api_operations/list_spec.rb b/spec/airwallex/api_operations/list_spec.rb index 0638332..c215451 100644 --- a/spec/airwallex/api_operations/list_spec.rb +++ b/spec/airwallex/api_operations/list_spec.rb @@ -13,18 +13,7 @@ def self.resource_path end end - let(:auth_response) do - { - status: 200, - body: { token: "test_token" }.to_json, - headers: { "Content-Type" => "application/json" } - } - end - before do - stub_request(:post, "https://api-demo.airwallex.com/api/v1/authentication/login") - .to_return(auth_response) - stub_const("TestResource", test_class) end @@ -40,7 +29,7 @@ def self.resource_path end before do - stub_request(:get, "https://api-demo.airwallex.com/api/v1/test_resources") + stub_request(:get, "#{BASE_URL}/api/v1/test_resources") .with(query: { page_size: 10 }) .to_return( status: 200, @@ -67,12 +56,12 @@ def self.resource_path it "sends GET request with query params" do TestResource.list(page_size: 10) - expect(WebMock).to have_requested(:get, "https://api-demo.airwallex.com/api/v1/test_resources") + expect(WebMock).to have_requested(:get, "#{BASE_URL}/api/v1/test_resources") .with(query: { page_size: 10 }) end it "passes filters to API" do - stub_request(:get, "https://api-demo.airwallex.com/api/v1/test_resources") + stub_request(:get, "#{BASE_URL}/api/v1/test_resources") .with(query: { page_size: 20, status: "active" }) .to_return( status: 200, @@ -82,7 +71,7 @@ def self.resource_path TestResource.list(page_size: 20, status: "active") - expect(WebMock).to have_requested(:get, "https://api-demo.airwallex.com/api/v1/test_resources") + expect(WebMock).to have_requested(:get, "#{BASE_URL}/api/v1/test_resources") .with(query: { page_size: 20, status: "active" }) end end diff --git a/spec/airwallex/api_operations/retrieve_spec.rb b/spec/airwallex/api_operations/retrieve_spec.rb index 5ea2193..68bcde6 100644 --- a/spec/airwallex/api_operations/retrieve_spec.rb +++ b/spec/airwallex/api_operations/retrieve_spec.rb @@ -13,18 +13,7 @@ def self.resource_path end end - let(:auth_response) do - { - status: 200, - body: { token: "test_token" }.to_json, - headers: { "Content-Type" => "application/json" } - } - end - before do - stub_request(:post, "https://api-demo.airwallex.com/api/v1/authentication/login") - .to_return(auth_response) - stub_const("TestResource", test_class) end @@ -38,7 +27,7 @@ def self.resource_path end before do - stub_request(:get, "https://api-demo.airwallex.com/api/v1/test_resources/test_123") + stub_request(:get, "#{BASE_URL}/api/v1/test_resources/test_123") .to_return( status: 200, body: retrieve_response.to_json, @@ -58,7 +47,7 @@ def self.resource_path it "sends GET request to resource endpoint" do TestResource.retrieve("test_123") - expect(WebMock).to have_requested(:get, "https://api-demo.airwallex.com/api/v1/test_resources/test_123") + expect(WebMock).to have_requested(:get, "#{BASE_URL}/api/v1/test_resources/test_123") end it "returns instance of calling class" do diff --git a/spec/airwallex/api_operations/update_spec.rb b/spec/airwallex/api_operations/update_spec.rb index 52f9609..9c62daa 100644 --- a/spec/airwallex/api_operations/update_spec.rb +++ b/spec/airwallex/api_operations/update_spec.rb @@ -14,18 +14,7 @@ def self.resource_path end end - let(:auth_response) do - { - status: 200, - body: { token: "test_token" }.to_json, - headers: { "Content-Type" => "application/json" } - } - end - before do - stub_request(:post, "https://api-demo.airwallex.com/api/v1/authentication/login") - .to_return(auth_response) - stub_const("TestResource", test_class) end @@ -46,7 +35,7 @@ def self.resource_path end before do - stub_request(:put, "https://api-demo.airwallex.com/api/v1/test_resources/test_123") + stub_request(:put, "#{BASE_URL}/api/v1/test_resources/test_123") .with(body: hash_including(update_params)) .to_return( status: 200, @@ -67,7 +56,7 @@ def self.resource_path it "sends PUT request with params" do TestResource.update("test_123", update_params) - expect(WebMock).to have_requested(:put, "https://api-demo.airwallex.com/api/v1/test_resources/test_123") + expect(WebMock).to have_requested(:put, "#{BASE_URL}/api/v1/test_resources/test_123") .with(body: hash_including(update_params)) end end @@ -90,7 +79,7 @@ def self.resource_path end before do - stub_request(:put, "https://api-demo.airwallex.com/api/v1/test_resources/test_123") + stub_request(:put, "#{BASE_URL}/api/v1/test_resources/test_123") .to_return( status: 200, body: update_response.to_json, @@ -131,7 +120,7 @@ def self.resource_path context "when attributes changed" do before do - stub_request(:put, "https://api-demo.airwallex.com/api/v1/test_resources/test_123") + stub_request(:put, "#{BASE_URL}/api/v1/test_resources/test_123") .to_return( status: 200, body: update_response.to_json, @@ -152,7 +141,7 @@ def self.resource_path resource.name = "Updated Name" resource.save - expect(WebMock).to have_requested(:put, "https://api-demo.airwallex.com/api/v1/test_resources/test_123") + expect(WebMock).to have_requested(:put, "#{BASE_URL}/api/v1/test_resources/test_123") .with(body: hash_including(name: "Updated Name")) end diff --git a/spec/airwallex/api_resource_spec.rb b/spec/airwallex/api_resource_spec.rb index ada0bda..aba423e 100644 --- a/spec/airwallex/api_resource_spec.rb +++ b/spec/airwallex/api_resource_spec.rb @@ -129,14 +129,7 @@ def self.name let(:resource) { test_class.new(attributes) } before do - stub_request(:post, "https://api-demo.airwallex.com/api/v1/authentication/login") - .to_return( - status: 200, - body: { token: "test_token" }.to_json, - headers: { "Content-Type" => "application/json" } - ) - - stub_request(:get, "https://api-demo.airwallex.com/api/v1/test_resources/test_123") + stub_request(:get, "#{BASE_URL}/api/v1/test_resources/test_123") .to_return( status: 200, body: { id: "test_123", name: "Updated Name", amount: 200 }.to_json, diff --git a/spec/airwallex/client_spec.rb b/spec/airwallex/client_spec.rb index e77a087..82ac73e 100644 --- a/spec/airwallex/client_spec.rb +++ b/spec/airwallex/client_spec.rb @@ -31,10 +31,7 @@ } end - before do - stub_request(:post, "https://api-demo.airwallex.com/api/v1/authentication/login") - .to_return(success_response) - end + before { stub_login(token: "test_token_123") } it "exchanges credentials for access token" do token = client.authenticate! @@ -53,7 +50,7 @@ end it "sends correct headers" do - stub = stub_request(:post, "https://api-demo.airwallex.com/api/v1/authentication/login") + stub = stub_request(:post, "#{BASE_URL}#{LOGIN_PATH}") .with( headers: { "x-client-id" => "test_client_id", @@ -76,7 +73,7 @@ end before do - stub_request(:post, "https://api-demo.airwallex.com/api/v1/authentication/login") + stub_request(:post, "#{BASE_URL}#{LOGIN_PATH}") .to_return(error_response) end @@ -105,18 +102,7 @@ end describe "#ensure_authenticated!" do - let(:success_response) do - { - status: 200, - body: { token: "test_token_123", expires_at: (Time.now + 1800).to_i }.to_json, - headers: { "Content-Type" => "application/json" } - } - end - - before do - stub_request(:post, "https://api-demo.airwallex.com/api/v1/authentication/login") - .to_return(success_response) - end + before { stub_login(token: "test_token_123") } it "authenticates when token is expired" do expect(client.access_token).to be_nil @@ -139,7 +125,7 @@ end it "sets correct base URL" do - expect(client.connection.url_prefix.to_s).to eq("https://api-demo.airwallex.com/") + expect(client.connection.url_prefix.to_s).to eq("#{BASE_URL}/") end it "includes required headers" do diff --git a/spec/airwallex/configuration_spec.rb b/spec/airwallex/configuration_spec.rb index ec7abe6..1ba9154 100644 --- a/spec/airwallex/configuration_spec.rb +++ b/spec/airwallex/configuration_spec.rb @@ -41,7 +41,7 @@ before { config.environment = :sandbox } it "returns the sandbox API URL" do - expect(config.api_url).to eq("https://api-demo.airwallex.com") + expect(config.api_url).to eq(BASE_URL) end end diff --git a/spec/airwallex/list_object_spec.rb b/spec/airwallex/list_object_spec.rb index f1e0189..11a691b 100644 --- a/spec/airwallex/list_object_spec.rb +++ b/spec/airwallex/list_object_spec.rb @@ -144,18 +144,9 @@ def self.resource_path end describe "#next_page" do - before do - stub_request(:post, "https://api-demo.airwallex.com/api/v1/authentication/login") - .to_return( - status: 200, - body: { token: "test_token" }.to_json, - headers: { "Content-Type" => "application/json" } - ) - end - context "with cursor pagination" do it "fetches next page using cursor" do - stub_request(:get, "https://api-demo.airwallex.com/api/v1/test_resources") + stub_request(:get, "#{BASE_URL}/api/v1/test_resources") .with(query: { next_cursor: "cursor_123", page_size: 10 }) .to_return( status: 200, @@ -186,7 +177,7 @@ def self.resource_path context "with offset pagination" do it "fetches next page using offset" do - stub_request(:get, "https://api-demo.airwallex.com/api/v1/test_resources") + stub_request(:get, "#{BASE_URL}/api/v1/test_resources") .with(query: { offset: 20, page_size: 20 }) .to_return( status: 200, @@ -227,14 +218,7 @@ def self.resource_path describe "#auto_paging_each" do before do - stub_request(:post, "https://api-demo.airwallex.com/api/v1/authentication/login") - .to_return( - status: 200, - body: { token: "test_token" }.to_json, - headers: { "Content-Type" => "application/json" } - ) - - stub_request(:get, "https://api-demo.airwallex.com/api/v1/test_resources") + stub_request(:get, "#{BASE_URL}/api/v1/test_resources") .with(query: { page_size: 2 }) .to_return( status: 200, @@ -246,7 +230,7 @@ def self.resource_path headers: { "Content-Type" => "application/json" } ) - stub_request(:get, "https://api-demo.airwallex.com/api/v1/test_resources") + stub_request(:get, "#{BASE_URL}/api/v1/test_resources") .with(query: { page_size: 2, next_cursor: "cursor_1" }) .to_return( status: 200, diff --git a/spec/airwallex/middleware/auth_refresh_spec.rb b/spec/airwallex/middleware/auth_refresh_spec.rb index 533d9bd..f82d818 100644 --- a/spec/airwallex/middleware/auth_refresh_spec.rb +++ b/spec/airwallex/middleware/auth_refresh_spec.rb @@ -35,7 +35,7 @@ def authenticate_calls let(:middleware) { described_class.new(app, client) } def env_for(path) - { url: URI("https://api-demo.airwallex.com#{path}"), request_headers: {} } + { url: URI("#{BASE_URL}#{path}"), request_headers: {} } end def response_double(status) @@ -44,7 +44,7 @@ def response_double(status) describe "#call" do context "with the login endpoint" do - let(:env) { env_for("/api/v1/authentication/login") } + let(:env) { env_for(LOGIN_PATH) } it "does not ensure authentication or set the Authorization header" do allow(app).to receive(:call).and_return(response_double(200)) diff --git a/spec/airwallex/resources/balance_spec.rb b/spec/airwallex/resources/balance_spec.rb index 120d488..4037927 100644 --- a/spec/airwallex/resources/balance_spec.rb +++ b/spec/airwallex/resources/balance_spec.rb @@ -3,18 +3,10 @@ require "spec_helper" RSpec.describe Airwallex::Balance do - before do - stub_request(:post, "https://api-demo.airwallex.com/api/v1/authentication/login") - .to_return( - status: 200, - body: { token: "test_token", expires_at: (Time.now + 3600).iso8601 }.to_json, - headers: { "Content-Type" => "application/json" } - ) - end describe ".list" do it "lists all balances" do - stub_request(:get, "https://api-demo.airwallex.com/api/v1/balances/current") + stub_request(:get, "#{BASE_URL}/api/v1/balances/current") .to_return( status: 200, body: { @@ -56,7 +48,7 @@ end it "filters balances by currency" do - stub_request(:get, "https://api-demo.airwallex.com/api/v1/balances/current") + stub_request(:get, "#{BASE_URL}/api/v1/balances/current") .with(query: hash_including(currency: "USD")) .to_return( status: 200, @@ -82,7 +74,7 @@ describe ".retrieve" do it "retrieves balance for specific currency" do - stub_request(:get, "https://api-demo.airwallex.com/api/v1/balances/current") + stub_request(:get, "#{BASE_URL}/api/v1/balances/current") .with(query: { currency: "USD" }) .to_return( status: 200, @@ -109,7 +101,7 @@ end it "retrieves balance with zero amounts" do - stub_request(:get, "https://api-demo.airwallex.com/api/v1/balances/current") + stub_request(:get, "#{BASE_URL}/api/v1/balances/current") .with(query: { currency: "JPY" }) .to_return( status: 200, @@ -167,7 +159,7 @@ describe "error handling" do it "handles invalid currency code" do - stub_request(:get, "https://api-demo.airwallex.com/api/v1/balances/current") + stub_request(:get, "#{BASE_URL}/api/v1/balances/current") .with(query: { currency: "XXX" }) .to_return( status: 400, @@ -184,7 +176,7 @@ end it "handles currency not found" do - stub_request(:get, "https://api-demo.airwallex.com/api/v1/balances/current") + stub_request(:get, "#{BASE_URL}/api/v1/balances/current") .with(query: { currency: "AUD" }) .to_return( status: 200, @@ -201,7 +193,7 @@ end it "handles SCA required error" do - stub_request(:get, "https://api-demo.airwallex.com/api/v1/balances/current") + stub_request(:get, "#{BASE_URL}/api/v1/balances/current") .to_return( status: 400, body: { diff --git a/spec/airwallex/resources/batch_transfer_spec.rb b/spec/airwallex/resources/batch_transfer_spec.rb index 0a5eb4d..9a4d33c 100644 --- a/spec/airwallex/resources/batch_transfer_spec.rb +++ b/spec/airwallex/resources/batch_transfer_spec.rb @@ -5,18 +5,9 @@ RSpec.describe Airwallex::BatchTransfer do let(:client) { Airwallex.client } - before do - stub_request(:post, "https://api-demo.airwallex.com/api/v1/authentication/login") - .to_return( - status: 200, - body: { token: "test_token", expires_at: (Time.now + 3600).iso8601 }.to_json, - headers: { "Content-Type" => "application/json" } - ) - end - describe ".create" do it "creates a batch transfer with multiple transfers" do - stub_request(:post, "https://api-demo.airwallex.com/api/v1/batch_transfers/create") + stub_request(:post, "#{BASE_URL}/api/v1/batch_transfers/create") .to_return( status: 200, body: { @@ -63,7 +54,7 @@ end it "creates a batch transfer with single transfer" do - stub_request(:post, "https://api-demo.airwallex.com/api/v1/batch_transfers/create") + stub_request(:post, "#{BASE_URL}/api/v1/batch_transfers/create") .to_return( status: 200, body: { @@ -95,7 +86,7 @@ it "handles idempotency with request_id" do request_id = "idempotent_batch_#{Time.now.to_i}" - stub_request(:post, "https://api-demo.airwallex.com/api/v1/batch_transfers/create") + stub_request(:post, "#{BASE_URL}/api/v1/batch_transfers/create") .with(body: hash_including(request_id: request_id)) .to_return( status: 200, @@ -126,7 +117,7 @@ describe ".retrieve" do it "retrieves a batch transfer by ID" do - stub_request(:get, "https://api-demo.airwallex.com/api/v1/batch_transfers/batch_retrieve_123") + stub_request(:get, "#{BASE_URL}/api/v1/batch_transfers/batch_retrieve_123") .to_return( status: 200, body: { @@ -157,7 +148,7 @@ end it "shows individual transfer statuses" do - stub_request(:get, "https://api-demo.airwallex.com/api/v1/batch_transfers/batch_mixed") + stub_request(:get, "#{BASE_URL}/api/v1/batch_transfers/batch_mixed") .to_return( status: 200, body: { @@ -182,7 +173,7 @@ describe ".list" do it "lists batch transfers with pagination" do - stub_request(:get, "https://api-demo.airwallex.com/api/v1/batch_transfers") + stub_request(:get, "#{BASE_URL}/api/v1/batch_transfers") .with(query: hash_including(page_size: "10")) .to_return( status: 200, @@ -206,7 +197,7 @@ end it "filters batch transfers by status" do - stub_request(:get, "https://api-demo.airwallex.com/api/v1/batch_transfers") + stub_request(:get, "#{BASE_URL}/api/v1/batch_transfers") .with(query: hash_including(status: "COMPLETED")) .to_return( status: 200, @@ -227,7 +218,7 @@ end it "filters by date range" do - stub_request(:get, "https://api-demo.airwallex.com/api/v1/batch_transfers") + stub_request(:get, "#{BASE_URL}/api/v1/batch_transfers") .with(query: hash_including( from_created_at: "2025-11-01T00:00:00Z", to_created_at: "2025-11-30T23:59:59Z" @@ -252,7 +243,7 @@ end it "supports auto-paging" do - stub_request(:get, "https://api-demo.airwallex.com/api/v1/batch_transfers?page_size=2") + stub_request(:get, "#{BASE_URL}/api/v1/batch_transfers?page_size=2") .to_return( status: 200, body: { @@ -265,7 +256,7 @@ headers: { "Content-Type" => "application/json" } ) - stub_request(:get, "https://api-demo.airwallex.com/api/v1/batch_transfers?offset=2&page_size=2") + stub_request(:get, "#{BASE_URL}/api/v1/batch_transfers?offset=2&page_size=2") .to_return( status: 200, body: { @@ -288,7 +279,7 @@ describe "error handling" do it "handles validation errors" do - stub_request(:post, "https://api-demo.airwallex.com/api/v1/batch_transfers/create") + stub_request(:post, "#{BASE_URL}/api/v1/batch_transfers/create") .to_return( status: 400, body: { @@ -307,7 +298,7 @@ end it "handles insufficient funds error" do - stub_request(:post, "https://api-demo.airwallex.com/api/v1/batch_transfers/create") + stub_request(:post, "#{BASE_URL}/api/v1/batch_transfers/create") .to_return( status: 400, body: { diff --git a/spec/airwallex/resources/beneficiary_spec.rb b/spec/airwallex/resources/beneficiary_spec.rb index 2224bed..1916f82 100644 --- a/spec/airwallex/resources/beneficiary_spec.rb +++ b/spec/airwallex/resources/beneficiary_spec.rb @@ -3,18 +3,6 @@ require "spec_helper" RSpec.describe Airwallex::Beneficiary do - let(:auth_response) do - { - status: 200, - body: { token: "test_token" }.to_json, - headers: { "Content-Type" => "application/json" } - } - end - - before do - stub_request(:post, "https://api-demo.airwallex.com/api/v1/authentication/login") - .to_return(auth_response) - end describe ".resource_path" do it "returns correct path" do @@ -50,7 +38,7 @@ end before do - stub_request(:post, "https://api-demo.airwallex.com/api/v1/beneficiaries/create") + stub_request(:post, "#{BASE_URL}/api/v1/beneficiaries/create") .with(body: hash_including(create_params)) .to_return( status: 200, @@ -71,7 +59,7 @@ it "sends POST request to correct endpoint" do described_class.create(create_params) - expect(WebMock).to have_requested(:post, "https://api-demo.airwallex.com/api/v1/beneficiaries/create") + expect(WebMock).to have_requested(:post, "#{BASE_URL}/api/v1/beneficiaries/create") end end @@ -89,7 +77,7 @@ end before do - stub_request(:get, "https://api-demo.airwallex.com/api/v1/beneficiaries/ben_123") + stub_request(:get, "#{BASE_URL}/api/v1/beneficiaries/ben_123") .to_return( status: 200, body: beneficiary_response.to_json, @@ -118,7 +106,7 @@ end before do - stub_request(:get, "https://api-demo.airwallex.com/api/v1/beneficiaries") + stub_request(:get, "#{BASE_URL}/api/v1/beneficiaries") .with(query: { page_size: 20 }) .to_return( status: 200, @@ -145,7 +133,7 @@ describe ".delete" do before do - stub_request(:delete, "https://api-demo.airwallex.com/api/v1/beneficiaries/ben_123") + stub_request(:delete, "#{BASE_URL}/api/v1/beneficiaries/ben_123") .to_return( status: 200, body: {}.to_json, @@ -162,7 +150,7 @@ it "sends DELETE request to correct endpoint" do described_class.delete("ben_123") - expect(WebMock).to have_requested(:delete, "https://api-demo.airwallex.com/api/v1/beneficiaries/ben_123") + expect(WebMock).to have_requested(:delete, "#{BASE_URL}/api/v1/beneficiaries/ben_123") end end end diff --git a/spec/airwallex/resources/conversion_spec.rb b/spec/airwallex/resources/conversion_spec.rb index f935da9..cf62311 100644 --- a/spec/airwallex/resources/conversion_spec.rb +++ b/spec/airwallex/resources/conversion_spec.rb @@ -3,18 +3,10 @@ require "spec_helper" RSpec.describe Airwallex::Conversion do - before do - stub_request(:post, "https://api-demo.airwallex.com/api/v1/authentication/login") - .to_return( - status: 200, - body: { token: "test_token", expires_at: (Time.now + 3600).iso8601 }.to_json, - headers: { "Content-Type" => "application/json" } - ) - end describe ".create" do it "creates conversion with quote_id" do - stub_request(:post, "https://api-demo.airwallex.com/api/v1/conversions/create") + stub_request(:post, "#{BASE_URL}/api/v1/conversions/create") .with(body: hash_including( quote_id: "quote_123456", request_id: "conv_req_001" @@ -54,7 +46,7 @@ end it "creates conversion at market rate without quote" do - stub_request(:post, "https://api-demo.airwallex.com/api/v1/conversions/create") + stub_request(:post, "#{BASE_URL}/api/v1/conversions/create") .with(body: hash_including( from_currency: "GBP", to_currency: "JPY", @@ -91,7 +83,7 @@ end it "creates conversion with buy_amount" do - stub_request(:post, "https://api-demo.airwallex.com/api/v1/conversions/create") + stub_request(:post, "#{BASE_URL}/api/v1/conversions/create") .with(body: hash_including( from_currency: "USD", to_currency: "EUR", @@ -127,7 +119,7 @@ describe ".retrieve" do it "retrieves existing conversion" do - stub_request(:get, "https://api-demo.airwallex.com/api/v1/conversions/conv_123456") + stub_request(:get, "#{BASE_URL}/api/v1/conversions/conv_123456") .to_return( status: 200, body: { @@ -154,7 +146,7 @@ describe ".list" do it "lists conversions" do - stub_request(:get, "https://api-demo.airwallex.com/api/v1/conversions") + stub_request(:get, "#{BASE_URL}/api/v1/conversions") .to_return( status: 200, body: { @@ -185,7 +177,7 @@ end it "filters by from_currency" do - stub_request(:get, "https://api-demo.airwallex.com/api/v1/conversions") + stub_request(:get, "#{BASE_URL}/api/v1/conversions") .with(query: hash_including(from_currency: "USD")) .to_return( status: 200, @@ -206,7 +198,7 @@ describe "error handling" do it "handles expired quote" do - stub_request(:post, "https://api-demo.airwallex.com/api/v1/conversions/create") + stub_request(:post, "#{BASE_URL}/api/v1/conversions/create") .to_return( status: 400, body: { @@ -225,7 +217,7 @@ end it "handles insufficient funds" do - stub_request(:post, "https://api-demo.airwallex.com/api/v1/conversions/create") + stub_request(:post, "#{BASE_URL}/api/v1/conversions/create") .to_return( status: 400, body: { @@ -246,7 +238,7 @@ end it "handles duplicate request_id" do - stub_request(:post, "https://api-demo.airwallex.com/api/v1/conversions/create") + stub_request(:post, "#{BASE_URL}/api/v1/conversions/create") .to_return( status: 400, body: { @@ -267,7 +259,7 @@ end it "handles conversion not found" do - stub_request(:get, "https://api-demo.airwallex.com/api/v1/conversions/nonexistent") + stub_request(:get, "#{BASE_URL}/api/v1/conversions/nonexistent") .to_return( status: 404, body: { diff --git a/spec/airwallex/resources/customer_spec.rb b/spec/airwallex/resources/customer_spec.rb index 69994ad..7b43594 100644 --- a/spec/airwallex/resources/customer_spec.rb +++ b/spec/airwallex/resources/customer_spec.rb @@ -3,18 +3,6 @@ require "spec_helper" RSpec.describe Airwallex::Customer do - let(:auth_response) do - { - status: 200, - body: { token: "test_token" }.to_json, - headers: { "Content-Type" => "application/json" } - } - end - - before do - stub_request(:post, "https://api-demo.airwallex.com/api/v1/authentication/login") - .to_return(auth_response) - end describe ".resource_path" do it "returns correct path" do @@ -44,7 +32,7 @@ end before do - stub_request(:post, "https://api-demo.airwallex.com/api/v1/pa/customers/create") + stub_request(:post, "#{BASE_URL}/api/v1/pa/customers/create") .with(body: hash_including(email: "john@example.com")) .to_return( status: 200, @@ -66,7 +54,7 @@ it "sends POST request to correct endpoint" do described_class.create(create_params) - expect(WebMock).to have_requested(:post, "https://api-demo.airwallex.com/api/v1/pa/customers/create") + expect(WebMock).to have_requested(:post, "#{BASE_URL}/api/v1/pa/customers/create") end end @@ -81,7 +69,7 @@ end before do - stub_request(:get, "https://api-demo.airwallex.com/api/v1/pa/customers/cus_123") + stub_request(:get, "#{BASE_URL}/api/v1/pa/customers/cus_123") .to_return( status: 200, body: customer_response.to_json, @@ -110,7 +98,7 @@ end before do - stub_request(:get, "https://api-demo.airwallex.com/api/v1/pa/customers") + stub_request(:get, "#{BASE_URL}/api/v1/pa/customers") .with(query: { page_size: 10 }) .to_return( status: 200, @@ -153,7 +141,7 @@ end before do - stub_request(:put, "https://api-demo.airwallex.com/api/v1/pa/customers/cus_123") + stub_request(:put, "#{BASE_URL}/api/v1/pa/customers/cus_123") .to_return( status: 200, body: updated_response.to_json, @@ -188,7 +176,7 @@ end before do - stub_request(:put, "https://api-demo.airwallex.com/api/v1/pa/customers/cus_123") + stub_request(:put, "#{BASE_URL}/api/v1/pa/customers/cus_123") .to_return( status: 200, body: updated_response.to_json, @@ -206,7 +194,7 @@ describe ".delete" do before do - stub_request(:delete, "https://api-demo.airwallex.com/api/v1/pa/customers/cus_123") + stub_request(:delete, "#{BASE_URL}/api/v1/pa/customers/cus_123") .to_return( status: 200, body: {}.to_json, @@ -223,7 +211,7 @@ it "sends DELETE request" do described_class.delete("cus_123") - expect(WebMock).to have_requested(:delete, "https://api-demo.airwallex.com/api/v1/pa/customers/cus_123") + expect(WebMock).to have_requested(:delete, "#{BASE_URL}/api/v1/pa/customers/cus_123") end end @@ -243,7 +231,7 @@ end before do - stub_request(:get, "https://api-demo.airwallex.com/api/v1/pa/payment_methods") + stub_request(:get, "#{BASE_URL}/api/v1/pa/payment_methods") .with(query: { customer_id: "cus_123" }) .to_return( status: 200, @@ -263,12 +251,12 @@ it "passes customer_id to list call" do customer.payment_methods - expect(WebMock).to have_requested(:get, "https://api-demo.airwallex.com/api/v1/pa/payment_methods") + expect(WebMock).to have_requested(:get, "#{BASE_URL}/api/v1/pa/payment_methods") .with(query: hash_including(customer_id: "cus_123")) end it "accepts additional parameters" do - stub_request(:get, "https://api-demo.airwallex.com/api/v1/pa/payment_methods") + stub_request(:get, "#{BASE_URL}/api/v1/pa/payment_methods") .with(query: { customer_id: "cus_123", type: "card" }) .to_return( status: 200, @@ -278,7 +266,7 @@ customer.payment_methods(type: "card") - expect(WebMock).to have_requested(:get, "https://api-demo.airwallex.com/api/v1/pa/payment_methods") + expect(WebMock).to have_requested(:get, "#{BASE_URL}/api/v1/pa/payment_methods") .with(query: { customer_id: "cus_123", type: "card" }) end end diff --git a/spec/airwallex/resources/dispute_spec.rb b/spec/airwallex/resources/dispute_spec.rb index 0615d9b..707f047 100644 --- a/spec/airwallex/resources/dispute_spec.rb +++ b/spec/airwallex/resources/dispute_spec.rb @@ -5,18 +5,9 @@ RSpec.describe Airwallex::Dispute do let(:client) { Airwallex.client } - before do - stub_request(:post, "https://api-demo.airwallex.com/api/v1/authentication/login") - .to_return( - status: 200, - body: { token: "test_token", expires_at: (Time.now + 3600).iso8601 }.to_json, - headers: { "Content-Type" => "application/json" } - ) - end - describe ".retrieve" do it "retrieves a dispute by ID" do - stub_request(:get, "https://api-demo.airwallex.com/api/v1/disputes/dis_123") + stub_request(:get, "#{BASE_URL}/api/v1/disputes/dis_123") .to_return( status: 200, body: { @@ -43,7 +34,7 @@ end it "retrieves dispute with evidence deadline" do - stub_request(:get, "https://api-demo.airwallex.com/api/v1/disputes/dis_urgent") + stub_request(:get, "#{BASE_URL}/api/v1/disputes/dis_urgent") .to_return( status: 200, body: { @@ -64,7 +55,7 @@ describe ".list" do it "lists all disputes" do - stub_request(:get, "https://api-demo.airwallex.com/api/v1/disputes") + stub_request(:get, "#{BASE_URL}/api/v1/disputes") .with(query: hash_including(page_size: "20")) .to_return( status: 200, @@ -87,7 +78,7 @@ end it "filters disputes by status" do - stub_request(:get, "https://api-demo.airwallex.com/api/v1/disputes") + stub_request(:get, "#{BASE_URL}/api/v1/disputes") .with(query: hash_including(status: "OPEN")) .to_return( status: 200, @@ -108,7 +99,7 @@ end it "filters disputes by payment_intent_id" do - stub_request(:get, "https://api-demo.airwallex.com/api/v1/disputes") + stub_request(:get, "#{BASE_URL}/api/v1/disputes") .with(query: hash_including(payment_intent_id: "int_abc123")) .to_return( status: 200, @@ -128,7 +119,7 @@ end it "filters by reason" do - stub_request(:get, "https://api-demo.airwallex.com/api/v1/disputes") + stub_request(:get, "#{BASE_URL}/api/v1/disputes") .with(query: hash_including(reason: "product_not_received")) .to_return( status: 200, @@ -147,7 +138,7 @@ end it "supports auto-paging" do - stub_request(:get, "https://api-demo.airwallex.com/api/v1/disputes?page_size=2") + stub_request(:get, "#{BASE_URL}/api/v1/disputes?page_size=2") .to_return( status: 200, body: { @@ -160,7 +151,7 @@ headers: { "Content-Type" => "application/json" } ) - stub_request(:get, "https://api-demo.airwallex.com/api/v1/disputes?offset=2&page_size=2") + stub_request(:get, "#{BASE_URL}/api/v1/disputes?offset=2&page_size=2") .to_return( status: 200, body: { @@ -186,7 +177,7 @@ dispute_id = "dis_accept_123" # First retrieve - stub_request(:get, "https://api-demo.airwallex.com/api/v1/disputes/#{dispute_id}") + stub_request(:get, "#{BASE_URL}/api/v1/disputes/#{dispute_id}") .to_return( status: 200, body: { @@ -198,7 +189,7 @@ ) # Then accept - stub_request(:post, "https://api-demo.airwallex.com/api/v1/disputes/#{dispute_id}/accept") + stub_request(:post, "#{BASE_URL}/api/v1/disputes/#{dispute_id}/accept") .to_return( status: 200, body: { @@ -223,7 +214,7 @@ dispute_id = "dis_challenge_123" # First retrieve - stub_request(:get, "https://api-demo.airwallex.com/api/v1/disputes/#{dispute_id}") + stub_request(:get, "#{BASE_URL}/api/v1/disputes/#{dispute_id}") .to_return( status: 200, body: { @@ -235,7 +226,7 @@ ) # Then submit evidence - stub_request(:post, "https://api-demo.airwallex.com/api/v1/disputes/#{dispute_id}/evidence") + stub_request(:post, "#{BASE_URL}/api/v1/disputes/#{dispute_id}/evidence") .with( body: hash_including( customer_communication: "Email thread", @@ -270,14 +261,14 @@ it "submits comprehensive evidence" do dispute_id = "dis_full_evidence" - stub_request(:get, "https://api-demo.airwallex.com/api/v1/disputes/#{dispute_id}") + stub_request(:get, "#{BASE_URL}/api/v1/disputes/#{dispute_id}") .to_return( status: 200, body: { id: dispute_id, status: "OPEN" }.to_json, headers: { "Content-Type" => "application/json" } ) - stub_request(:post, "https://api-demo.airwallex.com/api/v1/disputes/#{dispute_id}/evidence") + stub_request(:post, "#{BASE_URL}/api/v1/disputes/#{dispute_id}/evidence") .with( body: hash_including( customer_communication: "Full email exchange", @@ -313,14 +304,14 @@ it "handles evidence submission errors" do dispute_id = "dis_error" - stub_request(:get, "https://api-demo.airwallex.com/api/v1/disputes/#{dispute_id}") + stub_request(:get, "#{BASE_URL}/api/v1/disputes/#{dispute_id}") .to_return( status: 200, body: { id: dispute_id, status: "OPEN" }.to_json, headers: { "Content-Type" => "application/json" } ) - stub_request(:post, "https://api-demo.airwallex.com/api/v1/disputes/#{dispute_id}/evidence") + stub_request(:post, "#{BASE_URL}/api/v1/disputes/#{dispute_id}/evidence") .to_return( status: 400, body: { @@ -342,7 +333,7 @@ describe "error handling" do it "handles dispute not found" do - stub_request(:get, "https://api-demo.airwallex.com/api/v1/disputes/dis_missing") + stub_request(:get, "#{BASE_URL}/api/v1/disputes/dis_missing") .to_return( status: 404, body: { @@ -358,7 +349,7 @@ end it "handles invalid status filter" do - stub_request(:get, "https://api-demo.airwallex.com/api/v1/disputes") + stub_request(:get, "#{BASE_URL}/api/v1/disputes") .with(query: hash_including(status: "INVALID_STATUS")) .to_return( status: 400, @@ -377,14 +368,14 @@ it "handles permission errors for accept" do dispute_id = "dis_forbidden" - stub_request(:get, "https://api-demo.airwallex.com/api/v1/disputes/#{dispute_id}") + stub_request(:get, "#{BASE_URL}/api/v1/disputes/#{dispute_id}") .to_return( status: 200, body: { id: dispute_id, status: "WON" }.to_json, headers: { "Content-Type" => "application/json" } ) - stub_request(:post, "https://api-demo.airwallex.com/api/v1/disputes/#{dispute_id}/accept") + stub_request(:post, "#{BASE_URL}/api/v1/disputes/#{dispute_id}/accept") .to_return( status: 403, body: { diff --git a/spec/airwallex/resources/payment_intent_spec.rb b/spec/airwallex/resources/payment_intent_spec.rb index 7e64c45..017c00e 100644 --- a/spec/airwallex/resources/payment_intent_spec.rb +++ b/spec/airwallex/resources/payment_intent_spec.rb @@ -3,18 +3,6 @@ require "spec_helper" RSpec.describe Airwallex::PaymentIntent do - let(:auth_response) do - { - status: 200, - body: { token: "test_token" }.to_json, - headers: { "Content-Type" => "application/json" } - } - end - - before do - stub_request(:post, "https://api-demo.airwallex.com/api/v1/authentication/login") - .to_return(auth_response) - end describe ".resource_path" do it "returns correct path" do @@ -43,7 +31,7 @@ end before do - stub_request(:post, "https://api-demo.airwallex.com/api/v1/pa/payment_intents/create") + stub_request(:post, "#{BASE_URL}/api/v1/pa/payment_intents/create") .with(body: hash_including(create_params)) .to_return( status: 200, @@ -65,7 +53,7 @@ it "sends POST request to correct endpoint" do described_class.create(create_params) - expect(WebMock).to have_requested(:post, "https://api-demo.airwallex.com/api/v1/pa/payment_intents/create") + expect(WebMock).to have_requested(:post, "#{BASE_URL}/api/v1/pa/payment_intents/create") .with(body: hash_including(create_params)) end end @@ -81,7 +69,7 @@ end before do - stub_request(:get, "https://api-demo.airwallex.com/api/v1/pa/payment_intents/pi_123") + stub_request(:get, "#{BASE_URL}/api/v1/pa/payment_intents/pi_123") .to_return( status: 200, body: intent_response.to_json, @@ -110,7 +98,7 @@ end before do - stub_request(:get, "https://api-demo.airwallex.com/api/v1/pa/payment_intents") + stub_request(:get, "#{BASE_URL}/api/v1/pa/payment_intents") .with(query: { page_size: 10 }) .to_return( status: 200, @@ -153,7 +141,7 @@ end before do - stub_request(:put, "https://api-demo.airwallex.com/api/v1/pa/payment_intents/pi_123") + stub_request(:put, "#{BASE_URL}/api/v1/pa/payment_intents/pi_123") .with(body: hash_including(update_params)) .to_return( status: 200, @@ -200,7 +188,7 @@ end before do - stub_request(:post, "https://api-demo.airwallex.com/api/v1/pa/payment_intents/pi_123/confirm") + stub_request(:post, "#{BASE_URL}/api/v1/pa/payment_intents/pi_123/confirm") .with(body: hash_including(confirm_params)) .to_return( status: 200, @@ -219,7 +207,7 @@ it "sends POST request to confirm endpoint" do intent.confirm(confirm_params) - expect(WebMock).to have_requested(:post, "https://api-demo.airwallex.com/api/v1/pa/payment_intents/pi_123/confirm") + expect(WebMock).to have_requested(:post, "#{BASE_URL}/api/v1/pa/payment_intents/pi_123/confirm") end end @@ -249,7 +237,7 @@ end before do - stub_request(:post, "https://api-demo.airwallex.com/api/v1/pa/payment_intents/pi_123/cancel") + stub_request(:post, "#{BASE_URL}/api/v1/pa/payment_intents/pi_123/cancel") .with(body: hash_including(cancel_params)) .to_return( status: 200, @@ -294,7 +282,7 @@ end before do - stub_request(:post, "https://api-demo.airwallex.com/api/v1/pa/payment_intents/pi_123/capture") + stub_request(:post, "#{BASE_URL}/api/v1/pa/payment_intents/pi_123/capture") .with(body: hash_including(capture_params)) .to_return( status: 200, @@ -336,7 +324,7 @@ end before do - stub_request(:put, "https://api-demo.airwallex.com/api/v1/pa/payment_intents/pi_123") + stub_request(:put, "#{BASE_URL}/api/v1/pa/payment_intents/pi_123") .with(body: hash_including(update_params)) .to_return( status: 200, @@ -371,7 +359,7 @@ end before do - stub_request(:put, "https://api-demo.airwallex.com/api/v1/pa/payment_intents/pi_123") + stub_request(:put, "#{BASE_URL}/api/v1/pa/payment_intents/pi_123") .to_return( status: 200, body: updated_response.to_json, diff --git a/spec/airwallex/resources/payment_method_spec.rb b/spec/airwallex/resources/payment_method_spec.rb index 92a2499..55f86d8 100644 --- a/spec/airwallex/resources/payment_method_spec.rb +++ b/spec/airwallex/resources/payment_method_spec.rb @@ -3,18 +3,6 @@ require "spec_helper" RSpec.describe Airwallex::PaymentMethod do - let(:auth_response) do - { - status: 200, - body: { token: "test_token" }.to_json, - headers: { "Content-Type" => "application/json" } - } - end - - before do - stub_request(:post, "https://api-demo.airwallex.com/api/v1/authentication/login") - .to_return(auth_response) - end describe ".resource_path" do it "returns correct path" do @@ -62,7 +50,7 @@ end before do - stub_request(:post, "https://api-demo.airwallex.com/api/v1/pa/payment_methods/create") + stub_request(:post, "#{BASE_URL}/api/v1/pa/payment_methods/create") .with(body: hash_including(type: "card")) .to_return( status: 200, @@ -84,7 +72,7 @@ it "sends POST request to correct endpoint" do described_class.create(create_params) - expect(WebMock).to have_requested(:post, "https://api-demo.airwallex.com/api/v1/pa/payment_methods/create") + expect(WebMock).to have_requested(:post, "#{BASE_URL}/api/v1/pa/payment_methods/create") end it "does not return full card number on creation" do @@ -111,7 +99,7 @@ end before do - stub_request(:get, "https://api-demo.airwallex.com/api/v1/pa/payment_methods/pm_123") + stub_request(:get, "#{BASE_URL}/api/v1/pa/payment_methods/pm_123") .to_return( status: 200, body: payment_method_response.to_json, @@ -148,7 +136,7 @@ end before do - stub_request(:get, "https://api-demo.airwallex.com/api/v1/pa/payment_methods") + stub_request(:get, "#{BASE_URL}/api/v1/pa/payment_methods") .with(query: { customer_id: "cus_123", page_size: 10 }) .to_return( status: 200, @@ -169,7 +157,7 @@ it "filters by customer_id" do described_class.list(customer_id: "cus_123", page_size: 10) - expect(WebMock).to have_requested(:get, "https://api-demo.airwallex.com/api/v1/pa/payment_methods") + expect(WebMock).to have_requested(:get, "#{BASE_URL}/api/v1/pa/payment_methods") .with(query: hash_including(customer_id: "cus_123")) end @@ -208,7 +196,7 @@ end before do - stub_request(:put, "https://api-demo.airwallex.com/api/v1/pa/payment_methods/pm_123") + stub_request(:put, "#{BASE_URL}/api/v1/pa/payment_methods/pm_123") .to_return( status: 200, body: updated_response.to_json, @@ -245,7 +233,7 @@ end before do - stub_request(:put, "https://api-demo.airwallex.com/api/v1/pa/payment_methods/pm_123") + stub_request(:put, "#{BASE_URL}/api/v1/pa/payment_methods/pm_123") .to_return( status: 200, body: updated_response.to_json, @@ -263,7 +251,7 @@ describe ".delete" do before do - stub_request(:delete, "https://api-demo.airwallex.com/api/v1/pa/payment_methods/pm_123") + stub_request(:delete, "#{BASE_URL}/api/v1/pa/payment_methods/pm_123") .to_return( status: 200, body: {}.to_json, @@ -280,7 +268,7 @@ it "sends DELETE request" do described_class.delete("pm_123") - expect(WebMock).to have_requested(:delete, "https://api-demo.airwallex.com/api/v1/pa/payment_methods/pm_123") + expect(WebMock).to have_requested(:delete, "#{BASE_URL}/api/v1/pa/payment_methods/pm_123") end end @@ -302,7 +290,7 @@ end before do - stub_request(:post, "https://api-demo.airwallex.com/api/v1/pa/payment_methods/pm_123/detach") + stub_request(:post, "#{BASE_URL}/api/v1/pa/payment_methods/pm_123/detach") .to_return( status: 200, body: detached_response.to_json, @@ -320,7 +308,7 @@ it "sends POST request to detach endpoint" do pm.detach - expect(WebMock).to have_requested(:post, "https://api-demo.airwallex.com/api/v1/pa/payment_methods/pm_123/detach") + expect(WebMock).to have_requested(:post, "#{BASE_URL}/api/v1/pa/payment_methods/pm_123/detach") end end end diff --git a/spec/airwallex/resources/quote_spec.rb b/spec/airwallex/resources/quote_spec.rb index 459e108..b63fb99 100644 --- a/spec/airwallex/resources/quote_spec.rb +++ b/spec/airwallex/resources/quote_spec.rb @@ -3,18 +3,10 @@ require "spec_helper" RSpec.describe Airwallex::Quote do - before do - stub_request(:post, "https://api-demo.airwallex.com/api/v1/authentication/login") - .to_return( - status: 200, - body: { token: "test_token", expires_at: (Time.now + 3600).iso8601 }.to_json, - headers: { "Content-Type" => "application/json" } - ) - end describe ".create" do it "creates quote with sell_amount" do - stub_request(:post, "https://api-demo.airwallex.com/api/v1/fx/quotes/create") + stub_request(:post, "#{BASE_URL}/api/v1/fx/quotes/create") .with(body: hash_including( from_currency: "USD", to_currency: "EUR", @@ -51,7 +43,7 @@ end it "creates quote with buy_amount" do - stub_request(:post, "https://api-demo.airwallex.com/api/v1/fx/quotes/create") + stub_request(:post, "#{BASE_URL}/api/v1/fx/quotes/create") .with(body: hash_including( from_currency: "GBP", to_currency: "JPY", @@ -86,7 +78,7 @@ describe ".retrieve" do it "retrieves existing quote" do - stub_request(:get, "https://api-demo.airwallex.com/api/v1/fx/quotes/quote_123456") + stub_request(:get, "#{BASE_URL}/api/v1/fx/quotes/quote_123456") .to_return( status: 200, body: { @@ -167,7 +159,7 @@ describe "error handling" do it "handles expired quote on creation" do - stub_request(:post, "https://api-demo.airwallex.com/api/v1/fx/quotes/create") + stub_request(:post, "#{BASE_URL}/api/v1/fx/quotes/create") .to_return( status: 400, body: { @@ -187,7 +179,7 @@ end it "handles missing amount parameter" do - stub_request(:post, "https://api-demo.airwallex.com/api/v1/fx/quotes/create") + stub_request(:post, "#{BASE_URL}/api/v1/fx/quotes/create") .to_return( status: 400, body: { @@ -206,7 +198,7 @@ end it "handles quote not found" do - stub_request(:get, "https://api-demo.airwallex.com/api/v1/fx/quotes/nonexistent") + stub_request(:get, "#{BASE_URL}/api/v1/fx/quotes/nonexistent") .to_return( status: 404, body: { diff --git a/spec/airwallex/resources/rate_spec.rb b/spec/airwallex/resources/rate_spec.rb index f29fee4..97880b4 100644 --- a/spec/airwallex/resources/rate_spec.rb +++ b/spec/airwallex/resources/rate_spec.rb @@ -3,18 +3,10 @@ require "spec_helper" RSpec.describe Airwallex::Rate do - before do - stub_request(:post, "https://api-demo.airwallex.com/api/v1/authentication/login") - .to_return( - status: 200, - body: { token: "test_token", expires_at: (Time.now + 3600).iso8601 }.to_json, - headers: { "Content-Type" => "application/json" } - ) - end describe ".retrieve" do it "retrieves rate for currency pair" do - stub_request(:get, "https://api-demo.airwallex.com/api/v1/fx/rates/current") + stub_request(:get, "#{BASE_URL}/api/v1/fx/rates/current") .with(query: { from_currency: "USD", to_currency: "EUR" }) .to_return( status: 200, @@ -38,7 +30,7 @@ end it "retrieves rate with different currency pair" do - stub_request(:get, "https://api-demo.airwallex.com/api/v1/fx/rates/current") + stub_request(:get, "#{BASE_URL}/api/v1/fx/rates/current") .with(query: { from_currency: "GBP", to_currency: "JPY" }) .to_return( status: 200, @@ -60,7 +52,7 @@ describe ".list" do it "lists multiple rates" do - stub_request(:get, "https://api-demo.airwallex.com/api/v1/fx/rates/current") + stub_request(:get, "#{BASE_URL}/api/v1/fx/rates/current") .with(query: hash_including(from_currency: "USD")) .to_return( status: 200, @@ -83,7 +75,7 @@ end it "filters rates by to_currencies" do - stub_request(:get, "https://api-demo.airwallex.com/api/v1/fx/rates/current") + stub_request(:get, "#{BASE_URL}/api/v1/fx/rates/current") .with(query: hash_including( from_currency: "USD", to_currencies: "EUR,GBP" @@ -111,7 +103,7 @@ describe "error handling" do it "handles invalid currency code" do - stub_request(:get, "https://api-demo.airwallex.com/api/v1/fx/rates/current") + stub_request(:get, "#{BASE_URL}/api/v1/fx/rates/current") .with(query: { from_currency: "XXX", to_currency: "EUR" }) .to_return( status: 400, @@ -128,7 +120,7 @@ end it "handles unsupported currency pair" do - stub_request(:get, "https://api-demo.airwallex.com/api/v1/fx/rates/current") + stub_request(:get, "#{BASE_URL}/api/v1/fx/rates/current") .with(query: { from_currency: "USD", to_currency: "BTC" }) .to_return( status: 400, diff --git a/spec/airwallex/resources/transfer_spec.rb b/spec/airwallex/resources/transfer_spec.rb index da2249f..c742d63 100644 --- a/spec/airwallex/resources/transfer_spec.rb +++ b/spec/airwallex/resources/transfer_spec.rb @@ -3,18 +3,6 @@ require "spec_helper" RSpec.describe Airwallex::Transfer do - let(:auth_response) do - { - status: 200, - body: { token: "test_token" }.to_json, - headers: { "Content-Type" => "application/json" } - } - end - - before do - stub_request(:post, "https://api-demo.airwallex.com/api/v1/authentication/login") - .to_return(auth_response) - end describe ".resource_path" do it "returns correct path" do @@ -45,7 +33,7 @@ end before do - stub_request(:post, "https://api-demo.airwallex.com/api/v1/transfers/create") + stub_request(:post, "#{BASE_URL}/api/v1/transfers/create") .with(body: hash_including(create_params)) .to_return( status: 200, @@ -66,7 +54,7 @@ it "sends POST request to correct endpoint" do described_class.create(create_params) - expect(WebMock).to have_requested(:post, "https://api-demo.airwallex.com/api/v1/transfers/create") + expect(WebMock).to have_requested(:post, "#{BASE_URL}/api/v1/transfers/create") end end @@ -82,7 +70,7 @@ end before do - stub_request(:get, "https://api-demo.airwallex.com/api/v1/transfers/tfr_123") + stub_request(:get, "#{BASE_URL}/api/v1/transfers/tfr_123") .to_return( status: 200, body: transfer_response.to_json, @@ -111,7 +99,7 @@ end before do - stub_request(:get, "https://api-demo.airwallex.com/api/v1/transfers") + stub_request(:get, "#{BASE_URL}/api/v1/transfers") .with(query: { page_size: 20 }) .to_return( status: 200, @@ -156,7 +144,7 @@ end before do - stub_request(:post, "https://api-demo.airwallex.com/api/v1/transfers/tfr_123/cancel") + stub_request(:post, "#{BASE_URL}/api/v1/transfers/tfr_123/cancel") .to_return( status: 200, body: cancelled_response.to_json, @@ -174,7 +162,7 @@ it "sends POST request to cancel endpoint" do transfer.cancel - expect(WebMock).to have_requested(:post, "https://api-demo.airwallex.com/api/v1/transfers/tfr_123/cancel") + expect(WebMock).to have_requested(:post, "#{BASE_URL}/api/v1/transfers/tfr_123/cancel") end end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 05c2d6b..c898691 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -6,6 +6,8 @@ # Configure WebMock to block all real HTTP requests WebMock.disable_net_connect!(allow_localhost: false) +Dir[File.join(__dir__, "support", "**", "*.rb")].sort.each { |f| require f } + RSpec.configure do |config| # Enable flags like --only-failures and --next-failure config.example_status_persistence_file_path = ".rspec_status" @@ -13,6 +15,8 @@ # Disable RSpec exposing methods globally on `Module` and `main` config.disable_monkey_patching! + config.include AirwallexTestHelpers + config.expect_with :rspec do |c| c.syntax = :expect end @@ -25,6 +29,8 @@ c.client_id = "test_client_id" c.environment = :sandbox end + + stub_login end # Clean up after each test diff --git a/spec/support/airwallex_test_helpers.rb b/spec/support/airwallex_test_helpers.rb new file mode 100644 index 0000000..5dbf8a0 --- /dev/null +++ b/spec/support/airwallex_test_helpers.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +# Central source for the sandbox host and login path, instead of repeating +# the literal strings in every spec file. Defined at the top level (not +# inside a module) so they resolve unqualified from any spec file: constant +# lookup is lexical, so a constant nested in a module included via +# `config.include` would NOT be visible here the way an included method is. +BASE_URL = Airwallex::Configuration::SANDBOX_API_URL +LOGIN_PATH = Airwallex::Client::LOGIN_PATH + +# Shared stubs for hitting the sandbox API across specs. +module AirwallexTestHelpers + def stub_login(status: 200, token: "test_token") + stub_request(:post, "#{BASE_URL}#{LOGIN_PATH}") + .to_return( + status: status, + body: { token: token }.to_json, + headers: { "Content-Type" => "application/json" } + ) + end +end From 19d5ca081e1655849b489c7e776f769fd7eaa4a0 Mon Sep 17 00:00:00 2001 From: adrian-y1 <80251505+adrian-y1@users.noreply.github.com> Date: Thu, 27 Aug 2026 10:28:55 +1000 Subject: [PATCH 3/3] remove dead code --- lib/airwallex/client.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/airwallex/client.rb b/lib/airwallex/client.rb index 860ad08..33dd12e 100644 --- a/lib/airwallex/client.rb +++ b/lib/airwallex/client.rb @@ -56,7 +56,6 @@ def authenticate! response = connection.post(LOGIN_PATH) do |req| req.headers["x-client-id"] = config.client_id req.headers["x-api-key"] = config.api_key - req.headers.delete("Authorization") end handle_response_errors(response)