mirror of
https://github.com/we-promise/sure.git
synced 2026-09-09 00:24:15 +00:00
* Add Trade Republic provider integration
Introduce authenticated web and QR login, resilient account synchronization, deterministic financial imports, account discovery, and provider diagnostics. Keep login state encrypted, PINs transient, and incomplete provider responses non-destructive.
* Address Trade Republic review findings
Keep QR-authenticated sessions syncable, preserve historical holding snapshots, correct dividend direction, handle unpriced positions safely, localize repair feedback, and align provider controls with the design system.
* Add Trade Republic translations for supported locales
* Restore German Trade Republic account labels
* Resolve remaining Trade Republic review findings
* Resolve remaining Trade Republic review findings
* Address latest Trade Republic review feedback
* Refactor Trade Republic panel buttons to use DS::Button component and add integration tests
* Fix 100x money inflation and missing positions locale key in TR views
Money.new takes major units, so multiplying by 100 displayed EUR 12.34
as EUR 1234 in the holdings category cards and expense summary. Also
add the pluralized holdings.index.positions key that t(".positions")
resolves to (previously only defined at the unused holdings.positions
root level), across all 18 locales.
* fix(db): repair merge artifacts in schema and migrations
- Remove duplicated icon/progress_basis columns on goals in schema.rb
- Renumber Trade Republic migrations to unique versions (clashed with
main's 20260824120000_add_lifecycle_to_goals)
- Bump schema version to match latest migration
* Address remaining Trade Republic review feedback
* fix(trade-republic): address open PR #3168 review findings\n\n- Reject authenticated sessions without a securities account number so a\n blank account does not mark the item connected on a broken session.\n- Derive a missing trade amount from |quantity| x price, and a missing\n price from the resolved amount, without changing the signed import amount.\n- Regenerate db/schema.rb so the Trade Republic item/account tables and\n indexes are present; a fresh test database was otherwise missing the\n tables even though the migrations were marked up.\n
* fix(trade-republic): localize activity labels in ActivitiesProcessor (i18n)
* fix(trade-republic): localize activity labels in ActivitiesProcessor (i18n)
* fix(trade-republic): add activity labels i18n keys to all locale files
* Fix Trade Republic PR review follow-ups
* fix(trade-republic): i18n-aware category guard and ignore generated graphify cache
- Category matcher skipped core deposit/withdrawal labels; guard now compares
against translated values so German etc skip correctly
- Remove committed graphify-out cache and ignore dir
* Protect holdings from malformed snapshots
* Consolidate Trade Republic migrations
* Address final Trade Republic review comments
* Address final Trade Republic review comments
- Remove hard-coded category matcher (merchant keyword taxonomy) and leave Trade Republic transactions uncategorized when no structured category exists; rely on Sure rules/AI
- Revert shared ProviderImportAdapter# import_trade extra: param; handle Trade Republic trade metadata locally in ActivitiesProcessor via post-import Trade extra merge (preserve existing extra, deep_merge)
- Preserve Trade Republic product distinctions (cash, brokerage/private_markets/interest_products/crypto_wallet via portfolio categories) without collapsing account kinds
---------
Co-authored-by: Aland Baban <snow@iBananaMac.fritz.box>
637 lines
24 KiB
Ruby
637 lines
24 KiB
Ruby
require "test_helper"
|
|
|
|
class Settings::ProvidersControllerTest < ActionDispatch::IntegrationTest
|
|
include ActiveJob::TestHelper
|
|
|
|
setup do
|
|
ensure_tailwind_build
|
|
sign_in users(:family_admin)
|
|
|
|
# Ensure provider adapters are loaded for all tests
|
|
Provider::Factory.ensure_adapters_loaded
|
|
end
|
|
|
|
test "GET /settings/bank_sync redirects permanently to /settings/providers" do
|
|
get "/settings/bank_sync"
|
|
assert_redirected_to "/settings/providers"
|
|
assert_equal 301, response.status
|
|
end
|
|
|
|
test "can access when self hosting is disabled (managed mode)" do
|
|
Rails.configuration.stubs(:app_mode).returns("managed".inquiry)
|
|
get settings_providers_url
|
|
assert_response :success
|
|
|
|
patch settings_providers_url, params: { setting: { plaid_client_id: "test123" } }
|
|
assert_redirected_to settings_providers_url
|
|
end
|
|
|
|
test "should get show when self hosting is enabled" do
|
|
with_self_hosting do
|
|
get settings_providers_url
|
|
assert_response :success
|
|
end
|
|
end
|
|
|
|
test "shows configured Brex connections in bank sync settings" do
|
|
get settings_providers_url
|
|
|
|
assert_response :success
|
|
assert_includes response.body, "Brex"
|
|
assert_includes response.body, "Test Brex Connection"
|
|
assert_includes response.body, "brex-providers-panel"
|
|
end
|
|
|
|
test "shows Brex as available when family has no Brex connections" do
|
|
sign_in users(:empty)
|
|
|
|
get settings_providers_url
|
|
|
|
assert_response :success
|
|
assert_includes response.body, "Brex"
|
|
assert_includes response.body, I18n.t("settings.providers.taglines.brex")
|
|
assert_includes response.body, connect_form_settings_providers_path(provider_key: "brex")
|
|
refute_includes response.body, "Test Brex Connection"
|
|
end
|
|
|
|
test "sync all control submits with POST" do
|
|
SimplefinItem.create!(
|
|
family: families(:dylan_family),
|
|
name: "Test SimpleFIN Sync All Control",
|
|
access_url: "https://bridge.simplefin.org/simplefin/access"
|
|
)
|
|
|
|
with_self_hosting do
|
|
get settings_providers_url
|
|
assert_response :success
|
|
assert_select "form[action=?][method=?]", sync_all_settings_providers_path, "post"
|
|
end
|
|
end
|
|
|
|
test "correctly identifies declared vs dynamic fields" do
|
|
# All current provider fields are dynamic, but the logic should correctly
|
|
# distinguish between declared and dynamic fields
|
|
with_self_hosting do
|
|
# plaid_client_id is a dynamic field (not defined in Setting)
|
|
refute Setting.singleton_class.method_defined?(:plaid_client_id=),
|
|
"plaid_client_id= should NOT be defined on Setting's singleton class"
|
|
|
|
# openai_model IS a declared field (defined in Setting)
|
|
# but it's not a provider field, so it won't go through this controller
|
|
assert Setting.singleton_class.method_defined?(:openai_model=),
|
|
"openai_model= should be defined on Setting's singleton class"
|
|
end
|
|
end
|
|
|
|
test "updates dynamic provider fields using batch update" do
|
|
# plaid_client_id is a dynamic field, stored as an individual entry
|
|
with_self_hosting do
|
|
# Clear any existing plaid settings
|
|
Setting["plaid_client_id"] = nil
|
|
|
|
patch settings_providers_url, params: {
|
|
setting: { plaid_client_id: "test_client_id" }
|
|
}
|
|
|
|
assert_redirected_to settings_providers_url
|
|
assert_equal "test_client_id", Setting["plaid_client_id"]
|
|
end
|
|
end
|
|
|
|
test "batches multiple dynamic fields from same provider atomically" do
|
|
# Test that multiple fields from Plaid are updated as individual entries
|
|
with_self_hosting do
|
|
# Clear existing fields
|
|
Setting["plaid_client_id"] = nil
|
|
Setting["plaid_secret"] = nil
|
|
Setting["plaid_environment"] = nil
|
|
|
|
patch settings_providers_url, params: {
|
|
setting: {
|
|
plaid_client_id: "new_client_id",
|
|
plaid_secret: "new_secret",
|
|
plaid_environment: "production"
|
|
}
|
|
}
|
|
|
|
assert_redirected_to settings_providers_url
|
|
|
|
# All three should be present as individual entries
|
|
assert_equal "new_client_id", Setting["plaid_client_id"]
|
|
assert_equal "new_secret", Setting["plaid_secret"]
|
|
assert_equal "production", Setting["plaid_environment"]
|
|
end
|
|
end
|
|
|
|
test "batches dynamic fields from multiple providers atomically" do
|
|
# Test that fields from different providers are stored as individual entries
|
|
with_self_hosting do
|
|
# Clear existing fields
|
|
Setting["plaid_client_id"] = nil
|
|
Setting["plaid_secret"] = nil
|
|
Setting["plaid_eu_client_id"] = nil
|
|
Setting["plaid_eu_secret"] = nil
|
|
|
|
patch settings_providers_url, params: {
|
|
setting: {
|
|
plaid_client_id: "plaid_client",
|
|
plaid_secret: "plaid_secret",
|
|
plaid_eu_client_id: "plaid_eu_client",
|
|
plaid_eu_secret: "plaid_eu_secret"
|
|
}
|
|
}
|
|
|
|
assert_redirected_to settings_providers_url
|
|
|
|
# All fields should be present
|
|
assert_equal "plaid_client", Setting["plaid_client_id"]
|
|
assert_equal "plaid_secret", Setting["plaid_secret"]
|
|
assert_equal "plaid_eu_client", Setting["plaid_eu_client_id"]
|
|
assert_equal "plaid_eu_secret", Setting["plaid_eu_secret"]
|
|
end
|
|
end
|
|
|
|
test "preserves existing dynamic fields when updating new ones" do
|
|
# Test that updating some fields doesn't overwrite other existing fields
|
|
with_self_hosting do
|
|
# Set initial fields
|
|
Setting["existing_field_1"] = "value1"
|
|
Setting["plaid_client_id"] = "old_client_id"
|
|
|
|
# Update one field and add a new one
|
|
patch settings_providers_url, params: {
|
|
setting: {
|
|
plaid_client_id: "new_client_id",
|
|
plaid_secret: "new_secret"
|
|
}
|
|
}
|
|
|
|
assert_redirected_to settings_providers_url
|
|
|
|
# Existing unrelated field should still be there
|
|
assert_equal "value1", Setting["existing_field_1"]
|
|
|
|
# Updated field should have new value
|
|
assert_equal "new_client_id", Setting["plaid_client_id"]
|
|
|
|
# New field should be added
|
|
assert_equal "new_secret", Setting["plaid_secret"]
|
|
end
|
|
end
|
|
|
|
test "skips placeholder values for secret fields" do
|
|
with_self_hosting do
|
|
# Set an initial secret value
|
|
Setting["plaid_secret"] = "real_secret"
|
|
|
|
# Try to update with placeholder
|
|
patch settings_providers_url, params: {
|
|
setting: {
|
|
plaid_client_id: "new_client_id",
|
|
plaid_secret: "********" # Placeholder value
|
|
}
|
|
}
|
|
|
|
assert_redirected_to settings_providers_url
|
|
|
|
# Client ID should be updated
|
|
assert_equal "new_client_id", Setting["plaid_client_id"]
|
|
|
|
# Secret should remain unchanged
|
|
assert_equal "real_secret", Setting["plaid_secret"]
|
|
end
|
|
end
|
|
|
|
test "converts blank values to nil and removes from dynamic_fields" do
|
|
with_self_hosting do
|
|
# Set initial values
|
|
Setting["plaid_client_id"] = "old_value"
|
|
assert_equal "old_value", Setting["plaid_client_id"]
|
|
assert Setting.key?("plaid_client_id")
|
|
|
|
patch settings_providers_url, params: {
|
|
setting: { plaid_client_id: " " } # Blank string with spaces
|
|
}
|
|
|
|
assert_redirected_to settings_providers_url
|
|
assert_nil Setting["plaid_client_id"]
|
|
# Entry should be removed, not just set to nil
|
|
refute Setting.key?("plaid_client_id"),
|
|
"nil values should delete the entry"
|
|
end
|
|
end
|
|
|
|
test "handles sequential updates to different dynamic fields safely" do
|
|
# This test simulates what would happen if two requests tried to update
|
|
# different dynamic fields sequentially. With individual entries,
|
|
# all changes should be preserved without conflicts.
|
|
with_self_hosting do
|
|
Setting["existing_field"] = "existing_value"
|
|
|
|
# Simulate first request updating plaid fields
|
|
patch settings_providers_url, params: {
|
|
setting: {
|
|
plaid_client_id: "client_id_1",
|
|
plaid_secret: "secret_1"
|
|
}
|
|
}
|
|
|
|
# Existing field should still be there
|
|
assert_equal "existing_value", Setting["existing_field"]
|
|
|
|
# New fields should be added
|
|
assert_equal "client_id_1", Setting["plaid_client_id"]
|
|
assert_equal "secret_1", Setting["plaid_secret"]
|
|
|
|
# Simulate second request updating different plaid fields
|
|
patch settings_providers_url, params: {
|
|
setting: {
|
|
plaid_environment: "production"
|
|
}
|
|
}
|
|
|
|
# All previously set fields should still be there
|
|
assert_equal "existing_value", Setting["existing_field"]
|
|
assert_equal "client_id_1", Setting["plaid_client_id"]
|
|
assert_equal "secret_1", Setting["plaid_secret"]
|
|
assert_equal "production", Setting["plaid_environment"]
|
|
end
|
|
end
|
|
|
|
test "only processes valid configuration fields" do
|
|
with_self_hosting do
|
|
# Try to update a field that doesn't exist in any provider configuration
|
|
patch settings_providers_url, params: {
|
|
setting: {
|
|
plaid_client_id: "valid_field",
|
|
fake_field_that_does_not_exist: "should_be_ignored"
|
|
}
|
|
}
|
|
|
|
assert_redirected_to settings_providers_url
|
|
|
|
# Valid field should be updated
|
|
assert_equal "valid_field", Setting["plaid_client_id"]
|
|
|
|
# Invalid field should not be stored
|
|
assert_nil Setting["fake_field_that_does_not_exist"]
|
|
end
|
|
end
|
|
|
|
test "calls reload_configuration on updated providers" do
|
|
with_self_hosting do
|
|
# Mock the adapter class to verify reload_configuration is called
|
|
Provider::PlaidAdapter.expects(:reload_configuration).once
|
|
|
|
patch settings_providers_url, params: {
|
|
setting: { plaid_client_id: "new_client_id" }
|
|
}
|
|
|
|
assert_redirected_to settings_providers_url
|
|
end
|
|
end
|
|
|
|
test "reloads configuration for multiple providers when updated" do
|
|
with_self_hosting do
|
|
# Both Plaid providers (US and EU) should have their configuration reloaded
|
|
Provider::PlaidAdapter.expects(:reload_configuration).once
|
|
Provider::PlaidEuAdapter.expects(:reload_configuration).once
|
|
|
|
patch settings_providers_url, params: {
|
|
setting: {
|
|
plaid_client_id: "plaid_client",
|
|
plaid_eu_client_id: "plaid_eu_client"
|
|
}
|
|
}
|
|
|
|
assert_redirected_to settings_providers_url
|
|
end
|
|
end
|
|
|
|
test "logs errors when update fails" do
|
|
with_self_hosting do
|
|
# Test that errors during update are properly logged and handled gracefully
|
|
# We'll force an error by making the []= method raise
|
|
Setting.expects(:[]=).with("plaid_client_id", "test").raises(StandardError.new("Database error")).once
|
|
|
|
# Mock logger to verify error is logged (pin both the exception class
|
|
# name and the message so a regression that drops one still fails).
|
|
Rails.logger.expects(:error).with(regexp_matches(/Failed to update provider settings: StandardError - Database error/)).once
|
|
|
|
patch settings_providers_url, params: {
|
|
setting: { plaid_client_id: "test" }
|
|
}
|
|
|
|
# Controller should handle the error gracefully with generic message (no internal details)
|
|
assert_response :unprocessable_entity
|
|
assert_equal "Failed to update provider settings. Please try again.", flash[:alert]
|
|
end
|
|
end
|
|
|
|
test "shows no changes message when no fields are updated" do
|
|
with_self_hosting do
|
|
# Only send a secret field with placeholder value (which gets skipped)
|
|
Setting["plaid_secret"] = "existing_secret"
|
|
|
|
patch settings_providers_url, params: {
|
|
setting: { plaid_secret: "********" }
|
|
}
|
|
|
|
assert_redirected_to settings_providers_url
|
|
assert_equal "No changes were made", flash[:notice]
|
|
end
|
|
end
|
|
|
|
test "POST sync_all enqueues SyncAllProvidersJob" do
|
|
SimplefinItem.create!(
|
|
family: families(:dylan_family),
|
|
name: "Test SimpleFIN Sync All",
|
|
access_url: "https://bridge.simplefin.org/simplefin/access"
|
|
)
|
|
families(:dylan_family).update_column(:last_sync_all_attempted_at, nil)
|
|
|
|
assert_enqueued_with(job: SyncAllProvidersJob) do
|
|
post sync_all_settings_providers_path
|
|
end
|
|
|
|
assert_redirected_to settings_providers_path
|
|
|
|
follow_redirect!
|
|
assert_response :success
|
|
assert_match(/Syncing all connected providers/i, response.body)
|
|
end
|
|
|
|
test "POST sync_all respects recent sync throttle" do
|
|
families(:dylan_family).update_column(:last_sync_all_attempted_at, Time.current)
|
|
|
|
assert_no_enqueued_jobs only: SyncAllProvidersJob do
|
|
post sync_all_settings_providers_path
|
|
end
|
|
|
|
assert_redirected_to settings_providers_path
|
|
assert_equal I18n.t("settings.providers.sync_all_recently"), flash[:notice]
|
|
end
|
|
|
|
test "POST sync for simplefin without an active Simplefin sync enqueues SyncJob" do
|
|
item = SimplefinItem.create!(
|
|
family: families(:dylan_family),
|
|
name: "Test SimpleFIN Per Row Sync",
|
|
access_url: "https://bridge.simplefin.org/simplefin/access"
|
|
)
|
|
Sync.where(syncable_type: "SimplefinItem", syncable_id: item.id).delete_all
|
|
|
|
assert_enqueued_jobs 1, only: SyncJob do
|
|
post sync_provider_settings_providers_path(provider_key: "simplefin")
|
|
end
|
|
|
|
assert_redirected_to settings_providers_path
|
|
|
|
follow_redirect!
|
|
assert_response :success
|
|
assert_match(/Sync started/i, response.body)
|
|
end
|
|
|
|
test "POST sync for brex without an active Brex sync enqueues SyncJob" do
|
|
item = brex_items(:one)
|
|
Sync.where(syncable_type: "BrexItem", syncable_id: item.id).delete_all
|
|
|
|
assert_enqueued_jobs 1, only: SyncJob do
|
|
post sync_provider_settings_providers_path(provider_key: "brex")
|
|
end
|
|
|
|
assert_redirected_to settings_providers_path
|
|
|
|
follow_redirect!
|
|
assert_response :success
|
|
assert_match(/Sync started/i, response.body)
|
|
end
|
|
|
|
test "GET show includes Interactive Brokers in bank sync providers" do
|
|
get settings_providers_url
|
|
|
|
assert_response :success
|
|
assert_match(/Interactive Brokers/i, response.body)
|
|
assert_match(/Flex Query/i, response.body)
|
|
end
|
|
|
|
test "GET show warns on configured provider forms when self-hosted encryption keys are not explicitly configured" do
|
|
Setting["plaid_client_id"] = "test-client-id"
|
|
Setting["plaid_secret"] = "test-secret"
|
|
Rails.configuration.stubs(:app_mode).returns("self_hosted".inquiry)
|
|
ActiveRecordEncryptionConfig.stubs(:explicitly_configured?).returns(false)
|
|
|
|
get settings_providers_url
|
|
|
|
assert_response :success
|
|
assert_includes response.body, I18n.t("settings.providers.provider_setup_encryption_warning.title")
|
|
assert_includes response.body, I18n.t("settings.providers.provider_setup_encryption_warning.message")
|
|
assert_includes response.body, I18n.t("settings.providers.drawer_trust_statement_encryption_unconfigured")
|
|
ensure
|
|
Setting["plaid_client_id"] = nil
|
|
Setting["plaid_secret"] = nil
|
|
end
|
|
|
|
test "GET show hides provider form encryption warning in managed mode" do
|
|
Setting["plaid_client_id"] = "test-client-id"
|
|
Setting["plaid_secret"] = "test-secret"
|
|
Rails.configuration.stubs(:app_mode).returns("managed".inquiry)
|
|
ActiveRecordEncryptionConfig.stubs(:explicitly_configured?).returns(false)
|
|
|
|
get settings_providers_url
|
|
|
|
assert_response :success
|
|
refute_includes response.body, I18n.t("settings.providers.provider_setup_encryption_warning.title")
|
|
refute_includes response.body, I18n.t("settings.providers.provider_setup_encryption_warning.message")
|
|
refute_includes response.body, I18n.t("settings.providers.drawer_trust_statement_encryption_unconfigured")
|
|
ensure
|
|
Setting["plaid_client_id"] = nil
|
|
Setting["plaid_secret"] = nil
|
|
end
|
|
|
|
test "GET connect_form renders Interactive Brokers panel" do
|
|
get connect_form_settings_providers_path(provider_key: "ibkr")
|
|
|
|
assert_response :success
|
|
assert_match(/Interactive Brokers/i, response.body)
|
|
assert_match(/Query ID/i, response.body)
|
|
end
|
|
|
|
test "GET connect_form warns when self-hosted encryption keys are not explicitly configured" do
|
|
Rails.configuration.stubs(:app_mode).returns("self_hosted".inquiry)
|
|
ActiveRecordEncryptionConfig.stubs(:explicitly_configured?).returns(false)
|
|
|
|
get connect_form_settings_providers_path(provider_key: "ibkr")
|
|
|
|
assert_response :success
|
|
assert_includes response.body, I18n.t("settings.providers.provider_setup_encryption_warning.title")
|
|
assert_includes response.body, I18n.t("settings.providers.provider_setup_encryption_warning.message")
|
|
assert_includes response.body, I18n.t("settings.providers.drawer_trust_statement_encryption_unconfigured")
|
|
refute_includes response.body, I18n.t("settings.providers.drawer_trust_statement")
|
|
end
|
|
|
|
test "GET connect_form hides encryption warning when self-hosted encryption keys are configured" do
|
|
Rails.configuration.stubs(:app_mode).returns("self_hosted".inquiry)
|
|
ActiveRecordEncryptionConfig.stubs(:explicitly_configured?).returns(true)
|
|
|
|
get connect_form_settings_providers_path(provider_key: "ibkr")
|
|
|
|
assert_response :success
|
|
refute_includes response.body, I18n.t("settings.providers.provider_setup_encryption_warning.title")
|
|
refute_includes response.body, I18n.t("settings.providers.provider_setup_encryption_warning.message")
|
|
assert_includes response.body, I18n.t("settings.providers.drawer_trust_statement")
|
|
refute_includes response.body, I18n.t("settings.providers.drawer_trust_statement_encryption_unconfigured")
|
|
end
|
|
|
|
test "GET connect_form hides encryption warning in managed mode" do
|
|
Rails.configuration.stubs(:app_mode).returns("managed".inquiry)
|
|
ActiveRecordEncryptionConfig.stubs(:explicitly_configured?).returns(false)
|
|
|
|
get connect_form_settings_providers_path(provider_key: "ibkr")
|
|
|
|
assert_response :success
|
|
refute_includes response.body, I18n.t("settings.providers.provider_setup_encryption_warning.title")
|
|
refute_includes response.body, I18n.t("settings.providers.provider_setup_encryption_warning.message")
|
|
assert_includes response.body, I18n.t("settings.providers.drawer_trust_statement")
|
|
refute_includes response.body, I18n.t("settings.providers.drawer_trust_statement_encryption_unconfigured")
|
|
end
|
|
|
|
test "GET connect_form uses shared encryption warning for provider panels" do
|
|
Rails.configuration.stubs(:app_mode).returns("self_hosted".inquiry)
|
|
ActiveRecordEncryptionConfig.stubs(:explicitly_configured?).returns(false)
|
|
|
|
get connect_form_settings_providers_path(provider_key: "wise")
|
|
|
|
assert_response :success
|
|
assert_includes response.body, I18n.t("settings.providers.provider_setup_encryption_warning.title")
|
|
assert_includes response.body, I18n.t("settings.providers.provider_setup_encryption_warning.message")
|
|
refute_includes response.body, I18n.t("wise_items.provider_panel.encryption_warning.title")
|
|
refute_includes response.body, I18n.t("wise_items.provider_panel.encryption_warning.message")
|
|
assert_includes response.body, I18n.t("settings.providers.drawer_trust_statement_encryption_unconfigured")
|
|
refute_includes response.body, I18n.t("settings.providers.drawer_trust_statement")
|
|
end
|
|
|
|
test "GET show includes Trade Republic in bank sync providers" do
|
|
get settings_providers_url
|
|
|
|
assert_response :success
|
|
assert_match(/Trade Republic/i, response.body)
|
|
assert_match(/Approve the login in your Trade Republic app/i, response.body)
|
|
end
|
|
|
|
test "GET connect_form renders Trade Republic panel" do
|
|
get connect_form_settings_providers_path(provider_key: "trade_republic")
|
|
|
|
assert_response :success
|
|
assert_match(/Trade Republic/i, response.body)
|
|
assert_match(I18n.t("settings.providers.trade_republic_panel.phone_number_label"), response.body)
|
|
end
|
|
|
|
test "GET connect_form for snaptrade shows OAuth setup instructions when instance is not configured" do
|
|
Provider::Snaptrade.stubs(:oauth_configured?).returns(false)
|
|
|
|
get connect_form_settings_providers_path(provider_key: "snaptrade")
|
|
|
|
assert_response :success
|
|
assert_includes response.body, I18n.t("providers.snaptrade.oauth_setup_step_3")
|
|
refute_includes response.body, I18n.t("providers.snaptrade.oauth_connect_button")
|
|
refute_includes response.body, I18n.t("providers.snaptrade.oauth_status_ready")
|
|
end
|
|
|
|
test "GET connect_form for snaptrade shows connect CTA when configured but item is not authorized" do
|
|
sign_in users(:empty)
|
|
Provider::Snaptrade.stubs(:oauth_configured?).returns(true)
|
|
Provider::Snaptrade.stubs(:authorization_code_configured?).returns(true)
|
|
|
|
get connect_form_settings_providers_path(provider_key: "snaptrade")
|
|
|
|
assert_response :success
|
|
assert_includes response.body, I18n.t("providers.snaptrade.oauth_connect_button")
|
|
assert_includes response.body, I18n.t("providers.snaptrade.oauth_status_ready")
|
|
# Both grants are available here, so the device code is offered alongside.
|
|
assert_includes response.body, I18n.t("providers.snaptrade.oauth_device_button")
|
|
refute_includes response.body, I18n.t("providers.snaptrade.oauth_status_authorized")
|
|
refute_includes response.body, I18n.t("providers.snaptrade.oauth_reauthorize_button")
|
|
end
|
|
|
|
test "GET connect_form for snaptrade offers only the device code without a confidential client" do
|
|
sign_in users(:empty)
|
|
Provider::Snaptrade.stubs(:oauth_configured?).returns(true)
|
|
Provider::Snaptrade.stubs(:authorization_code_configured?).returns(false)
|
|
|
|
get connect_form_settings_providers_path(provider_key: "snaptrade")
|
|
|
|
assert_response :success
|
|
assert_includes response.body, I18n.t("providers.snaptrade.oauth_device_button")
|
|
assert_includes response.body, I18n.t("providers.snaptrade.oauth_status_ready_device")
|
|
# The browser redirect needs a client secret this deployment does not have.
|
|
refute_includes response.body, I18n.t("providers.snaptrade.oauth_connect_button")
|
|
end
|
|
|
|
test "GET connect_form for snaptrade shows authorized status and reauthorize CTA when item is connected" do
|
|
# Default signed-in user (family_admin) belongs to dylan_family, which owns
|
|
# the oauth-authorized `configured_item` fixture.
|
|
Provider::Snaptrade.stubs(:oauth_configured?).returns(true)
|
|
Provider::Snaptrade.stubs(:authorization_code_configured?).returns(true)
|
|
|
|
get connect_form_settings_providers_path(provider_key: "snaptrade")
|
|
|
|
assert_response :success
|
|
assert_includes response.body, I18n.t("providers.snaptrade.oauth_status_authorized")
|
|
assert_includes response.body, I18n.t("providers.snaptrade.oauth_reauthorize_button")
|
|
assert_includes response.body, I18n.t("providers.snaptrade.manage_connections")
|
|
refute_includes response.body, I18n.t("providers.snaptrade.oauth_connect_button")
|
|
end
|
|
|
|
test "POST sync for ibkr without an active Ibkr sync enqueues SyncJob" do
|
|
item = ibkr_items(:configured_item)
|
|
Sync.where(syncable_type: "IbkrItem", syncable_id: item.id).delete_all
|
|
|
|
assert_enqueued_jobs 1, only: SyncJob do
|
|
post sync_provider_settings_providers_path(provider_key: "ibkr")
|
|
end
|
|
|
|
assert_redirected_to settings_providers_path
|
|
|
|
follow_redirect!
|
|
assert_response :success
|
|
assert_match(/Sync started/i, response.body)
|
|
end
|
|
|
|
test "non-admin users cannot update providers" do
|
|
with_self_hosting do
|
|
sign_in users(:family_member)
|
|
|
|
patch settings_providers_url, params: {
|
|
setting: { plaid_client_id: "test" }
|
|
}
|
|
|
|
assert_redirected_to root_path
|
|
assert_equal "Not authorized", flash[:alert]
|
|
|
|
# Value should not have changed
|
|
assert_nil Setting["plaid_client_id"]
|
|
end
|
|
end
|
|
|
|
test "uses singleton_class method_defined to detect declared fields" do
|
|
with_self_hosting do
|
|
# This test verifies the difference between respond_to? and singleton_class.method_defined?
|
|
|
|
# openai_model is a declared field
|
|
assert Setting.singleton_class.method_defined?(:openai_model=),
|
|
"openai_model= should be defined on Setting's singleton class"
|
|
assert Setting.respond_to?(:openai_model=),
|
|
"respond_to? should return true for declared field"
|
|
|
|
# plaid_client_id is a dynamic field
|
|
refute Setting.singleton_class.method_defined?(:plaid_client_id=),
|
|
"plaid_client_id= should NOT be defined on Setting's singleton class"
|
|
refute Setting.respond_to?(:plaid_client_id=),
|
|
"respond_to? should return false for dynamic field"
|
|
|
|
# Both methods currently return the same result, but singleton_class.method_defined?
|
|
# is more explicit and reliable for checking if a method is actually defined
|
|
end
|
|
end
|
|
end
|