Files
sure/test/controllers/settings/hostings_controller_test.rb
T
ed6b8b752a fix(hosting): consistent provider-block visibility + fix Twelve Data toggle bug (#3333)
* fix(hosting): consistent provider-block visibility + fix Twelve Data toggle bug (#3089)

T-Invest was the only provider block always rendered regardless of its
checkbox state; now it follows the same pattern as every other provider
(shown when tinkoff_invest or moex_public is enabled, since T-Invest also
serves as a brand-logo fallback for MOEX-priced securities).

Also fixes a related functional bug: unchecking every securities provider
tried to clear the legacy securities_provider setting by assigning nil,
but rails-settings-cached treats nil as "delete override", which silently
reverted the field to its own default ("twelve_data") — re-enabling Twelve
Data right after the user disabled it. Assigning "" instead persists the
cleared state.

Twelve Data and Yahoo Finance settings blocks can still be shown purely
because they're the selected FX/exchange-rate provider even when unchecked
for securities pricing; added an info notice explaining that instead of
leaving it unexplained.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(hosting): address review feedback on #3333

- Drop the FX-only notice for Twelve Data/Yahoo Finance per feedback —
  the block staying visible while unchecked (because it's still the FX
  provider) doesn't need extra UI explanation.
- Fix Codex finding: T-Invest settings must stay visible/manageable
  whenever a token is already configured, not just when tinkoff_invest
  or moex_public is checked. Security::Provided#import_brand_logo calls
  the T-Invest provider unconditionally for every non-crypto security
  once a token exists, regardless of price provider — hiding the field
  in that case would leave an active credential impossible to see,
  rotate, or clear through the UI. Reworded the notice to reflect the
  real, provider-independent reason instead of the narrower "MOEX only"
  framing.
- Fix CodeRabbit finding: setting_test.rb's default-fallback test now
  isolates against SECURITIES_PROVIDER(S) env vars, and the
  explicit-clear test captures and restores the pre-test values instead
  of hardcoding a restore target.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* No overexplaining in code

---------

Co-authored-by: Gerald <248542187+gfr-free@users.noreply.github.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: Juan José Mata <jjmata@jjmata.com>
2026-09-02 19:48:32 +02:00

796 lines
28 KiB
Ruby

require "test_helper"
require "ostruct"
class Settings::HostingsControllerTest < ActionDispatch::IntegrationTest
include ProviderTestHelper
setup do
sign_in users(:family_admin)
@provider = mock
Provider::Registry.stubs(:get_provider).with(:twelve_data).returns(@provider)
@provider.stubs(:health_status).returns(:healthy)
Provider::Registry.stubs(:get_provider).with(:yahoo_finance).returns(@provider)
Provider::Registry.stubs(:get_provider).with(:rentcast).returns(nil)
Provider::Registry.stubs(:get_provider).with(:realie).returns(nil)
@provider.stubs(:usage).returns(provider_success_response(
OpenStruct.new(
used: 10,
limit: 100,
utilization: 10,
plan: "free",
)
))
end
teardown do
# These tests persist global Setting.* values; reset them so state can't
# leak into later (order-dependent) tests.
%i[anthropic_access_token anthropic_base_url anthropic_model llm_provider twelve_data_api_key openai_access_token openai_request_timeout ai_response_timeout external_assistant_token rentcast_api_key realie_api_key].each do |key|
Setting.public_send("#{key}=", nil)
end
end
test "cannot edit when self hosting is disabled" do
@provider.stubs(:usage).returns(@usage_response)
Rails.configuration.stubs(:app_mode).returns("managed".inquiry)
get settings_hosting_url
assert_response :forbidden
patch settings_hosting_url, params: { setting: { onboarding_state: "invite_only" } }
assert_response :forbidden
end
test "should get edit when self hosting is enabled" do
@provider.expects(:usage).returns(@usage_response)
with_self_hosting do
get settings_hosting_url
assert_response :success
end
end
test "can update rentcast api key when self hosting is enabled" do
with_self_hosting do
patch settings_hosting_url, params: { setting: { rentcast_api_key: "rentcast-token" } }
assert_equal "rentcast-token", Setting.rentcast_api_key
end
end
test "can update realie api key when self hosting is enabled" do
with_self_hosting do
patch settings_hosting_url, params: { setting: { realie_api_key: "realie-token" } }
assert_equal "realie-token", Setting.realie_api_key
end
end
test "shows Yahoo Finance rate limiting as a warning" do
@provider.stubs(:health_status).returns(:rate_limited)
with_env_overrides("EXCHANGE_RATE_PROVIDER" => "yahoo_finance") do
with_self_hosting do
get settings_hosting_url
assert_response :success
assert_select "div[class~=?]", "bg-warning/10"
assert_includes response.body, "Yahoo Finance is temporarily rate limiting requests."
assert_includes response.body, "Yahoo Finance rate limit reached."
assert_includes response.body, "No action is required."
assert_not_includes response.body, "firewall"
end
end
end
test "renders healthy unavailable and unknown Yahoo Finance states" do
@provider.stubs(:health_status).returns(:healthy, :unavailable, :unknown)
with_env_overrides("EXCHANGE_RATE_PROVIDER" => "yahoo_finance") do
with_self_hosting do
get settings_hosting_url
assert_includes response.body, "Yahoo Finance is active and working."
assert_select "div[class~=?]", "bg-success"
get settings_hosting_url
assert_includes response.body, "Yahoo Finance is currently unavailable."
assert_includes response.body, "Could not verify Yahoo Finance."
assert_includes response.body, "Check your internet connection and try again later."
assert_not_includes response.body, "firewall"
assert_select "div[class~=?]", "bg-destructive"
get settings_hosting_url
assert_includes response.body, "Yahoo Finance status is being checked."
assert_not_includes response.body, "Could not verify Yahoo Finance."
assert_not_includes response.body, "Yahoo Finance rate limit reached."
assert_select "div[class~=?]", "bg-surface-inset"
end
end
end
test "renders Spanish Yahoo Finance health guidance" do
@provider.stubs(:health_status).returns(:rate_limited, :unavailable, :unknown)
with_env_overrides("EXCHANGE_RATE_PROVIDER" => "yahoo_finance") do
with_self_hosting do
get settings_hosting_url(locale: :es)
assert_includes response.body, "Yahoo Finance está limitando temporalmente las solicitudes."
assert_includes response.body, "No es necesario realizar ninguna acción."
get settings_hosting_url(locale: :es)
assert_includes response.body, "Yahoo Finance no está disponible en este momento."
assert_includes response.body, "Comprueba tu conexión a internet"
get settings_hosting_url(locale: :es)
assert_includes response.body, "Se está comprobando el estado de Yahoo Finance."
end
end
end
test "falls back to English for untranslated Yahoo Finance health guidance" do
@provider.stubs(:health_status).returns(:rate_limited)
with_env_overrides("EXCHANGE_RATE_PROVIDER" => "yahoo_finance") do
with_self_hosting do
get settings_hosting_url(locale: :fr)
assert_includes response.body, "Yahoo Finance is temporarily rate limiting requests."
assert_not_includes response.body, "translation missing"
end
end
end
test "can update settings when self hosting is enabled" do
with_self_hosting do
patch settings_hosting_url, params: { setting: { twelve_data_api_key: "1234567890" } }
assert_equal "1234567890", Setting.twelve_data_api_key
end
end
test "can clear an encrypted api key by submitting a blank value" do
with_self_hosting do
patch settings_hosting_url, params: { setting: { twelve_data_api_key: "1234567890" } }
assert_equal "1234567890", Setting.twelve_data_api_key
patch settings_hosting_url, params: { setting: { twelve_data_api_key: "" } }
assert_nil Setting.twelve_data_api_key
end
end
test "submitting the masked placeholder leaves an encrypted api key unchanged" do
with_self_hosting do
patch settings_hosting_url, params: { setting: { twelve_data_api_key: "1234567890" } }
patch settings_hosting_url, params: { setting: { twelve_data_api_key: "********" } }
assert_equal "1234567890", Setting.twelve_data_api_key
end
end
test "can update onboarding state when self hosting is enabled" do
sign_in users(:sure_support_staff)
with_self_hosting do
patch settings_hosting_url, params: { setting: { onboarding_state: "invite_only" } }
assert_equal "invite_only", Setting.onboarding_state
assert Setting.require_invite_for_signup
patch settings_hosting_url, params: { setting: { onboarding_state: "closed" } }
assert_equal "closed", Setting.onboarding_state
refute Setting.require_invite_for_signup
end
end
test "can update openai access token when self hosting is enabled" do
with_self_hosting do
patch settings_hosting_url, params: { setting: { openai_access_token: "token" } }
assert_equal "token", Setting.openai_access_token
end
end
# Regression: issue #2465 symptom for the OpenAI token. Blanking the field
# (the form auto-submits on blur) must clear the stored value, not silently
# keep the old one.
test "can clear openai access token by submitting a blank value" do
with_self_hosting do
Setting.openai_access_token = "previous-token"
patch settings_hosting_url, params: { setting: { openai_access_token: "" } }
assert_nil Setting.openai_access_token
end
end
test "ignores redacted openai token placeholder" do
with_self_hosting do
Setting.openai_access_token = "previous-token"
patch settings_hosting_url, params: { setting: { openai_access_token: "********" } }
assert_equal "previous-token", Setting.openai_access_token
end
end
test "can update anthropic access token when self hosting is enabled" do
with_self_hosting do
patch settings_hosting_url, params: { setting: { anthropic_access_token: "fake-anthropic-key-for-tests" } }
assert_equal "fake-anthropic-key-for-tests", Setting.anthropic_access_token
end
end
# Regression: issue #2465 symptom for the Anthropic token.
test "can clear anthropic access token by submitting a blank value" do
with_self_hosting do
Setting.anthropic_access_token = "previous-token"
patch settings_hosting_url, params: { setting: { anthropic_access_token: "" } }
assert_nil Setting.anthropic_access_token
end
end
test "ignores redacted anthropic token placeholder" do
with_self_hosting do
Setting.anthropic_access_token = "previous-token"
patch settings_hosting_url, params: { setting: { anthropic_access_token: "********" } }
assert_equal "previous-token", Setting.anthropic_access_token
end
end
test "can update anthropic base_url and model" do
with_self_hosting do
patch settings_hosting_url, params: { setting: { anthropic_base_url: "https://bedrock.example.com", anthropic_model: "claude-opus-4-7" } }
assert_equal "https://bedrock.example.com", Setting.anthropic_base_url
assert_equal "claude-opus-4-7", Setting.anthropic_model
end
end
test "rejects non-URL anthropic base_url" do
with_self_hosting do
Setting.anthropic_base_url = nil
patch settings_hosting_url, params: { setting: { anthropic_base_url: "not-a-url" } }
assert_response :unprocessable_entity
assert_match(/Anthropic Base URL must be an http/, flash[:alert])
assert_nil Setting.anthropic_base_url
end
end
test "clears anthropic base_url when blank value submitted" do
with_self_hosting do
Setting.anthropic_base_url = "https://bedrock.example.com"
patch settings_hosting_url, params: { setting: { anthropic_base_url: "" } }
assert_nil Setting.anthropic_base_url
end
end
test "requires anthropic model when a custom base_url is set" do
with_self_hosting do
Setting.anthropic_base_url = nil
Setting.anthropic_model = nil
patch settings_hosting_url, params: { setting: { anthropic_base_url: "https://bedrock.example.com" } }
assert_response :unprocessable_entity
assert_match(/Anthropic Model is required/, flash[:alert])
assert_nil Setting.anthropic_base_url
end
end
test "can update llm_provider to anthropic" do
with_self_hosting do
patch settings_hosting_url, params: { setting: { llm_provider: "anthropic" } }
assert_equal "anthropic", Setting.llm_provider
end
end
test "falls back to openai when stored llm_provider is invalid" do
with_self_hosting do
Setting.llm_provider = "bogus"
Provider::Openai.stubs(:configured?).returns(false)
get settings_hosting_url
assert_response :success
assert_select "select[name=?] option[selected][value=?]", "setting[llm_provider]", "openai"
assert_no_match(/translation missing/i, @response.body)
end
ensure
Setting.llm_provider = nil
end
test "rejects unknown llm_provider values" do
with_self_hosting do
Setting.llm_provider = "openai"
patch settings_hosting_url, params: { setting: { llm_provider: "bogus" } }
assert_equal "openai", Setting.llm_provider
end
end
test "can update openai uri base and model together when self hosting is enabled" do
with_self_hosting do
patch settings_hosting_url, params: { setting: { openai_uri_base: "https://api.example.com/v1", openai_model: "gpt-4" } }
assert_equal "https://api.example.com/v1", Setting.openai_uri_base
assert_equal "gpt-4", Setting.openai_model
end
end
test "cannot update openai uri base without model when self hosting is enabled" do
with_self_hosting do
Setting.openai_model = ""
patch settings_hosting_url, params: { setting: { openai_uri_base: "https://api.example.com/v1" } }
assert_response :unprocessable_entity
assert_match(/OpenAI model is required/, flash[:alert])
assert Setting.openai_uri_base.blank?, "Expected openai_uri_base to remain blank after failed validation"
end
end
# Regression: issue #1824. The OpenAI form auto-submits on blur, so entering
# the URI base before the model fires a partial submit that fails validation.
# The re-rendered form must show the user's submitted URI base — not the
# still-blank saved value — so they can finish typing the model.
test "preserves submitted openai uri base in form when validation fails" do
with_self_hosting do
Setting.openai_uri_base = nil
Setting.openai_model = ""
patch settings_hosting_url, params: { setting: { openai_uri_base: "https://api.example.com/v1" } }
assert_response :unprocessable_entity
assert_select "input[name=?]", "setting[openai_uri_base]" do |inputs|
assert_equal "https://api.example.com/v1", inputs.first["value"]
end
end
ensure
Setting.openai_uri_base = nil
Setting.openai_model = nil
end
# PR #1862 review (jjmata): symmetric coverage for the model field. When the
# user changes the URI base and clears the model in the same auto-submit, the
# cross-field validation fails — the re-rendered model input must reflect the
# user's submitted (cleared) value, not silently revert to the saved model.
test "preserves submitted openai model in form when validation fails" do
with_self_hosting do
Setting.openai_uri_base = "https://saved.example.com/v1"
Setting.openai_model = "saved-model"
patch settings_hosting_url, params: { setting: {
openai_uri_base: "https://new.example.com/v1",
openai_model: ""
} }
assert_response :unprocessable_entity
assert_select "input[name=?]", "setting[openai_uri_base]" do |inputs|
assert_equal "https://new.example.com/v1", inputs.first["value"]
end
assert_select "input[name=?]", "setting[openai_model]" do |inputs|
assert_not_equal "saved-model", inputs.first["value"].to_s,
"model field must reflect the submitted (cleared) value, not the saved model"
end
end
ensure
Setting.openai_uri_base = nil
Setting.openai_model = nil
end
test "can update openai model alone when self hosting is enabled" do
with_self_hosting do
patch settings_hosting_url, params: { setting: { openai_model: "gpt-4" } }
assert_equal "gpt-4", Setting.openai_model
end
end
test "cannot clear openai model when custom uri base is set" do
with_self_hosting do
Setting.openai_uri_base = "https://api.example.com/v1"
Setting.openai_model = "gpt-4"
patch settings_hosting_url, params: { setting: { openai_model: "" } }
assert_response :unprocessable_entity
assert_match(/OpenAI model is required/, flash[:alert])
assert_equal "gpt-4", Setting.openai_model
end
end
test "can clear data cache when self hosting is enabled" do
account = accounts(:investment)
holding = account.holdings.first
exchange_rate = exchange_rates(:one)
security_price = holding.security.prices.first
account_balance = account.balances.create!(date: Date.current, balance: 1000, currency: "USD")
with_self_hosting do
perform_enqueued_jobs(only: DataCacheClearJob) do
delete clear_cache_settings_hosting_url
end
end
assert_redirected_to settings_hosting_url
assert_equal I18n.t("settings.hostings.clear_cache.cache_cleared"), flash[:notice]
assert_not ExchangeRate.exists?(exchange_rate.id)
assert_not Security::Price.exists?(security_price.id)
assert_not Holding.exists?(holding.id)
assert_not Balance.exists?(account_balance.id)
end
test "can update assistant type to external" do
with_self_hosting do
assert_equal "builtin", users(:family_admin).family.assistant_type
patch settings_hosting_url, params: { family: { assistant_type: "external" } }
assert_redirected_to settings_hosting_url
assert_equal "external", users(:family_admin).family.reload.assistant_type
end
end
test "ignores invalid assistant type values" do
with_self_hosting do
patch settings_hosting_url, params: { family: { assistant_type: "hacked" } }
assert_redirected_to settings_hosting_url
assert_equal "builtin", users(:family_admin).family.reload.assistant_type
end
end
test "ignores assistant type update when ASSISTANT_TYPE env is set" do
with_self_hosting do
with_env_overrides("ASSISTANT_TYPE" => "external") do
patch settings_hosting_url, params: { family: { assistant_type: "external" } }
assert_redirected_to settings_hosting_url
# DB value should NOT change when env override is active
assert_equal "builtin", users(:family_admin).family.reload.assistant_type
end
end
end
test "can update external assistant settings" do
with_self_hosting do
patch settings_hosting_url, params: { setting: {
external_assistant_url: "https://agent.example.com/v1/chat",
external_assistant_token: "my-secret-token",
external_assistant_agent_id: "finance-bot"
} }
assert_redirected_to settings_hosting_url
assert_equal "https://agent.example.com/v1/chat", Setting.external_assistant_url
assert_equal "my-secret-token", Setting.external_assistant_token
assert_equal "finance-bot", Setting.external_assistant_agent_id
end
ensure
Setting.external_assistant_url = nil
Setting.external_assistant_token = nil
Setting.external_assistant_agent_id = nil
end
test "does not overwrite token with masked placeholder" do
with_self_hosting do
Setting.external_assistant_token = "real-secret"
patch settings_hosting_url, params: { setting: { external_assistant_token: "********" } }
assert_equal "real-secret", Setting.external_assistant_token
end
ensure
Setting.external_assistant_token = nil
end
# Regression: issue #2465 symptom for the external assistant token.
test "can clear external assistant token by submitting a blank value" do
with_self_hosting do
Setting.external_assistant_token = "real-secret"
patch settings_hosting_url, params: { setting: { external_assistant_token: "" } }
assert_nil Setting.external_assistant_token
end
ensure
Setting.external_assistant_token = nil
end
test "disconnect external assistant clears settings and resets type" do
with_self_hosting do
with_env_overrides("EXTERNAL_ASSISTANT_URL" => nil, "EXTERNAL_ASSISTANT_TOKEN" => nil) do
Setting.external_assistant_url = "https://agent.example.com/v1/chat"
Setting.external_assistant_token = "token"
Setting.external_assistant_agent_id = "finance-bot"
users(:family_admin).family.update!(assistant_type: "external")
delete disconnect_external_assistant_settings_hosting_url
assert_redirected_to settings_hosting_url
# Force cache refresh so configured? reads fresh DB state after
# the disconnect action cleared the settings within its own request.
Setting.clear_cache
assert_not Assistant::External.configured?
assert_equal "builtin", users(:family_admin).family.reload.assistant_type
end
end
ensure
Setting.external_assistant_url = nil
Setting.external_assistant_token = nil
Setting.external_assistant_agent_id = nil
end
test "disconnect external assistant requires admin" do
with_self_hosting do
sign_in users(:family_member)
delete disconnect_external_assistant_settings_hosting_url
assert_redirected_to settings_hosting_url
assert_equal I18n.t("settings.hostings.not_authorized"), flash[:alert]
end
end
test "accepts valid llm budget overrides and blanks clear them" do
with_self_hosting do
patch settings_hosting_url, params: { setting: {
llm_context_window: "4096",
llm_max_response_tokens: "1024",
llm_max_items_per_call: "40",
openai_request_timeout: "180",
ai_response_timeout: "240"
} }
assert_redirected_to settings_hosting_url
assert_equal 4096, Setting.llm_context_window
assert_equal 1024, Setting.llm_max_response_tokens
assert_equal 40, Setting.llm_max_items_per_call
assert_equal 180, Setting.openai_request_timeout
assert_equal 240, Setting.ai_response_timeout
patch settings_hosting_url, params: { setting: {
llm_context_window: "",
llm_max_response_tokens: "",
llm_max_items_per_call: "",
openai_request_timeout: "",
ai_response_timeout: ""
} }
assert_nil Setting.llm_context_window
assert_nil Setting.llm_max_response_tokens
assert_nil Setting.llm_max_items_per_call
assert_nil Setting.openai_request_timeout
assert_nil Setting.ai_response_timeout
end
ensure
Setting.llm_context_window = nil
Setting.llm_max_response_tokens = nil
Setting.llm_max_items_per_call = nil
Setting.openai_request_timeout = nil
Setting.ai_response_timeout = nil
end
test "rejects llm budget below field minimum" do
with_self_hosting do
patch settings_hosting_url, params: { setting: { llm_context_window: "0" } }
assert_response :unprocessable_entity
assert_match(/must be a whole number/, flash[:alert])
assert_nil Setting.llm_context_window
patch settings_hosting_url, params: { setting: { llm_max_response_tokens: "-5" } }
assert_response :unprocessable_entity
assert_match(/must be a whole number/, flash[:alert])
assert_nil Setting.llm_max_response_tokens
patch settings_hosting_url, params: { setting: { llm_max_items_per_call: "not-a-number" } }
assert_response :unprocessable_entity
assert_match(/must be a whole number/, flash[:alert])
assert_nil Setting.llm_max_items_per_call
patch settings_hosting_url, params: { setting: { openai_request_timeout: "0" } }
assert_response :unprocessable_entity
assert_match(/must be a whole number/, flash[:alert])
assert_nil Setting.openai_request_timeout
end
ensure
Setting.llm_context_window = nil
Setting.llm_max_response_tokens = nil
Setting.llm_max_items_per_call = nil
Setting.openai_request_timeout = nil
end
test "shows environment backed OpenAI request timeout when field is disabled" do
with_self_hosting do
Setting.openai_request_timeout = 180
with_env_overrides("OPENAI_REQUEST_TIMEOUT" => "300") do
get settings_hosting_url
assert_response :success
assert_select "input[name='setting[openai_request_timeout]'][value='300'][disabled='disabled']"
end
end
ensure
Setting.openai_request_timeout = nil
end
test "can clear data only when admin" do
with_self_hosting do
sign_in users(:family_member)
assert_no_enqueued_jobs do
delete clear_cache_settings_hosting_url
end
assert_redirected_to settings_hosting_url
assert_equal I18n.t("settings.hostings.not_authorized"), flash[:alert]
end
end
# --- Securities provider toggle ---
test "can update securities providers" do
with_self_hosting do
patch settings_hosting_url, params: { setting: { securities_providers: [ "twelve_data", "yahoo_finance" ] } }
assert_redirected_to settings_hosting_url
assert_equal "twelve_data,yahoo_finance", Setting.securities_providers
end
ensure
Setting.securities_providers = ""
end
test "filters out invalid provider names" do
with_self_hosting do
patch settings_hosting_url, params: { setting: { securities_providers: [ "twelve_data", "fake_provider", "hacked" ] } }
assert_redirected_to settings_hosting_url
# Only valid providers are stored
enabled = Setting.enabled_securities_providers
assert_includes enabled, "twelve_data"
refute_includes enabled, "fake_provider"
refute_includes enabled, "hacked"
end
ensure
Setting.securities_providers = ""
end
test "removing a provider marks linked securities offline" do
with_self_hosting do
security = Security.create!(ticker: "CSPX", exchange_operating_mic: "XLON", price_provider: "tiingo", offline: false)
# First enable tiingo
Setting.securities_providers = "twelve_data,tiingo"
# Then remove tiingo
patch settings_hosting_url, params: { setting: { securities_providers: [ "twelve_data" ] } }
security.reload
assert security.offline?, "Security should be marked offline when its provider is removed"
assert_equal "provider_disabled", security.offline_reason
end
ensure
Setting.securities_providers = ""
end
test "re-adding a provider brings securities back online" do
with_self_hosting do
security = Security.create!(
ticker: "CSPX2", exchange_operating_mic: "XLON",
price_provider: "tiingo", offline: true, offline_reason: "provider_disabled"
)
# Start without tiingo
Setting.securities_providers = "twelve_data"
# Re-add tiingo
patch settings_hosting_url, params: { setting: { securities_providers: [ "twelve_data", "tiingo" ] } }
security.reload
refute security.offline?, "Security should come back online when its provider is re-added"
assert_nil security.offline_reason
end
ensure
Setting.securities_providers = ""
end
test "unchecking every securities provider does not re-enable twelve_data via the legacy fallback" do
with_self_hosting do
# Start from the out-of-the-box default (only twelve_data enabled)
assert_equal [ "twelve_data" ], Setting.enabled_securities_providers
patch settings_hosting_url, params: { setting: { securities_providers: [] } }
assert_redirected_to settings_hosting_url
assert_equal [], Setting.enabled_securities_providers
end
ensure
# Explicitly restore the real default value rather than assigning nil —
# rails-settings-cached's cache layer doesn't reliably invalidate on
# delete within a single test process, so a later test can still read
# back the just-deleted blank override instead of falling through to
# the field's default.
Setting.securities_providers = ""
Setting.securities_provider = "twelve_data"
end
# --- T-Invest visibility (issue #3089) ---
test "hides T-Invest settings when neither tinkoff_invest nor moex_public is enabled" do
with_self_hosting do
patch settings_hosting_url, params: { setting: { securities_providers: [ "twelve_data" ] } }
get settings_hosting_url
assert_response :success
# "T-Invest (T-Bank)" also appears as a checkbox label in the always-rendered
# securities checklist, so assert on the settings block's own field instead.
assert_select "input[name='setting[tinkoff_invest_api_key]']", false
end
ensure
Setting.securities_providers = ""
end
test "shows T-Invest settings when tinkoff_invest is enabled, without the moex-only notice" do
with_self_hosting do
patch settings_hosting_url, params: { setting: { securities_providers: [ "tinkoff_invest" ] } }
get settings_hosting_url
assert_response :success
assert_select "input[name='setting[tinkoff_invest_api_key]']"
assert_not_includes response.body, I18n.t("settings.hostings.tinkoff_invest_settings.moex_only_notice")
end
ensure
Setting.securities_providers = ""
end
test "shows T-Invest settings with the moex-only notice when moex_public is enabled, even without tinkoff_invest" do
with_self_hosting do
patch settings_hosting_url, params: { setting: { securities_providers: [ "moex_public" ] } }
get settings_hosting_url
assert_response :success
assert_select "input[name='setting[tinkoff_invest_api_key]']"
assert_includes response.body, I18n.t("settings.hostings.tinkoff_invest_settings.moex_only_notice")
end
ensure
Setting.securities_providers = ""
end
test "shows T-Invest settings when a token is already configured, even with neither checkbox enabled" do
with_self_hosting do
Setting.tinkoff_invest_api_key = "some-token"
patch settings_hosting_url, params: { setting: { securities_providers: [ "twelve_data" ] } }
get settings_hosting_url
assert_response :success
assert_select "input[name='setting[tinkoff_invest_api_key]']"
assert_includes response.body, I18n.t("settings.hostings.tinkoff_invest_settings.moex_only_notice")
end
ensure
Setting.securities_providers = ""
Setting.tinkoff_invest_api_key = nil
end
end