Files
sure/test/models/simplefin_item_test.rb
William Wei Ming f210d1ca4c Perf/accounts controller index optimization (#1926)
* fix Prism issue - assigned but unused variable plaid_item

* measurement to improve accounts_controller__index performance based on skylight

* fix FEEDBACK - Stop eager-loading full sync histories on accounts index

* fix FEEDBACK - Remove sticky memoization from SimplefinItem#accounts

* fix lint error in PR review

* fix FEEDBACK - Avoid returning map misses as authoritative results

* fix test error

* fix FEEDBACK - Avoid redundant query by using already-loaded @manual_accounts

* Address maintainer review on accounts index sync preloading
Memoize SimplefinItem#accounts, align syncing? with Sync#visible?,
and add fallback regression tests.

* Add composite index for sync DISTINCT ON queries
Support latest_by_syncable ordering with syncable_type, syncable_id,
created_at DESC, and id DESC.

* fix error in dockerfile.preview file

* Suppress Pipelock false positives on CI database fixtures.
Pipelock 2.7.0 full-repo audit flags ephemeral postgres:// URLs in
workflow env blocks. Re-apply inline suppressions lost in the main merge.

* Address review: guard partial Current sync maps and drop unrelated diff
Add key? checks so partially populated Current sync maps fall back to DB
queries. Revert Dockerfile.preview and pipelock.yml changes unrelated to
the accounts index N+1 optimization.

* fix(sync): fully populate Current sync maps for all preloaded syncables

* fix(ci): skip scheduled preview cleanup on forks
Only run the hourly Cloudflare preview cleanup on we-promise/sure,
where the required secrets exist.

* Remove schema.rb Postgres-version churn, keep only new syncs index

Reset db/schema.rb to upstream/main and re-add only the
index_syncs_on_syncable_and_created_at_and_id index. The prior diff
included ~30 lines of noise (check-constraint reformatting, virtual
column reformat, and column reorderings) caused by dumping under a
different PostgreSQL version, which obscured the single intended
schema change and invited merge conflicts.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-25 03:35:35 +02:00

259 lines
7.8 KiB
Ruby

require "test_helper"
class SimplefinItemTest < ActiveSupport::TestCase
setup do
@family = families(:dylan_family)
@simplefin_item = SimplefinItem.create!(
family: @family,
name: "Test SimpleFin Connection",
access_url: "https://example.com/access_token"
)
end
test "belongs to family" do
assert_equal @family, @simplefin_item.family
end
test "has many simplefin_accounts" do
account = @simplefin_item.simplefin_accounts.create!(
name: "Test Account",
account_id: "test_123",
currency: "USD",
account_type: "checking",
current_balance: 1000.00
)
assert_includes @simplefin_item.simplefin_accounts, account
end
test "has good status by default" do
assert_equal "good", @simplefin_item.status
end
test "setup token update is required when requires_update has no successful account sync" do
@simplefin_item.update!(status: :requires_update)
Sync.create!(
syncable: @simplefin_item,
status: "failed",
failed_at: Time.current,
error: "SimpleFIN access forbidden",
sync_stats: { "total_accounts" => 0 }
)
assert @simplefin_item.setup_token_update_required?
assert_equal "requires_update", @simplefin_item.effective_status
assert_includes @simplefin_item.attention_summary, "Connection needs update"
end
test "setup token update is not required when latest sync returned accounts" do
@simplefin_item.update!(status: :requires_update, pending_account_setup: true)
# Must be a terminal sync: a pending/in-progress sync short-circuits before
# the account-count check, so this would pass without exercising the branch.
Sync.create!(
syncable: @simplefin_item,
status: "completed",
completed_at: Time.current,
sync_stats: {
"total_accounts" => 18,
"error_buckets" => { "auth" => 1 },
"errors" => [ "Connection to Cash App may need attention. Auth required" ]
}
)
refute @simplefin_item.setup_token_update_required?
assert_equal "good", @simplefin_item.effective_status
refute_includes @simplefin_item.attention_summary, "Connection needs update"
assert_includes @simplefin_item.attention_summary, "Accounts need setup"
end
test "setup token update is not required when latest sync stats use symbol keys" do
@simplefin_item.update!(status: :requires_update)
latest_sync = Sync.new(
syncable: @simplefin_item,
status: "completed",
completed_at: Time.current,
sync_stats: {
total_accounts: 18,
error_buckets: { auth: 1 }
}
)
refute @simplefin_item.setup_token_update_required?(latest_sync:)
assert_equal "good", @simplefin_item.effective_status(latest_sync:)
end
test "setup token update is not required while latest sync is unresolved" do
@simplefin_item.update!(status: :requires_update)
Sync.create!(
syncable: @simplefin_item,
status: "pending",
sync_stats: {
"import_started" => true
}
)
refute @simplefin_item.setup_token_update_required?
assert_equal "good", @simplefin_item.effective_status
end
test "setup token update is required when latest sync is stale without accounts" do
@simplefin_item.update!(status: :requires_update)
Sync.create!(
syncable: @simplefin_item,
status: "stale",
sync_stats: {
"import_started" => true
}
)
assert @simplefin_item.setup_token_update_required?
assert_equal "requires_update", @simplefin_item.effective_status
end
test "can be marked for deletion" do
refute @simplefin_item.scheduled_for_deletion?
@simplefin_item.destroy_later
assert @simplefin_item.scheduled_for_deletion?
end
test "is syncable" do
assert_respond_to @simplefin_item, :sync_later
assert_respond_to @simplefin_item, :syncing?
end
test "memoizes accounts across repeated calls" do
queries = capture_sql_queries do
@simplefin_item.accounts
@simplefin_item.accounts
end
assert_equal 1, queries.grep(/FROM "simplefin_accounts"/).size
end
test "scopes work correctly" do
# Create one for deletion
item_for_deletion = SimplefinItem.create!(
family: @family,
name: "Delete Me",
access_url: "https://example.com/delete_token",
scheduled_for_deletion: true
)
active_items = SimplefinItem.active
ordered_items = SimplefinItem.ordered
assert_includes active_items, @simplefin_item
refute_includes active_items, item_for_deletion
assert_equal [ @simplefin_item, item_for_deletion ].sort_by(&:created_at).reverse,
ordered_items.to_a
end
test "upserts institution data correctly" do
org_data = {
id: "bank123",
name: "Test Bank",
domain: "testbank.com",
url: "https://testbank.com",
"sfin-url": "https://sfin.testbank.com"
}
@simplefin_item.upsert_institution_data!(org_data)
assert_equal "bank123", @simplefin_item.institution_id
assert_equal "Test Bank", @simplefin_item.institution_name
assert_equal "testbank.com", @simplefin_item.institution_domain
assert_equal "https://testbank.com", @simplefin_item.institution_url
assert_equal org_data.stringify_keys, @simplefin_item.raw_institution_payload
end
test "institution display name fallback works" do
# No institution data
assert_equal @simplefin_item.name, @simplefin_item.institution_display_name
# With institution name
@simplefin_item.update!(institution_name: "Chase Bank")
assert_equal "Chase Bank", @simplefin_item.institution_display_name
# With domain fallback
@simplefin_item.update!(institution_name: nil, institution_domain: "chase.com")
assert_equal "chase.com", @simplefin_item.institution_display_name
end
test "connected institutions returns unique institutions" do
# Create accounts with different institutions
account1 = @simplefin_item.simplefin_accounts.create!(
name: "Checking",
account_id: "acc1",
currency: "USD",
account_type: "checking",
current_balance: 1000,
org_data: { "name" => "Chase Bank", "domain" => "chase.com" }
)
account2 = @simplefin_item.simplefin_accounts.create!(
name: "Savings",
account_id: "acc2",
currency: "USD",
account_type: "savings",
current_balance: 2000,
org_data: { "name" => "Wells Fargo", "domain" => "wellsfargo.com" }
)
institutions = @simplefin_item.connected_institutions
assert_equal 2, institutions.count
assert_includes institutions.map { |i| i["name"] }, "Chase Bank"
assert_includes institutions.map { |i| i["name"] }, "Wells Fargo"
end
test "institution summary with multiple institutions" do
# No institutions
assert_equal "No institutions connected", @simplefin_item.institution_summary
# One institution
@simplefin_item.simplefin_accounts.create!(
name: "Checking",
account_id: "acc1",
currency: "USD",
account_type: "checking",
current_balance: 1000,
org_data: { "name" => "Chase Bank" }
)
assert_equal "Chase Bank", @simplefin_item.institution_summary
# Multiple institutions
@simplefin_item.simplefin_accounts.create!(
name: "Savings",
account_id: "acc2",
currency: "USD",
account_type: "savings",
current_balance: 2000,
org_data: { "name" => "Wells Fargo" }
)
assert_equal "2 institutions", @simplefin_item.institution_summary
end
private
def capture_sql_queries
queries = []
callback = lambda do |_name, _start, _finish, _message_id, payload|
sql = payload[:sql].to_s
name = payload[:name].to_s
next if name == "SCHEMA"
next if sql.match?(/\A(?:BEGIN|COMMIT|ROLLBACK)\b/i)
queries << sql
end
ActiveSupport::Notifications.subscribed(callback, "sql.active_record") do
yield
end
queries
end
end