mirror of
https://github.com/we-promise/sure.git
synced 2026-07-12 12:55:20 +00:00
* Add SnapTrade OAuth device flow * Add SnapTrade OAuth device flow ### Motivation - Integrate SnapTrade device-code OAuth so administrators can start a device authorization flow and poll for tokens using SnapTrade's well-known OAuth metadata endpoint. - Persist and encrypt device-flow token material on `SnaptradeItem` to support long-lived API calls and future refresh handling. ### Description - Add OAuth discovery and device-code support to the SnapTrade provider with `oauth_authorization_server_metadata`, `start_device_authorization`, and `poll_device_token` helper methods and shared `oauth_connection` / `parse_oauth_response` helpers. - Persist OAuth device-flow tokens on `snaptrade_items` via a migration that adds `oauth_access_token`, `oauth_refresh_token`, `oauth_token_type`, `oauth_scope`, and `oauth_token_expires_at`, and update `db/schema.rb` accordingly. - Extend `SnaptradeItem` to encrypt stored `oauth_access_token` and `oauth_refresh_token` when ActiveRecord encryption is configured and add `oauth_token_active?` to check token validity. - Add high-level `start_oauth_device_flow` and `complete_oauth_device_flow!` helpers to the `SnaptradeItem::Provided` concern to start authorization and persist token responses. - Expose admin-only member routes and JSON controller actions `start_oauth_device_flow` and `complete_oauth_device_flow` on `SnaptradeItemsController` to drive the device flow from the UI. - Add focused Minitest coverage: `test/models/provider/snaptrade_oauth_test.rb` for provider requests and error handling and `test/models/snaptrade_item_oauth_test.rb` for token persistence. ### Testing - Ran the targeted test suite: `bin/rails test test/models/provider/snaptrade_oauth_test.rb test/models/snaptrade_item_oauth_test.rb test/controllers/snaptrade_items_controller_test.rb`, and all tests passed. - Ran `bin/rubocop` against modified files and no offenses were reported. * Add SnapTrade OAuth device flow ### Motivation - Integrate SnapTrade device-code OAuth so administrators can start a device authorization flow and poll for tokens using SnapTrade's well-known OAuth metadata endpoint. - Persist and encrypt device-flow token material on `SnaptradeItem` to support long-lived API calls and future refresh handling. ### Description - Add OAuth discovery and device-code support to the SnapTrade provider with `oauth_authorization_server_metadata`, `start_device_authorization`, and `poll_device_token` helper methods and shared `oauth_connection` / `parse_oauth_response` helpers. - Persist OAuth device-flow tokens on `snaptrade_items` via a migration that adds `oauth_access_token`, `oauth_refresh_token`, `oauth_token_type`, `oauth_scope`, and `oauth_token_expires_at`, and update `db/schema.rb` accordingly. - Extend `SnaptradeItem` to encrypt stored `oauth_access_token` and `oauth_refresh_token` when ActiveRecord encryption is configured and add `oauth_token_active?` to check token validity. - Add high-level `start_oauth_device_flow` and `complete_oauth_device_flow!` helpers to the `SnaptradeItem::Provided` concern to start authorization and persist token responses. - Expose admin-only member routes and JSON controller actions `start_oauth_device_flow` and `complete_oauth_device_flow` on `SnaptradeItemsController` to drive the device flow from the UI. - Add focused Minitest coverage: `test/models/provider/snaptrade_oauth_test.rb` for provider requests and error handling and `test/models/snaptrade_item_oauth_test.rb` for token persistence. ### Testing - Ran the targeted test suite: `bin/rails test test/models/provider/snaptrade_oauth_test.rb test/models/snaptrade_item_oauth_test.rb test/controllers/snaptrade_items_controller_test.rb`, and all tests passed. - Ran `bin/rubocop` against modified files and no offenses were reported. * Add SnapTrade OAuth device flow ### Motivation - Integrate SnapTrade device-code OAuth so administrators can start a device authorization flow and poll for tokens using SnapTrade's well-known OAuth metadata endpoint. - Persist and encrypt device-flow token material on `SnaptradeItem` to support long-lived API calls and future refresh handling. ### Description - Add OAuth discovery and device-code support to the SnapTrade provider with `oauth_authorization_server_metadata`, `start_device_authorization`, and `poll_device_token` helper methods and shared `oauth_connection` / `parse_oauth_response` helpers. - Persist OAuth device-flow tokens on `snaptrade_items` via a migration that adds `oauth_access_token`, `oauth_refresh_token`, `oauth_token_type`, `oauth_scope`, and `oauth_token_expires_at`, and update `db/schema.rb` accordingly. - Extend `SnaptradeItem` to encrypt stored `oauth_access_token` and `oauth_refresh_token` when ActiveRecord encryption is configured and add `oauth_token_active?` to check token validity. - Add high-level `start_oauth_device_flow` and `complete_oauth_device_flow!` helpers to the `SnaptradeItem::Provided` concern to start authorization and persist token responses. - Expose admin-only member routes and JSON controller actions `start_oauth_device_flow` and `complete_oauth_device_flow` on `SnaptradeItemsController` to drive the device flow from the UI. - Add focused Minitest coverage: `test/models/provider/snaptrade_oauth_test.rb` for provider requests and error handling and `test/models/snaptrade_item_oauth_test.rb` for token persistence. ### Testing - Ran the targeted test suite: `bin/rails test test/models/provider/snaptrade_oauth_test.rb test/models/snaptrade_item_oauth_test.rb test/controllers/snaptrade_items_controller_test.rb`, and all tests passed. - Ran `bin/rubocop` against modified files and no offenses were reported. * Add SnapTrade OAuth device flow ### Motivation - Integrate SnapTrade device-code OAuth so administrators can start a device authorization flow and poll for tokens using SnapTrade's well-known OAuth metadata endpoint. - Persist and encrypt device-flow token material on `SnaptradeItem` to support long-lived API calls and future refresh handling. ### Description - Add OAuth discovery and device-code support to the SnapTrade provider with `oauth_authorization_server_metadata`, `start_device_authorization`, and `poll_device_token` helper methods and shared `oauth_connection` / `parse_oauth_response` helpers. - Persist OAuth device-flow tokens on `snaptrade_items` via a migration that adds `oauth_access_token`, `oauth_refresh_token`, `oauth_token_type`, `oauth_scope`, and `oauth_token_expires_at`, and update `db/schema.rb` accordingly. - Extend `SnaptradeItem` to encrypt stored `oauth_access_token` and `oauth_refresh_token` when ActiveRecord encryption is configured and add `oauth_token_active?` to check token validity. - Add high-level `start_oauth_device_flow` and `complete_oauth_device_flow!` helpers to the `SnaptradeItem::Provided` concern to start authorization and persist token responses. - Expose admin-only member routes and JSON controller actions `start_oauth_device_flow` and `complete_oauth_device_flow` on `SnaptradeItemsController` to drive the device flow from the UI. - Add focused Minitest coverage: `test/models/provider/snaptrade_oauth_test.rb` for provider requests and error handling and `test/models/snaptrade_item_oauth_test.rb` for token persistence. ### Testing - Ran the targeted test suite: `bin/rails test test/models/provider/snaptrade_oauth_test.rb test/models/snaptrade_item_oauth_test.rb test/controllers/snaptrade_items_controller_test.rb`, and all tests passed. - Ran `bin/rubocop` against modified files and no offenses were reported. * Add SnapTrade OAuth device flow (provider, model, controller, routes, migration, tests) ### Motivation - Add support for SnapTrade OAuth 2.0 device authorization flow so users can authorize SnapTrade via device codes in addition to the existing portal flow. - Persist OAuth token metadata on `SnaptradeItem` for subsequent polling and usage by the provider and UI. ### Description - Implemented OAuth device flow in the provider with `oauth_authorization_server_metadata`, `start_device_authorization`, and `poll_device_token`, plus HTTP helper methods `oauth_connection`, `oauth_client_id`, and `parse_oauth_response` in `Provider::Snaptrade`. - Added controller endpoints `start_oauth_device_flow` and `complete_oauth_device_flow` in `SnaptradeItemsController` with robust error handling and helpers `oauth_error_payload` and `parse_oauth_error_body` to surface OAuth error fields. - Extended `SnaptradeItem` with encrypted columns for `oauth_access_token`, `oauth_refresh_token`, token metadata, `oauth_token_active?`, and model methods `start_oauth_device_flow` and `complete_oauth_device_flow!` to store token metadata. - Added migration `AddOauthDeviceFlowToSnaptradeItems` and updated `db/schema.rb` to include the new columns, registered a new initializer `config/initializers/snaptrade.rb` to read `SNAPTRADE_OAUTH_CLIENT_ID`, and updated `.env*.example` files to document `SNAPTRADE_OAUTH_CLIENT_ID`. - Exposed new routes `start_oauth_device_flow` and `complete_oauth_device_flow` for `snaptrade_items`. ### Testing - Added unit tests for provider OAuth behavior in `test/models/provider/snaptrade_oauth_test.rb`, model token persistence in `test/models/snaptrade_item_oauth_test.rb`, and controller error propagation in `test/controllers/snaptrade_items_controller_test.rb`. - Ran the test suite with `bin/rails test` and the full test run (including the new SnapTrade OAuth tests) passed. * Add SnapTrade OAuth device flow (start/poll), store tokens, and tests ### Motivation - Add support for SnapTrade OAuth Device Authorization flow so administrators can start device authorization and poll for tokens without exposing provider internals. - Persist OAuth token metadata on `SnaptradeItem` so the app can reuse and surface token state for SnapTrade integrations. - Improve error handling and sanitization for OAuth API errors returned by SnapTrade to avoid leaking upstream internals. ### Description - Introduces new environment examples and initializer: adds `SNAPTRADE_OAUTH_CLIENT_ID` to `.env.local.example`/`.env.test.example` and configures `Rails.configuration.x.snaptrade.oauth_client_id` in `config/initializers/snaptrade.rb`. - Extends `Provider::Snaptrade` with OAuth device flow support, adding discovery URL, device grant constant, Faraday `oauth_connection`, and methods `oauth_authorization_server_metadata`, `start_device_authorization`, `poll_device_token`, `oauth_client_id`, and `parse_oauth_response`, plus improved retry and API error wrapping. - Adds DB migration and schema changes to store OAuth fields on `snaptrade_items` (`oauth_access_token`, `oauth_refresh_token`, `oauth_token_type`, `oauth_scope`, `oauth_token_expires_at`) and marks those attributes encrypted when ActiveRecord encryption is enabled. - Adds `SnaptradeItem` helpers `start_oauth_device_flow`, `complete_oauth_device_flow!`, and `oauth_token_active?` to start/poll and persist token metadata. - Adds controller actions `start_oauth_device_flow` and `complete_oauth_device_flow` with sanitized error responses and helper methods `oauth_error_payload` and `parse_oauth_error_body`, and wires new member routes in `config/routes.rb`. - Updates `SnaptradeItemsController` before_action lists to include the new actions and adds user-facing error message helper `start_oauth_device_flow_error_message`. ### Testing - Added unit tests `test/models/provider/snaptrade_oauth_test.rb` to validate discovery, device authorization request, token polling, missing client ID handling, and OAuth error propagation, and all assertions passed. - Added `test/models/snaptrade_item_oauth_test.rb` to assert `complete_oauth_device_flow!` stores tokens and expiry and that `oauth_token_active?` reports correctly, and the test passed. - Extended `test/controllers/snaptrade_items_controller_test.rb` with controller-level tests confirming sanitized OAuth error payloads and behavior for start/complete endpoints, and these controller tests passed. * Add SnapTrade OAuth device flow support and token storage ### Motivation - Add support for the OAuth 2.0 device authorization flow for SnapTrade so users can link brokerages via a device-code flow without traditional browser-based client redirects. - Persist OAuth token metadata on the SnaptradeItem so tokens can be reused and expiration tracked. - Surface and sanitize provider error payloads to callers while avoiding leakage of internal configuration details. ### Description - Added new DB columns and a migration (`oauth_access_token`, `oauth_refresh_token`, `oauth_token_type`, `oauth_scope`, `oauth_token_expires_at`) and updated `db/schema.rb` to reflect the change. - Encrypted the new token fields on `SnaptradeItem` and added `oauth_token_active?`, `start_oauth_device_flow`, and `complete_oauth_device_flow!` helpers to the model. - Implemented OAuth logic in `Provider::Snaptrade` including discovery (`OAUTH_DISCOVERY_URL`), `start_device_authorization`, `poll_device_token`, request parsing, retry handling, and a small Faraday connection wrapper; added configuration accessors for `oauth_client_id` via `config.x.snaptrade`. - Added controller endpoints `start_oauth_device_flow` and `complete_oauth_device_flow` with safe error handling and helpers to format OAuth error payloads, and registered new member routes for `snaptrade_items`. - Added an initializer to load `SNAPTRADE_OAUTH_CLIENT_ID` into `Rails.configuration.x.snaptrade`, and updated `.env.local.example` and `.env.test.example` to include the new env var. - Added unit tests for provider OAuth behavior (`test/models/provider/snaptrade_oauth_test.rb`), SnaptradeItem token persistence (`test/models/snaptrade_item_oauth_test.rb`), and controller error handling (`test/controllers/snaptrade_items_controller_test.rb`), and updated controller tests accordingly. ### Testing - Ran provider-level tests in `Provider::SnaptradeOauthTest` to validate discovery, device authorization, token polling, and error handling, which passed. - Ran model tests in `SnaptradeItemOauthTest` to verify token metadata is stored and `oauth_token_active?` works, which passed. - Ran controller tests in `SnaptradeItemsControllerTest` that exercise the start/complete endpoints and error sanitization, which passed. * Add SnapTrade OAuth device-flow support and token storage ### Motivation - Add support for SnapTrade OAuth device authorization so users can perform device-code based OAuth without embedding secrets in the browser. - Persist OAuth token metadata on `SnaptradeItem` and expose programmatic start/complete endpoints while avoiding leaking provider internals on errors. - Make OAuth client ID configurable via environment or credentials for Sure deployments. ### Description - Introduce `SNAPTRADE_OAUTH_CLIENT_ID` to `.env` examples and add `config.x.snaptrade.oauth_client_id` initializer for configuration. - Add migration `AddOauthDeviceFlowToSnaptradeItems` and update `schema.rb` to add `oauth_access_token`, `oauth_refresh_token`, `oauth_token_type`, `oauth_scope`, and `oauth_token_expires_at` to `snaptrade_items`. - Persist and encrypt new token fields in `SnaptradeItem` and add helper methods `oauth_token_active?`, `start_oauth_device_flow`, and `complete_oauth_device_flow!`. - Extend `Provider::Snaptrade` with OAuth discovery, device authorization (`start_device_authorization`), token polling (`poll_device_token`), Faraday-based `oauth_connection`, parsing and error-wrapping logic, and retry handling. - Add controller actions `start_oauth_device_flow` and `complete_oauth_device_flow` to `SnaptradeItemsController`, plus helper methods to format OAuth error payloads and user-facing error messages. - Wire up new routes (`post :start_oauth_device_flow`, `post :complete_oauth_device_flow`) and add `config/initializers/snaptrade.rb`. - Add comprehensive tests for the OAuth flow and controller error handling and update a system test stub to avoid provider leakage during UI tests. ### Testing - Added `Provider::SnaptradeOauthTest` which stubs discovery, device authorization and token endpoints and asserts request payloads and responses; tests passed. - Added `SnaptradeItemOauthTest` which verifies token metadata persistence and `oauth_token_active?`; test passed. - Extended `SnaptradeItemsControllerTest` with scenarios for successful and failing device-flow completion and start flow error handling; tests passed. - Ran the affected system test changes in `TradesTest` (provider stubbing and modal behavior); the updated tests passed.
337 lines
13 KiB
Ruby
337 lines
13 KiB
Ruby
require "test_helper"
|
|
|
|
class SnaptradeItemsControllerTest < ActionDispatch::IntegrationTest
|
|
setup do
|
|
sign_in @user = users(:family_admin)
|
|
@snaptrade_item = snaptrade_items(:configured_item)
|
|
end
|
|
|
|
def sign_out
|
|
@user.sessions.each do |session|
|
|
delete session_path(session)
|
|
end
|
|
end
|
|
|
|
test "connect handles decryption error gracefully" do
|
|
SnaptradeItem.any_instance
|
|
.stubs(:user_registered?)
|
|
.raises(ActiveRecord::Encryption::Errors::Decryption.new("cannot decrypt"))
|
|
|
|
get connect_snaptrade_item_url(@snaptrade_item)
|
|
|
|
assert_redirected_to settings_providers_path
|
|
assert_match(/Unable to read SnapTrade credentials/, flash[:alert])
|
|
end
|
|
|
|
test "connect handles general error gracefully" do
|
|
SnaptradeItem.any_instance
|
|
.stubs(:user_registered?)
|
|
.raises(StandardError.new("something broke"))
|
|
|
|
get connect_snaptrade_item_url(@snaptrade_item)
|
|
|
|
assert_redirected_to settings_providers_path
|
|
assert_match(/Failed to connect/, flash[:alert])
|
|
end
|
|
|
|
test "connect redirects to portal when successful" do
|
|
portal_url = "https://app.snaptrade.com/portal/test123"
|
|
|
|
SnaptradeItem.any_instance.stubs(:user_registered?).returns(true)
|
|
SnaptradeItem.any_instance.stubs(:connection_portal_url).returns(portal_url)
|
|
|
|
get connect_snaptrade_item_url(@snaptrade_item)
|
|
|
|
assert_redirected_to portal_url
|
|
end
|
|
|
|
test "complete oauth device flow preserves provider oauth error fields" do
|
|
error = Provider::Snaptrade::ApiError.new(
|
|
"SnapTrade OAuth error (poll_device_token): authorization_pending",
|
|
status_code: 400,
|
|
response_body: {
|
|
error: "authorization_pending",
|
|
error_description: "The user has not completed authorization",
|
|
error_uri: "https://api.snaptrade.com/docs/oauth",
|
|
interval: 5
|
|
}.to_json
|
|
)
|
|
SnaptradeItem.any_instance
|
|
.stubs(:complete_oauth_device_flow!)
|
|
.raises(error)
|
|
|
|
post complete_oauth_device_flow_snaptrade_item_url(@snaptrade_item), params: { device_code: "device-code" }
|
|
|
|
assert_response :bad_request
|
|
payload = JSON.parse(response.body)
|
|
assert_equal "authorization_pending", payload["error"]
|
|
assert_equal "The user has not completed authorization", payload["error_description"]
|
|
assert_equal "https://api.snaptrade.com/docs/oauth", payload["error_uri"]
|
|
assert_equal 5, payload["interval"]
|
|
end
|
|
|
|
test "complete oauth device flow falls back to api error message without json body" do
|
|
error = Provider::Snaptrade::ApiError.new(
|
|
"SnapTrade OAuth error (poll_device_token): invalid response",
|
|
status_code: 502,
|
|
response_body: "upstream unavailable"
|
|
)
|
|
SnaptradeItem.any_instance
|
|
.stubs(:complete_oauth_device_flow!)
|
|
.raises(error)
|
|
|
|
post complete_oauth_device_flow_snaptrade_item_url(@snaptrade_item), params: { device_code: "device-code" }
|
|
|
|
assert_response :bad_gateway
|
|
payload = JSON.parse(response.body)
|
|
assert_equal "SnapTrade OAuth error (poll_device_token): invalid response", payload["error"]
|
|
end
|
|
|
|
test "complete oauth device flow does not expose non-api provider error details" do
|
|
SnaptradeItem.any_instance
|
|
.stubs(:complete_oauth_device_flow!)
|
|
.raises(Provider::Snaptrade::ConfigurationError.new("missing secret at /srv/app/config.yml"))
|
|
|
|
post complete_oauth_device_flow_snaptrade_item_url(@snaptrade_item), params: { device_code: "device-code" }
|
|
|
|
assert_response :unprocessable_entity
|
|
payload = JSON.parse(response.body)
|
|
assert_equal "Unable to complete SnapTrade OAuth device authorization. Please try again.", payload["error"]
|
|
end
|
|
|
|
test "start oauth device flow does not expose provider error details" do
|
|
SnaptradeItem.any_instance
|
|
.stubs(:start_oauth_device_flow)
|
|
.raises(Provider::Snaptrade::ApiError.new("upstream leaked internal path /srv/app/config.yml"))
|
|
|
|
post start_oauth_device_flow_snaptrade_item_url(@snaptrade_item)
|
|
|
|
assert_response :unprocessable_entity
|
|
payload = JSON.parse(response.body)
|
|
assert_equal "Unable to start SnapTrade OAuth device authorization. Please try again.", payload["error"]
|
|
end
|
|
|
|
test "select_accounts redirects unregistered users into connect flow" do
|
|
sign_out
|
|
sign_in @user = users(:empty)
|
|
snaptrade_item = snaptrade_items(:pending_registration_item)
|
|
|
|
get select_accounts_snaptrade_items_url, params: { accountable_type: "Investment", return_to: "setup_accounts" }
|
|
|
|
assert_redirected_to connect_snaptrade_item_path(snaptrade_item)
|
|
end
|
|
|
|
test "callback resumes setup flow after first-time connect detour" do
|
|
sign_out
|
|
sign_in @user = users(:empty)
|
|
snaptrade_item = snaptrade_items(:pending_registration_item)
|
|
|
|
assert_difference "Sync.count", 1 do
|
|
get select_accounts_snaptrade_items_url, params: { accountable_type: "Investment", return_to: "setup_accounts" }
|
|
assert_redirected_to connect_snaptrade_item_path(snaptrade_item)
|
|
|
|
get callback_snaptrade_items_url, params: { item_id: snaptrade_item.id }
|
|
end
|
|
|
|
assert_redirected_to setup_accounts_snaptrade_item_path(snaptrade_item, accountable_type: "Investment")
|
|
end
|
|
|
|
test "select_accounts redirects registered users to setup flow" do
|
|
get select_accounts_snaptrade_items_url, params: { accountable_type: "Investment", return_to: "/accounts" }
|
|
|
|
assert_redirected_to setup_accounts_snaptrade_item_path(@snaptrade_item, accountable_type: "Investment", return_to: "/accounts")
|
|
end
|
|
|
|
test "preload_accounts redirects unregistered users into connect flow" do
|
|
sign_out
|
|
sign_in @user = users(:empty)
|
|
snaptrade_item = snaptrade_items(:pending_registration_item)
|
|
|
|
assert_no_difference "Sync.count" do
|
|
get preload_accounts_snaptrade_items_url
|
|
end
|
|
|
|
assert_redirected_to connect_snaptrade_item_path(snaptrade_item)
|
|
end
|
|
|
|
test "preload_accounts redirects registered users to setup flow and queues sync" do
|
|
assert_difference "Sync.count", 1 do
|
|
get preload_accounts_snaptrade_items_url
|
|
end
|
|
|
|
assert_redirected_to setup_accounts_snaptrade_item_path(@snaptrade_item)
|
|
end
|
|
|
|
test "entry routing prefers a registered active item over a pending one" do
|
|
pending_item = @user.family.snaptrade_items.create!(
|
|
name: "Pending Registration",
|
|
client_id: "pending_client_id",
|
|
consumer_key: "pending_consumer_key",
|
|
status: :good,
|
|
scheduled_for_deletion: false,
|
|
pending_account_setup: true
|
|
)
|
|
|
|
get select_accounts_snaptrade_items_url, params: { accountable_type: "Investment", return_to: "/accounts" }
|
|
assert_redirected_to setup_accounts_snaptrade_item_path(@snaptrade_item, accountable_type: "Investment", return_to: "/accounts")
|
|
|
|
assert_difference "Sync.count", 1 do
|
|
get preload_accounts_snaptrade_items_url
|
|
end
|
|
assert_redirected_to setup_accounts_snaptrade_item_path(@snaptrade_item)
|
|
|
|
assert_not pending_item.user_registered?
|
|
end
|
|
|
|
test "setup_accounts shows linkable investment and crypto accounts in dropdown" do
|
|
get setup_accounts_snaptrade_item_url(@snaptrade_item)
|
|
|
|
assert_response :success
|
|
|
|
# Investment and crypto accounts (no provider) should appear in the link dropdown
|
|
assert_match accounts(:investment).name, response.body
|
|
assert_match accounts(:crypto).name, response.body
|
|
|
|
# Depository should NOT appear in the link dropdown (wrong type)
|
|
# The depository name may appear elsewhere on the page, so check the select options specifically
|
|
refute_match(/option.*#{accounts(:depository).name}/, response.body)
|
|
end
|
|
|
|
test "setup_accounts excludes accounts that already have a provider from dropdown" do
|
|
# Link the investment account to a snaptrade_account
|
|
AccountProvider.create!(
|
|
account: accounts(:investment),
|
|
provider: snaptrade_accounts(:fidelity_401k)
|
|
)
|
|
|
|
get setup_accounts_snaptrade_item_url(@snaptrade_item)
|
|
|
|
assert_response :success
|
|
|
|
# Investment account is now linked → should NOT appear in link dropdown options
|
|
refute_match(/option.*#{accounts(:investment).name}/, response.body)
|
|
# Crypto still unlinked → should appear
|
|
assert_match accounts(:crypto).name, response.body
|
|
end
|
|
|
|
test "select_existing_account prefers registered active item over pending one" do
|
|
pending_item = @user.family.snaptrade_items.create!(
|
|
name: "Pending Registration",
|
|
client_id: "pending_client_id",
|
|
consumer_key: "pending_consumer_key",
|
|
status: :good,
|
|
scheduled_for_deletion: false,
|
|
pending_account_setup: true
|
|
)
|
|
pending_item.snaptrade_accounts.create!(
|
|
snaptrade_account_id: "pending_snaptrade_account",
|
|
name: "Pending Brokerage Account",
|
|
brokerage_name: "Pending Broker",
|
|
currency: "USD",
|
|
current_balance: 0
|
|
)
|
|
|
|
get select_existing_account_snaptrade_items_url, params: { account_id: accounts(:investment).id }
|
|
|
|
assert_response :success
|
|
assert_includes response.body, snaptrade_accounts(:fidelity_401k).name
|
|
refute_includes response.body, "Pending Brokerage Account"
|
|
end
|
|
|
|
test "link_existing_account links account to snaptrade_account" do
|
|
account = accounts(:investment)
|
|
snaptrade_account = snaptrade_accounts(:fidelity_401k)
|
|
|
|
assert_difference "AccountProvider.count", 1 do
|
|
post link_existing_account_snaptrade_items_url, params: {
|
|
account_id: account.id,
|
|
snaptrade_account_id: snaptrade_account.id,
|
|
snaptrade_item_id: @snaptrade_item.id
|
|
}
|
|
end
|
|
|
|
assert_redirected_to account_path(account)
|
|
assert_match(/Successfully linked/, flash[:notice])
|
|
|
|
snaptrade_account.reload
|
|
assert_equal account, snaptrade_account.current_account
|
|
end
|
|
|
|
test "link_existing_account handles missing account gracefully" do
|
|
snaptrade_account = snaptrade_accounts(:fidelity_401k)
|
|
|
|
assert_no_difference "AccountProvider.count" do
|
|
post link_existing_account_snaptrade_items_url, params: {
|
|
account_id: "nonexistent",
|
|
snaptrade_account_id: snaptrade_account.id,
|
|
snaptrade_item_id: @snaptrade_item.id
|
|
}
|
|
end
|
|
|
|
assert_redirected_to settings_providers_path
|
|
assert_match(/not found/i, flash[:alert])
|
|
end
|
|
|
|
# --- setup_accounts throttle-sync fix ---
|
|
#
|
|
# The fix on setup_accounts ensures sync_later is only called when there are no
|
|
# accounts AND the item has never been synced (last_synced_at.blank?). This
|
|
# prevents the infinite-spinner loop where every page load re-triggered a sync
|
|
# even after SnapTrade already confirmed 0 linked accounts.
|
|
#
|
|
# Three view-state branches we need to cover:
|
|
# A) No accounts + never synced → trigger sync, render spinner
|
|
# B) No accounts + synced once, now idle → skip sync, show "no accounts found"
|
|
# C) No accounts + synced once, still syncing → show spinner, do NOT re-queue
|
|
|
|
test "setup_accounts triggers sync and shows spinner when item has no accounts and has never been synced" do
|
|
# Pre-condition: no snaptrade_accounts and no completed syncs (last_synced_at is nil)
|
|
@snaptrade_item.snaptrade_accounts.destroy_all
|
|
@snaptrade_item.syncs.destroy_all
|
|
|
|
assert_difference "Sync.count", 1 do
|
|
get setup_accounts_snaptrade_item_url(@snaptrade_item)
|
|
end
|
|
|
|
assert_response :success
|
|
assert_select "#snaptrade-sync-spinner", count: 1, message: "Expected the spinner to be shown on first visit with no accounts"
|
|
assert_select ".no-accounts-found", count: 0, message: "Expected the no-accounts UI to be hidden while syncing"
|
|
end
|
|
|
|
test "setup_accounts shows no-accounts-found state after a completed sync returns zero accounts" do
|
|
# Pre-condition: no snaptrade_accounts, but there IS a past completed sync
|
|
@snaptrade_item.snaptrade_accounts.destroy_all
|
|
@snaptrade_item.syncs.destroy_all
|
|
@snaptrade_item.syncs.create!(status: :completed, completed_at: 1.minute.ago)
|
|
|
|
# Item is not currently syncing → @syncing is false
|
|
assert_not @snaptrade_item.reload.syncing?, "Item should not be syncing for this test"
|
|
|
|
assert_no_difference "Sync.count" do
|
|
get setup_accounts_snaptrade_item_url(@snaptrade_item)
|
|
end
|
|
|
|
assert_response :success
|
|
assert_select ".no-accounts-found", count: 1, message: "Expected the no-accounts UI to be shown after a completed sync with zero accounts"
|
|
assert_select "#snaptrade-sync-spinner", count: 0, message: "Expected the spinner to be hidden when there is no active sync"
|
|
end
|
|
|
|
test "setup_accounts does not re-queue a sync when a sync is already in progress" do
|
|
# Pre-condition: no accounts, one past completed sync, + one visible (in-flight) sync
|
|
@snaptrade_item.snaptrade_accounts.destroy_all
|
|
@snaptrade_item.syncs.destroy_all
|
|
@snaptrade_item.syncs.create!(status: :completed, completed_at: 5.minutes.ago)
|
|
@snaptrade_item.syncs.create!(status: :pending, created_at: 1.minute.ago) # visible/in-flight
|
|
|
|
assert @snaptrade_item.reload.syncing?, "Item should be syncing for this test"
|
|
|
|
assert_no_difference "Sync.count" do
|
|
get setup_accounts_snaptrade_item_url(@snaptrade_item)
|
|
end
|
|
|
|
assert_response :success
|
|
assert_select "#snaptrade-sync-spinner", count: 1, message: "Expected the spinner to be shown while sync is in progress"
|
|
assert_select ".no-accounts-found", count: 0, message: "Expected the no-accounts UI to be hidden while a sync is active"
|
|
end
|
|
end
|