Files
sure/test/models/security/resolver_test.rb
soky srm dcebda05de Move back to brandfetch (#1427)
* Move back to brandfetch

* Update security.rb

* Update security.rb
2026-04-10 17:42:16 +02:00

211 lines
8.0 KiB
Ruby
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
require "test_helper"
class Security::ResolverTest < ActiveSupport::TestCase
test "resolves DB security" do
# Given an existing security in the DB that exactly matches the lookup params
db_security = Security.create!(ticker: "TSLA", exchange_operating_mic: "XNAS", country_code: "US")
# The resolver should return the DB record and never hit the provider
Security.expects(:search_provider).never
resolved = Security::Resolver.new("TSLA", exchange_operating_mic: "XNAS", country_code: "US").resolve
assert_equal db_security, resolved
end
test "resolves exact provider match" do
# Provider returns multiple results, one of which exactly matches symbol + exchange (and country)
exact_match = Security.new(ticker: "NVDA", exchange_operating_mic: "XNAS", country_code: "US")
near_miss = Security.new(ticker: "NVDA", exchange_operating_mic: "XNYS", country_code: "US")
Security.expects(:search_provider)
.with("NVDA", exchange_operating_mic: "XNAS", country_code: "US")
.returns([ near_miss, exact_match ])
assert_difference "Security.count", 1 do
resolved = Security::Resolver.new("NVDA", exchange_operating_mic: "XNAS", country_code: "US").resolve
assert resolved.persisted?
assert_equal "NVDA", resolved.ticker
assert_equal "XNAS", resolved.exchange_operating_mic
assert_equal "US", resolved.country_code
refute resolved.offline, "Exact provider matches should not be marked offline"
end
end
test "resolves close provider match" do
# No exact match resolver should choose the most relevant close match based on exchange + country ranking
preferred = Security.new(ticker: "TEST1", exchange_operating_mic: "XNAS", country_code: "US")
other = Security.new(ticker: "TEST2", exchange_operating_mic: "XNYS", country_code: "GB")
# Return in reverse-priority order to prove the sorter works
Security.expects(:search_provider)
.with("TEST", exchange_operating_mic: "XNAS")
.returns([ other, preferred ])
assert_difference "Security.count", 1 do
resolved = Security::Resolver.new("TEST", exchange_operating_mic: "XNAS").resolve
assert resolved.persisted?
assert_equal "TEST1", resolved.ticker
assert_equal "XNAS", resolved.exchange_operating_mic
assert_equal "US", resolved.country_code
refute resolved.offline, "Provider matches should not be marked offline"
end
end
test "resolves offline security" do
Security.expects(:search_provider).returns([])
assert_difference "Security.count", 1 do
resolved = Security::Resolver.new("FOO").resolve
assert resolved.persisted?, "Offline security should be saved"
assert_equal "FOO", resolved.ticker
assert resolved.offline, "Offline securities should be flagged offline"
end
end
test "returns nil when symbol blank" do
assert_raises(ArgumentError) { Security::Resolver.new(nil).resolve }
assert_raises(ArgumentError) { Security::Resolver.new("").resolve }
end
test "persists explicit price_provider on DB match" do
db_security = Security.create!(ticker: "CSPX", exchange_operating_mic: "XLON", country_code: "GB")
Security.expects(:search_provider).never
Setting.stubs(:enabled_securities_providers).returns([ "tiingo" ])
resolved = Security::Resolver.new(
"CSPX",
exchange_operating_mic: "XLON",
country_code: "GB",
price_provider: "tiingo"
).resolve
assert_equal db_security, resolved
assert_equal "tiingo", resolved.reload.price_provider
end
test "persists price_provider on provider match" do
match = Security.new(ticker: "VWCE", exchange_operating_mic: "XETR", country_code: "DE", price_provider: "eodhd")
Security.expects(:search_provider)
.with("VWCE", exchange_operating_mic: "XETR")
.returns([ match ])
Setting.stubs(:enabled_securities_providers).returns([ "eodhd" ])
resolved = Security::Resolver.new(
"VWCE",
exchange_operating_mic: "XETR",
price_provider: "eodhd"
).resolve
assert resolved.persisted?
assert_equal "eodhd", resolved.price_provider
end
test "rejects unknown price_provider" do
db_security = Security.create!(ticker: "AAPL2", exchange_operating_mic: "XNAS", country_code: "US")
Security.expects(:search_provider).never
resolved = Security::Resolver.new(
"AAPL2",
exchange_operating_mic: "XNAS",
country_code: "US",
price_provider: "fake_provider"
).resolve
assert_equal db_security, resolved
assert_nil resolved.reload.price_provider, "Unknown providers should be rejected"
end
test "resolves Binance crypto match for a non-AE family" do
# Regression: BinancePublic search results carry country_code="AE" (the ISO
# 10383 MIC country), but the transactions controller passes the family's
# country (e.g. "US"). The resolver used to require an exact country match
# for both exact and close paths, so non-AE families would fall through to
# offline_security for every Binance pick — the user saw their BTCUSD
# holding resolve to an offline security that never fetched prices.
binance_match = Security.new(
ticker: "BTCUSD",
exchange_operating_mic: "BNCX",
country_code: nil,
price_provider: "binance_public"
)
Security.expects(:search_provider)
.with("BTCUSD", exchange_operating_mic: "BNCX", country_code: "US")
.returns([ binance_match ])
Setting.stubs(:enabled_securities_providers).returns([ "binance_public" ])
resolved = Security::Resolver.new(
"BTCUSD",
exchange_operating_mic: "BNCX",
country_code: "US",
price_provider: "binance_public"
).resolve
assert resolved.persisted?
refute resolved.offline, "Binance security must not fall through to offline_security"
assert_equal "BTCUSD", resolved.ticker
assert_equal "BNCX", resolved.exchange_operating_mic
assert_equal "binance_public", resolved.price_provider
end
test "resolved provider match is persisted with nil name/logo_url" do
# Documents that find_or_create_provider_match! intentionally copies only
# ticker, MIC, country_code, and price_provider from the match — not name
# or logo_url. This means Security#import_provider_details always has
# blank metadata on first resolution and runs fetch_security_info +
# probe_brandfetch_crypto_coverage! as expected. Regression guard: if
# someone adds name/logo copying to the resolver, the Brandfetch
# coverage probe would be bypassed on first sync.
match = Security.new(
ticker: "BTCUSD",
exchange_operating_mic: "BNCX",
country_code: nil,
name: "BTC",
logo_url: "https://cdn.brandfetch.io/crypto/BTC/icon/fallback/lettermark/w/120/h/120?c=test",
price_provider: "binance_public"
)
Security.expects(:search_provider)
.with("BTCUSD", exchange_operating_mic: "BNCX", country_code: "US")
.returns([ match ])
Setting.stubs(:enabled_securities_providers).returns([ "binance_public" ])
resolved = Security::Resolver.new(
"BTCUSD",
exchange_operating_mic: "BNCX",
country_code: "US",
price_provider: "binance_public"
).resolve
assert_nil resolved.reload.name, "Resolver must not copy name from the search match"
assert_nil resolved.logo_url, "Resolver must not copy logo_url from the search match"
end
test "rejects disabled price_provider" do
db_security = Security.create!(ticker: "GOOG2", exchange_operating_mic: "XNAS", country_code: "US")
Security.expects(:search_provider).never
Setting.stubs(:enabled_securities_providers).returns([ "twelve_data" ])
resolved = Security::Resolver.new(
"GOOG2",
exchange_operating_mic: "XNAS",
country_code: "US",
price_provider: "tiingo"
).resolve
assert_equal db_security, resolved
assert_nil resolved.reload.price_provider, "Disabled providers should be rejected"
end
end