Files
sure/test/models/provider/twelve_data_test.rb
soky srm 0aca297e9c Add binance security provider for crypto (#1424)
* Binance as securities provider

* Disable twelve data crypto results

* Add logo support and new currency pairs

* FIX importer fallback

* Add price clamping and optiimize retrieval

* Review

* Update adding-a-securities-provider.md

* day gap miss fix

* New fixes

* Brandfetch doesn't support crypto. add new CDN

* Update _investment_performance.html.erb
2026-04-10 15:43:22 +02:00

224 lines
7.1 KiB
Ruby

require "test_helper"
class Provider::TwelveDataTest < ActiveSupport::TestCase
setup do
@provider = Provider::TwelveData.new("test_api_key")
end
# ================================
# Rate Limit Detection Tests
# ================================
test "detects rate limit from JSON body code 429" do
rate_limit_body = {
"code" => 429,
"message" => "You have run out of API credits for the current minute.",
"status" => "error"
}.to_json
mock_response = mock
mock_response.stubs(:body).returns(rate_limit_body)
@provider.stubs(:throttle_request)
@provider.stubs(:client).returns(mock_client = mock)
mock_client.stubs(:get).returns(mock_response)
result = @provider.fetch_exchange_rates(from: "USD", to: "EUR", start_date: Date.current, end_date: Date.current)
assert_not result.success?
assert_instance_of Provider::TwelveData::RateLimitError, result.error
end
test "detects rate limit on single exchange rate fetch" do
rate_limit_body = {
"code" => 429,
"message" => "Rate limit exceeded"
}.to_json
mock_response = mock
mock_response.stubs(:body).returns(rate_limit_body)
@provider.stubs(:throttle_request)
@provider.stubs(:client).returns(mock_client = mock)
mock_client.stubs(:get).returns(mock_response)
result = @provider.fetch_exchange_rate(from: "USD", to: "EUR", date: Date.current)
assert_not result.success?
assert_instance_of Provider::TwelveData::RateLimitError, result.error
end
test "does not fall through to cross API when rate limited" do
rate_limit_body = {
"code" => 429,
"message" => "Rate limit exceeded"
}.to_json
mock_response = mock
mock_response.stubs(:body).returns(rate_limit_body)
@provider.stubs(:throttle_request)
mock_client = mock
# Should only be called once (time_series), NOT a second time (time_series/cross)
mock_client.expects(:get).once.returns(mock_response)
@provider.stubs(:client).returns(mock_client)
result = @provider.fetch_exchange_rates(from: "USD", to: "EUR", start_date: Date.current, end_date: Date.current)
assert_not result.success?
assert_instance_of Provider::TwelveData::RateLimitError, result.error
end
# ================================
# Error Transformer Tests
# ================================
test "default_error_transformer preserves RateLimitError" do
error = Provider::TwelveData::RateLimitError.new("Rate limit exceeded")
result = @provider.send(:with_provider_response) { raise error }
assert_not result.success?
assert_instance_of Provider::TwelveData::RateLimitError, result.error
end
test "default_error_transformer converts Faraday 429 to RateLimitError" do
error = Faraday::TooManyRequestsError.new("Too Many Requests", { body: "Rate limited" })
result = @provider.send(:with_provider_response) { raise error }
assert_not result.success?
assert_instance_of Provider::TwelveData::RateLimitError, result.error
end
test "default_error_transformer wraps generic errors as Error" do
error = StandardError.new("Something went wrong")
result = @provider.send(:with_provider_response) { raise error }
assert_not result.success?
assert_instance_of Provider::TwelveData::Error, result.error
end
# ================================
# Crypto Filter Tests
# ================================
test "search_securities excludes Digital Currency rows" do
body = {
"data" => [
{
"symbol" => "ETH",
"instrument_name" => "Grayscale Ethereum Trust ETF",
"mic_code" => "ARCX",
"instrument_type" => "ETF",
"country" => "United States",
"currency" => "USD"
},
{
"symbol" => "ETH/EUR",
"instrument_name" => "Ethereum Euro",
"mic_code" => "DIGITAL_CURRENCY",
"instrument_type" => "Digital Currency",
"country" => "",
"currency" => ""
},
{
"symbol" => "BTC/USD",
"instrument_name" => "Bitcoin US Dollar",
"mic_code" => "DIGITAL_CURRENCY",
"instrument_type" => "Digital Currency",
"country" => "",
"currency" => ""
}
]
}.to_json
mock_response = mock
mock_response.stubs(:body).returns(body)
@provider.stubs(:throttle_request)
@provider.stubs(:client).returns(mock_client = mock)
mock_client.stubs(:get).returns(mock_response)
result = @provider.search_securities("ETH")
assert result.success?
assert_equal 1, result.data.size
assert_equal "ETH", result.data.first.symbol
refute result.data.any? { |s| s.symbol.include?("/") }
end
test "search_securities excludes crypto even with mixed-case instrument_type" do
body = {
"data" => [
{
"symbol" => "BTC/EUR",
"instrument_name" => "Bitcoin Euro",
"mic_code" => "",
"instrument_type" => "digital currency",
"currency" => ""
}
]
}.to_json
mock_response = mock
mock_response.stubs(:body).returns(body)
@provider.stubs(:throttle_request)
@provider.stubs(:client).returns(mock_client = mock)
mock_client.stubs(:get).returns(mock_response)
result = @provider.search_securities("BTC")
assert_empty result.data
end
# ================================
# Throttle Tests
# ================================
test "throttle_request enforces minimum interval between calls" do
@provider.send(:instance_variable_set, :@last_request_time, Time.current)
# Stub sleep to capture the call without actually sleeping
sleep_called_with = nil
@provider.define_singleton_method(:sleep) { |duration| sleep_called_with = duration }
# Stub cache to return under limit (read returns current count, increment charges)
Rails.cache.stubs(:read).returns(0)
Rails.cache.stubs(:increment).returns(1)
@provider.send(:throttle_request)
assert_not_nil sleep_called_with, "Should have called sleep to enforce minimum interval"
assert_operator sleep_called_with, :>, 0
end
test "throttle_request waits when per-minute credit limit is exceeded" do
# Stub cache read to return count at limit (adding 1 more would exceed 7)
Rails.cache.stubs(:read).returns(7)
Rails.cache.stubs(:increment).returns(8)
sleep_called = false
@provider.define_singleton_method(:sleep) { |_duration| sleep_called = true }
@provider.send(:throttle_request)
assert sleep_called, "Should have called sleep when credit limit exceeded"
end
test "throttle_request does not wait when under credit limit" do
# Set last_request_time far in the past so per-instance throttle doesn't trigger
@provider.send(:instance_variable_set, :@last_request_time, Time.at(0))
# Stub cache to return under limit
Rails.cache.stubs(:read).returns(3)
Rails.cache.stubs(:increment).returns(4)
sleep_called = false
@provider.define_singleton_method(:sleep) { |_duration| sleep_called = true }
@provider.send(:throttle_request)
assert_not sleep_called, "Should not sleep when under credit limit"
end
end