require "test_helper" class HoldingsControllerTest < ActionDispatch::IntegrationTest setup do sign_in users(:family_admin) @account = accounts(:investment) @holding = @account.holdings.first end test "gets holdings" do get holdings_url(account_id: @account.id) assert_response :success end test "gets holding" do get holding_path(@holding) assert_response :success end test "destroys holding and associated entries" do assert_difference -> { Holding.count } => -1, -> { Entry.count } => -1 do delete holding_path(@holding) end assert_redirected_to account_path(@holding.account) assert_empty @holding.account.entries.where(entryable: @holding.account.trades.where(security: @holding.security)) end test "updates cost basis with total amount divided by qty" do # Given: holding with 10 shares @holding.update!(qty: 10, cost_basis: nil, cost_basis_source: nil, cost_basis_locked: false) # When: user submits total cost basis of $100 (should become $10 per share) patch holding_path(@holding), params: { holding: { cost_basis: "100.00" } } # Redirects to account page holdings tab to refresh list assert_redirected_to account_path(@holding.account, tab: "holdings") @holding.reload # Then: cost_basis should be per-share ($10), not total assert_equal 10.0, @holding.cost_basis.to_f assert_equal "manual", @holding.cost_basis_source assert @holding.cost_basis_locked? end test "unlock_cost_basis removes lock" do # Given: locked holding @holding.update!(cost_basis: 50.0, cost_basis_source: "manual", cost_basis_locked: true) # When: user unlocks post unlock_cost_basis_holding_path(@holding) # Redirects to account page holdings tab to refresh list assert_redirected_to account_path(@holding.account, tab: "holdings") @holding.reload # Then: lock is removed but cost_basis and source remain assert_not @holding.cost_basis_locked? assert_equal 50.0, @holding.cost_basis.to_f assert_equal "manual", @holding.cost_basis_source end test "remap_security brings offline security back online" do # Given: the target security is marked offline (e.g. created by a failed QIF import) msft = securities(:msft) msft.update!(offline: true, failed_fetch_count: 3) # When: user explicitly selects it from the provider search and saves patch remap_security_holding_path(@holding), params: { security_id: "MSFT|XNAS" } # Then: the security is brought back online and the holding is remapped assert_redirected_to account_path(@holding.account, tab: "holdings") @holding.reload msft.reload assert_equal msft.id, @holding.security_id assert_not msft.offline? assert_equal 0, msft.failed_fetch_count end test "sync_prices redirects with alert for offline security" do @holding.security.update!(offline: true) post sync_prices_holding_path(@holding) assert_redirected_to account_path(@holding.account, tab: "holdings") assert_equal I18n.t("holdings.sync_prices.unavailable"), flash[:alert] end test "sync_prices syncs market data and redirects with notice" do Security.any_instance.expects(:import_provider_prices).with( start_date: 31.days.ago.to_date, end_date: Date.current, clear_cache: true ).returns([ 31, nil ]) Security.any_instance.stubs(:import_provider_details) materializer = mock("materializer") materializer.expects(:materialize_balances).once Balance::Materializer.expects(:new).with( @holding.account, strategy: :forward, security_ids: [ @holding.security_id ] ).returns(materializer) post sync_prices_holding_path(@holding) assert_redirected_to account_path(@holding.account, tab: "holdings") assert_equal I18n.t("holdings.sync_prices.success"), flash[:notice] end test "sync_prices shows provider error inline when provider returns no prices" do Security.any_instance.stubs(:import_provider_prices).returns([ 0, "Yahoo Finance rate limit exceeded" ]) Security.any_instance.stubs(:import_provider_details) post sync_prices_holding_path(@holding) assert_redirected_to account_path(@holding.account, tab: "holdings") assert_equal "Yahoo Finance rate limit exceeded", flash[:alert] end end