Files
sure/test/models/provider/brex_test.rb
ghost 95f6451b39 feat(sync): add Brex provider connections (#1752)
* feat(sync): add Brex provider schema

Adds Brex item and account tables with per-family credentials, scoped upstream account uniqueness, encrypted token storage, and sanitized provider payload columns.

* feat(sync): add Brex provider core

Adds Brex item/account models, provider client and adapter support, family connection helpers, and provider enum registration for read-only Brex cash and card data.

* feat(sync): add Brex import pipeline

Adds Brex account discovery, linked-account sync, cash/card balance processors, transaction import, sanitized metadata handling, and idempotent provider entry processing.

* feat(sync): add Brex connection flows

Adds Mercury-style Brex connection management, explicit item-scoped account selection and linking, settings provider UI, account index visibility, localized copy, and per-item cache handling.

* test(sync): cover Brex provider workflows

Adds targeted coverage for Brex provider requests, adapter config, item/account guards, importer behavior, entry processing, and Mercury-style controller flows.

* fix(sync): align Brex API edge cases

Tightens Brex account fetching against the official card-account response shape, sends transaction start filters as RFC3339 date-times, and keeps provider error bodies out of user-facing messages while expanding provider client guard coverage.

* fix(sync): harden Brex provider integration

Restrict Brex API base URLs to official hosts, tighten account-selection UI behavior, and add tests for invalid credentials, cache scoping, and provider setup edge cases.

* test(sync): avoid Brex secret-shaped fixtures

* refactor(sync): extract Brex account flows

* fix(sync): address Brex provider review feedback

* fix(sync): address Brex review follow-ups

Move remaining Brex review cleanup into focused model behavior, tighten link/setup edge cases, localize summaries, and add regression coverage from CodeRabbit feedback.

Also records the security-review pass as no-findings after diff-scoped inspection and Brakeman validation.

* refactor(sync): split Brex account flow controllers

Route Brex account selection and setup actions through small namespaced controllers while keeping existing URLs and helpers stable.

Business flow remains in BrexItem::AccountFlow; the main Brex item controller now only handles connection CRUD, provider-panel rendering, destroy, and sync.

* fix(sync): address Brex CodeRabbit review

* fix(sync): address Brex follow-up review

* fix(sync): address Brex review follow-ups

* fix(sync): address Brex sync review findings

* fix(sync): polish Brex review copy and errors

* fix(sync): register Brex provider health

* fix(sync): polish Brex bank sync presentation

* fix(sync): address Brex review follow-ups

* fix(sync): tighten Brex setup params

* test(api): stabilize usage rate-limit window

* fix(sync): polish Brex setup flow nits

* fix(sync): harden Brex setup params

* fix(sync): finalize Brex review cleanup

---------

Signed-off-by: Juan José Mata <juanjo.mata@gmail.com>
Co-authored-by: Juan José Mata <juanjo.mata@gmail.com>
2026-05-13 18:13:48 +02:00

290 lines
8.8 KiB
Ruby

require "test_helper"
class Provider::BrexTest < ActiveSupport::TestCase
def setup
@provider = Provider::Brex.new("test_token", base_url: "https://api-staging.brex.com")
end
test "initializes with token and default base_url" do
provider = Provider::Brex.new("my_token")
assert_equal "my_token", provider.token
assert_equal "https://api.brex.com", provider.base_url
end
test "initializes with custom base_url" do
assert_equal "test_token", @provider.token
assert_equal "https://api-staging.brex.com", @provider.base_url
end
test "initializes with stripped token and removes trailing base url slash" do
provider = Provider::Brex.new(" test_token \n", base_url: "https://api.brex.com/")
assert_equal "test_token", provider.token
assert_equal "https://api.brex.com", provider.base_url
end
test "initializes with official staging base url" do
provider = Provider::Brex.new("test_token", base_url: "https://api-staging.brex.com/")
assert_equal "https://api-staging.brex.com", provider.base_url
end
test "rejects arbitrary base urls" do
[
"http://api.brex.com",
"https://evil.example.test",
"https://localhost",
"https://127.0.0.1",
"https://10.0.0.1",
"https://api.brex.com.evil.example",
"https://api.brex.com@127.0.0.1",
"https://api.brex.com:444",
"https://api.brex.com/v1",
"https://api.brex.com?host=evil.example.test",
"//api.brex.com"
].each do |base_url|
assert_raises ArgumentError do
Provider::Brex.new("test_token", base_url: base_url)
end
end
end
test "BrexError includes error_type" do
error = Provider::Brex::BrexError.new("Test error", :unauthorized)
assert_equal "Test error", error.message
assert_equal :unauthorized, error.error_type
end
test "BrexError defaults error_type to unknown" do
error = Provider::Brex::BrexError.new("Test error")
assert_equal :unknown, error.error_type
end
test "fetches cash accounts from the v2 endpoint with bearer auth" do
response = OpenStruct.new(
code: 200,
body: { items: [ { id: "cash_1", name: "Operating" } ] }.to_json,
headers: {}
)
Provider::Brex.expects(:get)
.with(
"https://api.brex.com/v2/accounts/cash?limit=1000",
headers: {
"Authorization" => "Bearer test_token",
"Content-Type" => "application/json",
"Accept" => "application/json"
}
)
.returns(response)
accounts = Provider::Brex.new(" test_token ").get_cash_accounts
assert_equal 1, accounts.length
assert_equal "cash_1", accounts.first[:id]
assert_equal "cash", accounts.first[:account_kind]
end
test "fetches card accounts from the paginated v2 endpoint" do
response = OpenStruct.new(
code: 200,
body: [ { id: "card_account_1", status: "ACTIVE" } ].to_json,
headers: {}
)
Provider::Brex.expects(:get)
.with(
"https://api.brex.com/v2/accounts/card?limit=1000",
headers: {
"Authorization" => "Bearer test_token",
"Content-Type" => "application/json",
"Accept" => "application/json"
}
)
.returns(response)
accounts = Provider::Brex.new("test_token").get_card_accounts
assert_equal 1, accounts.length
assert_equal "card_account_1", accounts.first[:id]
assert_equal "card", accounts.first[:account_kind]
end
test "aggregates card accounts into one provider account" do
cash_response = OpenStruct.new(
code: 200,
body: { items: [] }.to_json,
headers: {}
)
card_response = OpenStruct.new(
code: 200,
body: {
items: [
{
id: "card_account_1",
status: "ACTIVE",
current_balance: { amount: 12_345, currency: "USD" },
available_balance: { amount: 100_000, currency: "USD" },
account_limit: { amount: 250_000, currency: "USD" }
}
]
}.to_json,
headers: {}
)
Provider::Brex.stubs(:get).returns(cash_response, card_response)
accounts_data = Provider::Brex.new("test_token").get_accounts
assert_equal [ "card_primary" ], accounts_data[:accounts].map { |account| account[:id] }
assert_equal "card", accounts_data[:accounts].first[:account_kind]
assert_equal 1, accounts_data[:accounts].first[:card_accounts_count]
end
test "does not aggregate mixed currency card balances" do
cash_response = OpenStruct.new(
code: 200,
body: { items: [] }.to_json,
headers: {}
)
card_response = OpenStruct.new(
code: 200,
body: [
{
id: "card_account_1",
current_balance: { amount: 12_345, currency: "USD" }
},
{
id: "card_account_2",
current_balance: { amount: 6_789, currency: "EUR" }
}
].to_json,
headers: {}
)
Provider::Brex.stubs(:get).returns(cash_response, card_response)
accounts_data = Provider::Brex.new("test_token").get_accounts
assert_nil accounts_data[:accounts].first[:current_balance]
end
test "guards repeated pagination cursors" do
first_response = OpenStruct.new(
code: 200,
body: { items: [ { id: "tx_1" } ], next_cursor: "cursor_1" }.to_json,
headers: {}
)
second_response = OpenStruct.new(
code: 200,
body: { items: [ { id: "tx_2" } ], next_cursor: "cursor_1" }.to_json,
headers: {}
)
Provider::Brex.stubs(:get).returns(first_response, second_response)
error = assert_raises Provider::Brex::BrexError do
Provider::Brex.new("test_token").get_primary_card_transactions
end
assert_equal :pagination_error, error.error_type
end
test "guards pagination page cap" do
responses = (1..26).map do |page|
OpenStruct.new(
code: 200,
body: { items: [ { id: "tx_#{page}" } ], next_cursor: "cursor_#{page}" }.to_json,
headers: {}
)
end
Provider::Brex.stubs(:get).returns(*responses)
error = assert_raises Provider::Brex::BrexError do
Provider::Brex.new("test_token").get_primary_card_transactions
end
assert_equal :pagination_error, error.error_type
assert_includes error.message, "exceeded 25 pages"
end
test "sends posted_at_start as RFC3339 date time" do
response = OpenStruct.new(
code: 200,
body: { items: [] }.to_json,
headers: {}
)
Provider::Brex.expects(:get)
.with(
"https://api.brex.com/v2/transactions/card/primary?posted_at_start=2026-01-02T00%3A00%3A00Z&limit=1000",
headers: {
"Authorization" => "Bearer test_token",
"Content-Type" => "application/json",
"Accept" => "application/json"
}
)
.returns(response)
Provider::Brex.new("test_token").get_primary_card_transactions(start_date: Date.new(2026, 1, 2))
end
test "raises clear error for invalid start date" do
error = assert_raises ArgumentError do
Provider::Brex.new("test_token").get_primary_card_transactions(start_date: "not-a-date")
end
assert_includes error.message, "Invalid start_date"
end
test "maps rate limits and exposes trace id without leaking body" do
response = OpenStruct.new(
code: 429,
body: { message: "secret raw provider body" }.to_json,
headers: { "x-brex-trace-id" => "trace_123" }
)
Provider::Brex.stubs(:get).returns(response)
error = assert_raises Provider::Brex::BrexError do
Provider::Brex.new("test_token").get_cash_accounts
end
assert_equal :rate_limited, error.error_type
assert_equal 429, error.http_status
assert_equal "trace_123", error.trace_id
refute_includes error.message, "secret raw provider body"
end
test "maps non-success responses without exposing provider body" do
expectations = {
400 => [ :bad_request, "Bad request to Brex API" ],
401 => [ :unauthorized, "Invalid Brex API token or account permissions" ],
403 => [ :access_forbidden, "Access forbidden - check Brex API token scopes" ],
404 => [ :not_found, "Brex resource not found" ],
500 => [ :fetch_failed, "Failed to fetch data from Brex API: HTTP 500" ]
}
expectations.each do |status, (error_type, message)|
response = OpenStruct.new(
code: status,
body: { message: "secret provider body #{status}" }.to_json,
headers: { "X-Brex-Trace-Id" => "trace_#{status}" }
)
Provider::Brex.stubs(:get).returns(response)
error = assert_raises Provider::Brex::BrexError do
Provider::Brex.new("test_token").get_cash_accounts
end
assert_equal error_type, error.error_type
assert_equal status, error.http_status
assert_equal "trace_#{status}", error.trace_id
assert_equal message, error.message
refute_includes error.message, "secret provider body"
end
end
end