Commit Graph
11 Commits
Author SHA1 Message Date
Thomas SteiblandJohns 9f7fd6fdef fix(i18n): localize SnapTrade device authorization (#3283)
Co-authored-by: Johns <19662585+Rowdy@users.noreply.github.com>
2026-08-31 04:38:11 +02:00
Brandon bf5ceff269 Fix flaky sign_out teardown in six test suites (#3208)
Six suites (passkey, MFA, SnapTrade, categorize, onboarding and the Active
Storage authorization integration tests) share a sign_out helper that deletes
the user's sessions through the controller, one HTTP request per session,
iterating in unspecified order. The moment the loop deletes the session the
test itself is signed in with, every later request in the loop is
unauthenticated and silently deletes nothing, so whichever sessions happen to
sort after it survive. The sessions fixture belongs to the same user these
suites use, so a surviving fixture row then fails every assertion that expects
the user to have no sessions.

Row order usually favors the fixture, which is why the suites usually pass.
Under parallel CI they fail a few times a week, always in this file family,
always with the fixture session as the leftover. Forcing newest-first order
reproduces it deterministically on current main: ten of the fifteen passkey
tests fail.

Teardown hygiene is not the behavior under test, so the helpers now destroy
the sessions directly, which no order can break. All six suites run green
three times in a row.
2026-08-27 07:30:35 +02:00
Juan José MataandClaude Opus 5 d6462f5fc9 feat(snaptrade): add device-flow OAuth alongside the browser redirect (#3126)
* feat(snaptrade): add device-flow OAuth alongside the browser redirect

SnapTrade could only be connected through the authorization-code + PKCE
flow, which needs a confidential OAuth client: SNAPTRADE_OAUTH_CLIENT_SECRET
and a redirect URI registered on the OAuth app. A deployment that cannot
register one had no path at all.

Add the device grant (RFC 8628) as a second way to obtain the same token,
so people can pick the flow that suits their deployment. Both grants end at
SnaptradeItem#apply_oauth_tokens!, so a device-authorized item is
indistinguishable from a redirect-authorized one from there on -- same
Bearer data calls, refresh, revocation and sync. Nothing about existing
authorized items changes: no schema change, no migration, and the PKCE path
is untouched.

- Provider::Snaptrade gains start_device_authorization and poll_device_token,
  with endpoints read from SnapTrade's OAuth metadata document (cached).
- oauth_configured? now means "some flow is available" (public client id),
  which is what gates syncing and the provider panel; the new
  authorization_code_configured? gates the redirect flow specifically.
- Token and revocation requests authenticate as a public client when no
  secret is configured -- client_id in the body instead of HTTP Basic.
  Without this a device-authorized item would authorize fine and then fail
  at its first token rotation.
- The settings panel offers both when both are available; every other entry
  point picks one through SnaptradeItemsHelper#snaptrade_authorize_path.
- The device page carries a failed attempt's code back into the form, so
  "not confirmed yet" is a retry rather than a restart.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X3f2UyefTKJrgvNjPnhMRk

* fix(snaptrade): keep the provider panel's setup-step keys and cover both flows

Two test_unit failures from the panel change.

The setup steps were reordered and their keys renamed, which orphaned the
translations twelve locales already had for them and broke the test asserting
`oauth_setup_step_3`. The rename bought nothing: reword the steps in place
instead, leaving the callback URL on step 2 where the interpolation lives.

The panel tests stubbed `oauth_configured?`, which no longer decides which
buttons render -- that is now `authorization_code_configured?`. Stub both, so
the "configured" cases test the deployment they name, and add the device-only
case that was previously unreachable.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X3f2UyefTKJrgvNjPnhMRk

* fix(snaptrade): address device-flow review findings

Two real bugs from the bot reviews, plus consistency work.

The completion form posts into the `drawer` frame so errors re-render in place,
but a successful redirect was then followed as a frame navigation. Both
destinations carry the layout's empty `drawer` frame, so Turbo swapped that in
and merely closed the dialog: the notice was lost and `return_to=setup_accounts`
never advanced. Success now breaks out with a redirect stream action, the same
mechanism holdings and categorizes already use, while errors keep rendering in
the drawer.

RFC 8628 §3.1 requires a confidential client to authenticate its device
authorization request, and the panel offers the device code on deployments that
configured a secret. That request now carries the same client authentication as
the token request.

Token endpoint resolution is now shared by all three grants, since whatever
issued a token has to be what refreshes it. It reads the discovery document only
when already cached and never fetches it, so the browser flow keeps working off
the constant it has always used -- no new network call on refresh and no new way
for an existing authorized item to fail.

Also: the drawer no longer asks the provider whether it is configured, the
controller tells it; and the test helpers restore the previous OAuth config
rather than clearing it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X3f2UyefTKJrgvNjPnhMRk

* fix(snaptrade): reject a device authorization response that cannot drive the flow

A 2xx missing device_code, user_code or a verification URI was passed straight
to the drawer, which then rendered a blank code and a link to nowhere -- a dead
end the user could only abandon. Every one of those fields is load-bearing, and
a response without them is partial or schema-changed, so fail with a message
instead. Same reasoning as the results-array check in get_positions.

verification_uri_complete substitutes for verification_uri when present, since
the drawer prefers it for the link anyway.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X3f2UyefTKJrgvNjPnhMRk

* fix(snaptrade): filter device-flow codes from request logs

complete_oauth_device_flow receives the device code as a request parameter,
and none of the existing filter_parameters patterns is a substring of
"device_code" -- ParameterFilter matches on substrings, and "token", "_key",
"secret", "code_verifier" and "code_challenge" all miss it. So Rails' default
"Processing by ... Parameters: {...}" line was writing it in plaintext.

That matters more here than ordinary log hygiene: the device code is the only
capability check on redemption. Unlike the redirect flow's state, nothing binds
a device code to the family that requested it, so anyone who can read the logs
could redeem another family's in-flight authorization into their own item and
pick up a token for that family's brokerage data.

Adds :device_code, :user_code and :verification_uri_complete (which embeds the
user code) to the filter list, with a regression test in the style of the
existing Sophtron credential-filtering test.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X3f2UyefTKJrgvNjPnhMRk

* fix(snaptrade): bind a pending device authorization to its session

The device code was posted back from the drawer as a form field, so the
request body was the only thing deciding which item a pending authorization
redeemed into. Nothing tied a code to the family that asked for it -- the
guarantee `state` gives the redirect flow -- so a code recovered from
anywhere could be redeemed into an item belonging to someone else, handing
them a token for the victim's brokerage data.

Hold the pending authorization in the session instead, where oauth_callback
already keeps its code_verifier and state:

- start_oauth_device_flow records the code, what the page displays, the
  family, the item and the return_to context under :snaptrade_device_flow.
- complete_oauth_device_flow reads the code from there and refuses unless
  the flow was started by this session for this family and this item. A
  device_code parameter is no longer read at all, so there is no longer a
  way to inject one.
- return_to and accountable_type come from the session too, so completion
  needs nothing from the form to find its way back.

The code now never reaches the browser, which also makes the previous
commit's log filtering a second line of defence rather than the only one.

A failed attempt keeps the code only while it is still redeemable: expired_token
and access_denied clear it so the page offers a fresh start, while a transient
failure leaves it in place to retry. expires_in and interval are no longer
carried anywhere, since nothing ever read them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X3f2UyefTKJrgvNjPnhMRk

* fix(snaptrade): use one token endpoint for every grant

poll_device_token resolved the token endpoint from the cached discovery
document while exchange_code and refresh_tokens used TOKEN_URL, so which
URL a device-issued token was refreshed at depended on whether the 12h
metadata cache was still warm. If the discovered endpoint ever differed
from the constant, a device-authorized item would work until the cache
lapsed and then fail its first rotation -- and fail invisibly, since a
refresh failure marks the connection requires_update.

Resolve it by removing the choice rather than by making refresh depend on
discovery. RFC 8628 §3.4 redeems a device code at the authorization
server's token endpoint, the same one the authorization code grant uses:
there is one token endpoint, not one per grant, and nothing to keep in
sync between issuing a token and refreshing it. TOKEN_URL is also the
endpoint the browser flow has been using in production, so it is the one
with evidence behind it. Discovery is still consulted, but only for
device_authorization_endpoint, which has no hardcoded equivalent.

This also keeps refresh free of any network dependency it did not already
have: reintroducing discovery there would have put a fetch, with retries
and backoff, in front of every token rotation on items that never needed
one.

Also restore the previous OAuth configuration in the missing-client-id
test instead of leaving the client id nil, which made it order-dependent.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X3f2UyefTKJrgvNjPnhMRk

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-08-25 04:28:25 +02:00
Maximus Barbare 4c1bc774e5 Fix SnapTrade account setup and reconnection (#2858)
* Infer SnapTrade account types from categories

* Allow choosing SnapTrade account types

* Reauthorize SnapTrade connections needing attention

* Clear stale SnapTrade reconnect state

* Always schedule SnapTrade reconnect syncs

* Recognize SnapTrade credit card accounts

* Recognize SnapTrade crypto account types

* Schedule SnapTrade syncs after active imports

* Fix SnapTrade account type CI tests

* Normalize SnapTrade card account types
2026-07-31 02:20:03 +02:00
Max BarbareandClaude Sonnet 5 51c93649da feat(snaptrade): replace device-flow OAuth with authorization-code + PKCE flow (#2747)
* feat(snaptrade): replace device-flow OAuth with authorization-code + PKCE flow

Squashed from 16 commits on snaptrade-oauth-apps for a clean rebase onto
current upstream/main ahead of opening a PR.

* fix(snaptrade): address PR #2747 review feedback on OAuth PKCE flow

- Remove unreachable dead-code guard in import_latest_snaptrade_data
- Guard apply_oauth_tokens! against a malformed payload missing access_token
- Wrap token endpoint network errors in ApiError and retry like data calls
- Remove unused Provider::Snaptrade#revoke_token! instance method
- Preserve return_to/accountable_type through the SnapTrade portal callback
  so the account-linking flow no longer drops users back to accounts_path
- Show the real absolute OAuth callback URL in self-hosted setup instructions
- Refresh brakeman.ignore fingerprint for the connect redirect after the
  return_to/accountable_type params were added

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y8SCCmKX6RphB5E73WSUQQ

* fix(snaptrade): don't retry non-idempotent OAuth/API requests

CodeRabbit flagged that Provider::Snaptrade retried OAuth token
exchanges/refreshes and all API POST/DELETE calls (get_connection_url,
delete_connection) after timeouts/connection failures. If the response
is lost after SnapTrade already consumed a single-use auth code,
rotated the refresh token, or applied a POST/DELETE, replaying the
request either fails with invalid_grant on a token that actually
succeeded, or risks duplicate side effects. Retries are now limited to
GET requests; OAuth token requests and non-GET API calls translate a
network failure straight into an ApiError without replay.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NrrGkgSBEqhjjBmmH1fcXL

* fix(snaptrade): stop querying non-deterministically encrypted token via empty-string compare

CodeRabbit flagged that the syncable scope's where.not(oauth_access_token:
[nil, ""]) re-encrypts "" with a random IV on every query, so the ""
comparison can never match a stored ciphertext and is a silent no-op.
No code path ever persists oauth_access_token as "" (only nil or a real
token via apply_oauth_tokens!), so the exclusion is unnecessary --
narrowed the scope to a plain NULL check, which encryption handles
transparently since nil is never encrypted.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NrrGkgSBEqhjjBmmH1fcXL

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-24 22:45:44 +02:00
Juan José Mata b3f70c8951 feat:Add SnapTrade OAuth device flow (#2523)
* Add SnapTrade OAuth connection flow

* Restore SnapTrade brokerage portal links

* Guard SnapTrade OAuth setup completion

* Move SnapTrade OAuth start to POST

* Use one SnapTrade item in provider panel

* Fix SnapTrade OAuth controller tests

* Fix SnapTrade OAuth drawer completion redirect

* Update SnapTrade limits message.

* Restrict SnapTrade OAuth scopes
2026-06-29 00:30:03 +02:00
Juan José Mata b416618558 Add SnapTrade OAuth device flow (#2494)
* 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.
2026-06-27 06:43:12 +02:00
Sure Admin (bot)andSureBot e677d382c2 fix: send first-time SnapTrade users to connect flow (#1613)
* fix: route unregistered SnapTrade users to connect flow

* test: fix snaptrade controller sign-out helper

* fix: prefer active registered snaptrade items

* test: avoid Current.family outside request cycle

* fix: preserve snaptrade resume flow

* fix: read snaptrade resume session with indifferent keys

---------

Co-authored-by: SureBot <sure-bot@we-promise.com>
2026-05-03 10:28:31 +02:00
Anas Limouri 998ea6f7c5 fix: Resolve infinite sync loop on SnapTrade setup accounts page (#1256)
* Fix infinite sync loop on SnapTrade setup accounts page

* Address PR feedback: behavior assertions & stale sync recovery

- Uses `latest_sync.completed?` so we don't drop dropped/failed syncs
- Replaces `assigns` checks with `assert_select` DOM checks
- Adds required IDs/classes to the html template for assertions
2026-03-23 14:22:37 +01:00
LPW 24981ffd52 Add "Link to existing" option in SnapTrade Setup Accounts modal (#935)
* Add account linking functionality for SnapTrade items

- Introduced UI to link existing accounts when setting up SnapTrade items, preventing duplicate account creation.
- Updated controller to fetch linkable accounts.
- Added tests to verify proper filtering of accounts and linking behavior.

* Add `snaptrade_item_id` to account linking flow for SnapTrade items

- Updated controller to allow specifying `snaptrade_item_id` when linking accounts.
- Adjusted form and views to include `snaptrade_item_id` as a hidden field.
- Enhanced tests to validate behavior with the new parameter.
2026-02-08 10:30:46 +01:00
LPW 01c2209492 Add tests for SnapTrade error handling and refine unlink behavior (#931)
- Introduced new tests to cover SnapTrade decryption and connection errors in `SnaptradeItemsControllerTest`.
- Updated error messages for improved user clarity.
- Modified `unlink` functionality to preserve `SnaptradeAccount` records while ensuring proper detachment of associated holdings.
2026-02-07 23:15:18 +01:00