mirror of
https://github.com/we-promise/sure.git
synced 2026-07-27 12:12:13 +00:00
* 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>
259 lines
7.8 KiB
Ruby
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
|