Files
sure/test/controllers/holdings_controller_test.rb
Serge L a92fd3b3e8 feat: Enhance holding detail drawer with live price sync and enriched overview (#1086)
* Feat: Implement manual sync prices functionality and enhance holdings display

* Feat: Enhance sync prices functionality with error handling and update UI components

* Feat: Update sync prices error handling and enhance Spanish locale messages

* Fix: Address CodeRabbit review feedback

- Set fallback @provider_error when prices_updated == 0 so turbo stream
  never fails silently without a visible error message
- Move attr_reader :provider_error to class header in Price::Importer
  for conventional placement alongside other attribute declarations
- Precompute @last_price_updated in controller (show + sync_prices)
  instead of running a DB query directly inside ERB templates

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

* Fix: Replace bare rescue with explicit exception handling in turbo stream view

Bare `rescue` silently swallows all exceptions, making debugging impossible.
Match the pattern already used in show.html.erb: rescue ActiveRecord::RecordInvalid
explicitly, then catch StandardError with logging (message + backtrace) before
falling back to the unknown label.

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

* Fix: Update test assertion to expect actual provider error message

The stub returns "Yahoo Finance rate limit exceeded" as the provider error.
After the @provider_error fallback fix, the controller now correctly surfaces
the real provider error when present (using .presence || fallback), so the
flash[:alert] is the actual error string, not the generic fallback.

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

* Fix: Assert scoped security_ids in sync_prices materializer test

Replace loose stub with constructor expectation to verify that
Balance::Materializer is instantiated with the single-security scope.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix: Assert holding remap in remap_security test

Add assertion that @holding.security_id is updated to the target
security after remap, covering the core command outcome.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix: CI test failure - Update disconnect external assistant test to use env overrides

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-06 10:05:52 +01:00

122 lines
4.3 KiB
Ruby

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