diff --git a/app/controllers/snaptrade_items_controller.rb b/app/controllers/snaptrade_items_controller.rb index e5d771b75..166878370 100644 --- a/app/controllers/snaptrade_items_controller.rb +++ b/app/controllers/snaptrade_items_controller.rb @@ -149,8 +149,12 @@ class SnaptradeItemsController < ApplicationController no_accounts = @unlinked_accounts.blank? && @linked_accounts.blank? - # If no accounts and not syncing, trigger a sync - if no_accounts && !@snaptrade_item.syncing? + # We trigger an initial or recovery sync if there are no accounts, we aren't currently syncing, + # and the last attempt didn't successfully complete. (If it completed and found 0 accounts, we stop here to avoid an infinite loop.) + latest_sync = @snaptrade_item.syncs.ordered.first + should_sync = latest_sync.nil? || !latest_sync.completed? + + if no_accounts && !@snaptrade_item.syncing? && should_sync @snaptrade_item.sync_later end diff --git a/app/views/snaptrade_items/setup_accounts.html.erb b/app/views/snaptrade_items/setup_accounts.html.erb index 4ef329562..363626feb 100644 --- a/app/views/snaptrade_items/setup_accounts.html.erb +++ b/app/views/snaptrade_items/setup_accounts.html.erb @@ -34,7 +34,7 @@ <% if @waiting_for_sync %> <%# Syncing state - show spinner with manual refresh option %> -
+

<%= t("snaptrade_items.setup_accounts.loading", default: "Fetching accounts from SnapTrade...") %> @@ -60,7 +60,7 @@

<% elsif @no_accounts_found %> <%# No accounts found after sync completed %> -
+
<%= icon "alert-circle", size: "lg", class: "text-warning" %>

<%= t("snaptrade_items.setup_accounts.no_accounts_title", default: "No Accounts Found") %> diff --git a/test/controllers/snaptrade_items_controller_test.rb b/test/controllers/snaptrade_items_controller_test.rb index 2011832cb..18048ac2d 100644 --- a/test/controllers/snaptrade_items_controller_test.rb +++ b/test/controllers/snaptrade_items_controller_test.rb @@ -103,4 +103,66 @@ class SnaptradeItemsControllerTest < ActionDispatch::IntegrationTest 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