Fix: Yahoo Finance provider Cookie/Crumb Auth (#1082)

* Fix: use cookie/crumb auth in healthy? chart endpoint check

The health check was calling /v8/finance/chart/AAPL via the plain
unauthenticated client. Yahoo Finance requires cookie + crumb
authentication on the chart endpoint, so the health check would
fail even when credentials are valid. Updated healthy? to use
fetch_cookie_and_crumb + authenticated_client, consistent with
fetch_security_prices and fetch_chart_data.

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

* Fix: add cookie/crumb auth to all /v8/finance/chart/ calls

fetch_security_prices and fetch_chart_data (used for exchange rates)
were calling the chart endpoint without cookie/crumb authentication,
inconsistent with healthy? and fetch_security_info. Added auth to both,
including the same retry-on-Unauthorized pattern already used in
fetch_security_info.

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

* Update user-agent strings in yahoo_finance.rb

Updated user-agent strings to reflect current browser versions

Signed-off-by: Serge L <serge@souritech.ca>

* Fix: Add stale-crumb retry to healthy? and fetch_chart_data

Yahoo Finance returns 200 OK with {"chart":{"error":{"code":"Unauthorized"}}}
when a cached crumb expires server-side. Both healthy? and fetch_chart_data
now mirror the retry pattern already in fetch_security_prices: detect the
Unauthorized body, clear the crumb cache, fetch fresh credentials, and
retry the request once. Adds a test for the healthy? retry path.

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

* Refactor: Extract fetch_authenticated_chart helper to DRY crumb retry logic

The cookie/crumb fetch + stale-crumb retry pattern was duplicated across
healthy?, fetch_security_prices, and fetch_chart_data. Extract it into a
single private fetch_authenticated_chart(symbol, params) helper that
centralizes the retry logic; all three call sites now delegate to it.

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

* Fix: Catch JSON::ParserError in fetch_chart_data rescue clause

After moving JSON.parse inside fetch_authenticated_chart, a malformed
Yahoo response would throw JSON::ParserError through fetch_chart_data's
rescue Faraday::Error, breaking the inverse currency pair fallback.

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

* Fix: Raise AuthenticationError if retry still returns Unauthorized

After refreshing the crumb and retrying, if Yahoo still returns an
Unauthorized error body the helper now raises AuthenticationError instead
of silently returning the error payload. This prevents callers from
misinterpreting a persistent auth failure as missing chart data.

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

* Fix: Raise AuthenticationError after failed retry in fetch_security_info

Mirrors the same post-retry Unauthorized check added to fetch_authenticated_chart.
Without this, a persistent auth failure on the quoteSummary endpoint would
surface as a generic "No security info found" error instead of an AuthenticationError.

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

---------

Signed-off-by: Serge L <serge@souritech.ca>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Serge L
2026-03-01 16:48:48 -05:00
committed by GitHub
parent f13da4809b
commit 15cfcf585d
2 changed files with 77 additions and 42 deletions

View File

@@ -10,24 +10,42 @@ class Provider::YahooFinanceTest < ActiveSupport::TestCase
# ================================
test "healthy? returns true when API is working" do
# Mock successful response
mock_response = mock
mock_response.stubs(:body).returns('{"chart":{"result":[{"meta":{"symbol":"AAPL"}}]}}')
@provider.stubs(:client).returns(mock_client = mock)
@provider.stubs(:fetch_cookie_and_crumb).returns([ "test_cookie", "test_crumb" ])
@provider.stubs(:authenticated_client).returns(mock_client = mock)
mock_client.stubs(:get).returns(mock_response)
assert @provider.healthy?
end
test "healthy? returns false when API fails" do
# Mock failed response
@provider.stubs(:client).returns(mock_client = mock)
mock_client.stubs(:get).raises(Faraday::Error.new("Connection failed"))
@provider.stubs(:fetch_cookie_and_crumb).raises(Provider::YahooFinance::AuthenticationError.new("auth failed"))
assert_not @provider.healthy?
end
test "healthy? retries with fresh crumb on Unauthorized body response" do
unauthorized_body = '{"chart":{"error":{"code":"Unauthorized","description":"No crumb"}}}'
success_body = '{"chart":{"result":[{"meta":{"symbol":"AAPL"}}]}}'
unauthorized_response = mock
unauthorized_response.stubs(:body).returns(unauthorized_body)
success_response = mock
success_response.stubs(:body).returns(success_body)
mock_client = mock
mock_client.stubs(:get).returns(unauthorized_response, success_response)
@provider.stubs(:fetch_cookie_and_crumb).returns([ "cookie1", "crumb1" ], [ "cookie2", "crumb2" ])
@provider.stubs(:authenticated_client).returns(mock_client)
@provider.expects(:clear_crumb_cache).once
assert @provider.healthy?
end
# ================================
# Exchange Rate Tests
# ================================