mirror of
https://github.com/we-promise/sure.git
synced 2026-05-11 14:45:01 +00:00
* Complete Sophtron account mapping * Clarify Sophtron login challenge flow * Add Sophtron connection UI timeout * Treat Sophtron timeout jobs as failed * Reset failed Sophtron connection state * Handle stale Sophtron connection jobs * Advance Sophtron polling timeout * Shorten Sophtron connection timeout * Fix Sophtron modal polling updates * Stabilize Sophtron MFA polling * Give Sophtron OTP challenges more time * Clarify Sophtron institution login failures * Extend Sophtron polling during login progress * Probe Sophtron accounts after completed MFA step * Align Sophtron dialogs with design system * Start Sophtron initial load after linking accounts * Fix Sophtron initial transaction load * Fail Sophtron sync without institution connection * Fix tests * Wrap Sophtron account linking in transaction * Wrap Sophtron provider responses * Fix Sophtron MFA security tests * Guard Sophtron MFA challenge arrays * Respect Sophtron initial load window * Use unique Sophtron MFA answer field ids * Address Sophtron review follow-ups * Fix Sophtron transaction sync refresh * Avoid blocking Sophtron refresh polling * Move Sophtron account helpers to model * Keep Sophtron grouping provider-level * Start new Sophtron institution links * Isolate Sophtron institution connections --------- Signed-off-by: Juan José Mata <juanjo.mata@gmail.com>
166 lines
5.2 KiB
Ruby
166 lines
5.2 KiB
Ruby
require "test_helper"
|
|
|
|
class SophtronItemTest < ActiveSupport::TestCase
|
|
include ActiveJob::TestHelper
|
|
|
|
setup do
|
|
@family = families(:dylan_family)
|
|
@item = @family.sophtron_items.create!(
|
|
name: "Sophtron",
|
|
user_id: "developer-user",
|
|
access_key: Base64.strict_encode64("secret-key")
|
|
)
|
|
end
|
|
|
|
test "ensure_customer reuses persisted customer id" do
|
|
@item.update!(customer_id: "cust-existing")
|
|
provider = mock
|
|
provider.expects(:list_customers).never
|
|
|
|
assert_equal "cust-existing", @item.ensure_customer!(provider: provider)
|
|
end
|
|
|
|
test "ensure_customer reuses matching listed customer" do
|
|
provider = mock
|
|
provider.expects(:list_customers).returns([
|
|
{ CustomerID: "cust-1", CustomerName: @item.generated_customer_name }
|
|
])
|
|
provider.expects(:create_customer).never
|
|
|
|
assert_equal "cust-1", @item.ensure_customer!(provider: provider)
|
|
assert_equal "cust-1", @item.customer_id
|
|
assert_equal @item.generated_customer_name, @item.customer_name
|
|
end
|
|
|
|
test "ensure_customer creates customer when no matching customer exists" do
|
|
provider = mock
|
|
provider.expects(:list_customers).returns([])
|
|
provider.expects(:create_customer)
|
|
.with(unique_id: @item.generated_customer_unique_id, name: @item.generated_customer_name, source: "Sure")
|
|
.returns({ CustomerID: "cust-new", CustomerName: @item.generated_customer_name })
|
|
|
|
assert_equal "cust-new", @item.ensure_customer!(provider: provider)
|
|
assert_equal "cust-new", @item.customer_id
|
|
end
|
|
|
|
test "connected_to_institution ignores failed connection attempts" do
|
|
@item.update!(user_institution_id: "ui-1", status: :requires_update)
|
|
|
|
assert_not @item.connected_to_institution?
|
|
end
|
|
|
|
test "connected_to_institution ignores jobs that are still running" do
|
|
@item.update!(user_institution_id: "ui-1", current_job_id: "job-1", status: :good)
|
|
|
|
assert_not @item.connected_to_institution?
|
|
end
|
|
|
|
test "connected_to_institution ignores stale timeout job snapshots" do
|
|
@item.update!(
|
|
user_institution_id: "ui-1",
|
|
status: :good,
|
|
job_status: "Timeout",
|
|
raw_job_payload: {
|
|
SuccessFlag: false,
|
|
LastStatus: "Timeout"
|
|
}
|
|
)
|
|
|
|
assert_not @item.connected_to_institution?
|
|
end
|
|
|
|
test "provider_display_name keeps accounts grouping provider-level" do
|
|
@item.update!(name: "Bank of America", institution_name: "Bank of America")
|
|
|
|
assert_equal "Sophtron Connection", @item.provider_display_name
|
|
end
|
|
|
|
test "fetch_remote_accounts persists Sophtron account snapshots" do
|
|
@item.update!(user_institution_id: "ui-1")
|
|
provider = mock
|
|
provider.expects(:get_accounts).with("ui-1").returns({
|
|
accounts: [
|
|
{
|
|
id: "acct-1",
|
|
account_id: "acct-1",
|
|
account_name: "Sophtron Checking",
|
|
balance: "123.45",
|
|
balance_currency: "USD",
|
|
currency: "USD"
|
|
}.with_indifferent_access
|
|
],
|
|
total: 1
|
|
})
|
|
@item.stubs(:sophtron_provider).returns(provider)
|
|
|
|
accounts = @item.fetch_remote_accounts(force: true)
|
|
|
|
assert_equal 1, accounts.count
|
|
assert_equal "Sophtron Checking", @item.sophtron_accounts.find_by!(account_id: "acct-1").name
|
|
end
|
|
|
|
test "reject_already_linked removes accounts with existing account provider links" do
|
|
account = accounts(:depository)
|
|
sophtron_account = @item.sophtron_accounts.create!(
|
|
account_id: "acct-1",
|
|
name: "Sophtron Checking",
|
|
currency: "USD",
|
|
balance: 100
|
|
)
|
|
AccountProvider.create!(account: account, provider: sophtron_account)
|
|
|
|
available = @item.reject_already_linked([
|
|
{ id: "acct-1", account_name: "Linked" },
|
|
{ id: "acct-2", account_name: "Available" }
|
|
])
|
|
|
|
assert_equal [ "acct-2" ], available.map { |account_data| SophtronItem.external_account_id(account_data) }
|
|
end
|
|
|
|
test "build_mfa_challenge normalizes Sophtron job challenge fields" do
|
|
challenge = @item.build_mfa_challenge(
|
|
SecurityQuestion: [ "Question?" ].to_json,
|
|
TokenMethod: [ "sms" ].to_json,
|
|
TokenSentFlag: true,
|
|
TokenInputName: "Token",
|
|
TokenRead: "phone",
|
|
CaptchaImage: "YWJj"
|
|
)
|
|
|
|
assert_equal [ "Question?" ], challenge[:security_questions]
|
|
assert_equal [ "sms" ], challenge[:token_methods]
|
|
assert_equal true, challenge[:token_sent]
|
|
assert_equal "phone", challenge[:token_read]
|
|
assert_equal "YWJj", challenge[:captcha_image]
|
|
end
|
|
|
|
test "start_initial_load_later starts a sync when no active sync exists" do
|
|
assert_no_enqueued_jobs only: SophtronInitialLoadJob do
|
|
assert_difference "@item.syncs.count", 1 do
|
|
assert_enqueued_with job: SyncJob do
|
|
@item.start_initial_load_later
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
test "start_initial_load_later seeds sync window for transaction import" do
|
|
@item.update!(sync_start_date: Date.new(2026, 1, 1))
|
|
|
|
@item.start_initial_load_later
|
|
|
|
assert_equal Date.new(2026, 1, 1), @item.syncs.ordered.first.window_start_date
|
|
end
|
|
|
|
test "start_initial_load_later queues a follow-up when current sync is already running" do
|
|
sync = @item.syncs.create!
|
|
sync.start!
|
|
|
|
assert_no_difference "@item.syncs.count" do
|
|
assert_enqueued_with job: SophtronInitialLoadJob do
|
|
@item.start_initial_load_later
|
|
end
|
|
end
|
|
end
|
|
end
|