package binancex import ( "strings" "testing" "me.thuanle/bbot/internal/configs/binance" "me.thuanle/bbot/internal/data" "me.thuanle/bbot/internal/data/market" ) type resolverMarketStub struct { alphaTokens map[string]bool spotPairs map[string]bool futuresPairs map[string]bool } func (m *resolverMarketStub) GetFuturePrice(symbol string) (float64, float64, int64, bool) { return 0, 0, 0, false } func (m *resolverMarketStub) GetAllPremiumIndex() (map[string]market.PremiumIndex, error) { return nil, nil } func (m *resolverMarketStub) GetAllFundRate() (map[string]float64, map[string]int64) { return nil, nil } func (m *resolverMarketStub) GetSpotPrice(symbol string) (float64, bool) { return 0, false } func (m *resolverMarketStub) GetMarginInterestRates() map[string]float64 { return nil } func (m *resolverMarketStub) IsAlphaToken(symbol string) bool { return m.alphaTokens[symbol] } func (m *resolverMarketStub) GetAlphaToken(symbol string) (market.AlphaTokenInfo, bool) { return market.AlphaTokenInfo{}, false } func (m *resolverMarketStub) GetAlphaPrice(symbol string) (float64, bool) { return 0, false } func (m *resolverMarketStub) IsSpotPair(symbol string) bool { return m.spotPairs[symbol] } func (m *resolverMarketStub) IsFuturesPair(symbol string) bool { return m.futuresPairs[symbol] } func (m *resolverMarketStub) GetSpotSymbolByToken(token string) (string, bool) { token = strings.ToUpper(token) for sym := range m.spotPairs { if strings.ToUpper(Symbol2Token(sym)) == token { return sym, true } } return "", false } func (m *resolverMarketStub) GetFutureSymbolByToken(token string) (string, bool) { token = strings.ToUpper(token) for sym := range m.futuresPairs { if strings.ToUpper(Symbol2Token(sym)) == token { return sym, true } } return "", false } func (m *resolverMarketStub) RefreshTradingPairCache() error { return nil } func withResolverMarketStub(t *testing.T, marketStub *resolverMarketStub) { t.Helper() orig := data.Market data.Market = marketStub t.Cleanup(func() { data.Market = orig }) } func TestToken2FutureSymbols_ReturnsCanonicalFutureSymbol(t *testing.T) { withResolverMarketStub(t, &resolverMarketStub{ futuresPairs: map[string]bool{ "1000PEPEUSDT": true, "1000PEPEUSDC": true, }, }) syms := Token2FutureSymbols("pepe") if len(syms) != 1 || syms[0] != "1000PEPEUSDT" { t.Fatalf("expected canonical [1000PEPEUSDT], got %+v", syms) } } func TestToken2FutureSymbols_UsesCanonicalQuotePriority(t *testing.T) { withResolverMarketStub(t, &resolverMarketStub{ futuresPairs: map[string]bool{ "1000PEPEUSDT": true, "1000PEPEUSDC": true, "PEPEUSDT": true, "PEPEUSDC": true, }, }) syms := Token2FutureSymbols("pepe") if len(syms) != 1 || syms[0] != "1000PEPEUSDT" { t.Fatalf("expected canonical [1000PEPEUSDT], got %+v", syms) } } func TestToken2FutureSymbols_ResolvesUSDCAbbreviation(t *testing.T) { withResolverMarketStub(t, &resolverMarketStub{ futuresPairs: map[string]bool{"PEPEUSDC": true}, }) syms := Token2FutureSymbols("pepec") if len(syms) != 1 || syms[0] != "PEPEUSDC" { t.Fatalf("expected [PEPEUSDC], got %+v", syms) } } func TestSymbolSuffixListMatchesSymbolSuffixMap(t *testing.T) { seen := make(map[string]struct{}, len(binance.SymbolSuffixList)) for _, suffix := range binance.SymbolSuffixList { if _, ok := binance.SymbolSuffixMap[suffix]; !ok { t.Fatalf("SymbolSuffixList contains %q missing from SymbolSuffixMap", suffix) } seen[suffix] = struct{}{} } for suffix := range binance.SymbolSuffixMap { if _, ok := seen[suffix]; !ok { t.Fatalf("SymbolSuffixMap contains %q missing from SymbolSuffixList", suffix) } } } func TestIsToken(t *testing.T) { tests := []struct { name string input string marketStub *resolverMarketStub want bool }{ { name: "futures token resolution success", input: "pepe", marketStub: &resolverMarketStub{ futuresPairs: map[string]bool{"1000PEPEUSDT": true}, }, want: true, }, { name: "alpha token fallback", input: "alpha", marketStub: &resolverMarketStub{ alphaTokens: map[string]bool{"ALPHA": true}, }, want: true, }, { name: "spot only token fallback", input: "abc", marketStub: &resolverMarketStub{ spotPairs: map[string]bool{"ABCUSDT": true}, }, want: true, }, { name: "non alphanumeric input", input: "bad!", marketStub: &resolverMarketStub{}, want: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { withResolverMarketStub(t, tt.marketStub) got := IsToken(tt.input) if got != tt.want { t.Fatalf("expected %v, got %v", tt.want, got) } }) } } func TestToken2SpotSymbols_DoesNotDependOnFutureMappings(t *testing.T) { withResolverMarketStub(t, &resolverMarketStub{ futuresPairs: map[string]bool{"LUNA2USDT": true}, spotPairs: map[string]bool{"LUNAUSDT": true}, }) spots := Token2SpotSymbols("luna2") if len(spots) != 0 { t.Fatalf("expected no direct spot symbols for LUNA2, got %+v", spots) } } func TestToken2RelatedSpotSymbols_AppliesExplicitRemap(t *testing.T) { withResolverMarketStub(t, &resolverMarketStub{ futuresPairs: map[string]bool{"LUNA2USDT": true}, spotPairs: map[string]bool{"LUNAUSDT": true}, }) spots := Token2RelatedSpotSymbols("luna2") if len(spots) != 1 || spots[0] != "LUNAUSDT" { t.Fatalf("expected [LUNAUSDT], got %+v", spots) } } func TestToken2SpotSymbols_SpotOnlyFallback(t *testing.T) { withResolverMarketStub(t, &resolverMarketStub{ spotPairs: map[string]bool{"ABCUSDT": true}, }) spots := Token2SpotSymbols("abc") if len(spots) != 1 || spots[0] != "ABCUSDT" { t.Fatalf("expected [ABCUSDT], got %+v", spots) } }