Files
gocryptotrader/engine/engine_test.go
cranktakular fd9aaf00a2 Coinbase: Update exchange implementation (#1480)
* Slight enhance of Coinbase tests

Continual enhance of Coinbase tests

The revamp continues

Oh jeez the Orderbook part's unfinished don't look

Coinbase revamp, Orderbook still unfinished

* Coinbase revamp; CreateReport is still WIP

* More coinbase improvements; onto sandbox testing

* Coinbase revamp continues

* Coinbase revamp continues

* Coinbasepro revamp is ceaseless

* Coinbase revamp, starting on advanced trade API

* Coinbase Advanced Trade Starts in Ernest

V3 done, onto V2

Coinbase revamp nears completion

Coinbase revamp nears completion

Test commit should fail

Coinbase revamp nears completion

* Coinbase revamp stage wrapper

* Coinbase wrapper coherence continues

* Coinbase wrapper continues writhing

* Coinbase wrapper & codebase cleanup

* Coinbase updates & wrap progress

* More Coinbase wrapper progress

* Wrapper is wrapped, kinda

* Test & type checking

* Coinbase REST revamp finished

* Post-merge fix

* WS revamp begins

* WS Main Revamp Done?

* CB websocket tidying up

* Coinbase WS wrapperupperer

* Coinbase revamp done??

* Linter progress

* Continued lint cleanup

* Further lint cleanup

* Increased lint coverage

* Does this fix all sloppy reassigns & shadowing?

* Undoing retry policy change

* Documentation regeneration

* Coinbase code improvements

* Providing warning about known issue

* Updating an error to new format

* Making gocritic happy

* Review adherence

* Endpoints moved to V3 & nil pointer fixes

* Removing seemingly superfluous constant

* Glorious improvements

* Removing unused error

* Partial public endpoint addition

* Slight improvements

* Wrapper improvements; still a few errors left in other packages

* A lil Coinbase progress

* Json cleaning

* Lint appeasement

* Config repair

* Config fix (real)

* Little fix

* New public endpoint incorporation

* Additional fixes

* Improvements & Appeasements

* LineSaver

* Additional fixes

* Another fix

* Fixing picked nits

* Quick fixies

* Lil fixes

* Subscriptions: Add List.Enabled

* CoinbasePro: Add subscription templating

* fixup! CoinbasePro: Add subscription templating

* fixup! CoinbasePro: Add subscription templating

* Comment fix

* Subsequent fixes

* Issues hopefully fixed

* Lint fix

* Glorious fixes

* Json formatting

* ShazNits

* (L/N)i(n/)t

* Adding a test

* Tiny test improvement

* Template patch testing

* Fixes

* Further shaznits

* Lint nit

* JWT move and other fixes

* Small nits

* Shaznit, singular

* Post-merge fix

* Post-merge fixes

* Typo fix

* Some glorious nits

* Required changes

* Stop going

* Alias attempt

* Alias fix & test cleanup

* Test fix

* GetDepositAddress logic improvement

* Status update: Fixed

* Lint fix

* Happy birthday to PR 1480

* Cleanups

* Necessary nit corrections

* Fixing sillybug

* As per request

* Programming progress

* Order fixes

* Further fixies

* Test fix

* Pre-merge fixes

* More shaznits

* Context

* Sonic error handling

* Import fix

* Better Sonic error handling

* Perfect Sonic error handling?

* F purge

* Coinbase improvements

* API Update Conformity

* Coinbase continuation

* Coinbase order improvements

* Coinbase order improvements

* CreateOrderConfig improvements

* Managing API updates

* Coinbase API update progression

* jwt rename

* Comment link fix

* Coinbase v2 cleanup

* Post-merge fixes

* Review fixes

* GK's suggestions

* Linter fix

* Minor gbjk fixes

* Nit fixes

* Merge fix

* Lint fixes

* Coinbase rename stage 1

* Coinbase rename stage 2

* Coinbase rename stage 3

* Coinbase rename stage 4

* Coinbase rename final fix

* Coinbase: PoC on converting to request structs

* Applying requested changes

* Many review fixes, handled

* Thrashed by nits

* More minor modifications

* The last nit!?

---------

Co-authored-by: Gareth Kirwan <gbjkirwan@gmail.com>
2025-09-16 13:37:00 +10:00

490 lines
14 KiB
Go

package engine
import (
"os"
"slices"
"strings"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/thrasher-corp/gocryptotrader/config"
exchange "github.com/thrasher-corp/gocryptotrader/exchanges"
"github.com/thrasher-corp/gocryptotrader/exchanges/bitfinex"
"github.com/thrasher-corp/gocryptotrader/exchanges/bitstamp"
)
// blockedCIExchanges are exchanges that are not able to be tested on CI
var blockedCIExchanges = []string{
"binance", // binance API is banned from executing within the US where github Actions is ran
"bybit", // bybit API is banned from executing within the US where github Actions is ran
}
func isCITest() bool {
return os.Getenv("CI") == "true"
}
func TestLoadConfigWithSettings(t *testing.T) {
empty := ""
somePath := "somePath"
// Clean up after the tests
defer os.RemoveAll(somePath)
tests := []struct {
name string
flags []string
settings *Settings
want *string
wantErr bool
}{
{
name: "invalid file",
settings: &Settings{
ConfigFile: "nonExistent.json",
},
wantErr: true,
},
{
name: "test file",
settings: &Settings{
ConfigFile: config.TestFile,
CoreSettings: CoreSettings{EnableDryRun: true},
},
want: &empty,
wantErr: false,
},
{
name: "data dir in settings overrides config data dir",
flags: []string{"datadir"},
settings: &Settings{
ConfigFile: config.TestFile,
DataDir: somePath,
CoreSettings: CoreSettings{EnableDryRun: true},
},
want: &somePath,
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// prepare the 'flags'
flagSet := make(map[string]bool)
for _, v := range tt.flags {
flagSet[v] = true
}
// Run the test
got, err := loadConfigWithSettings(tt.settings, flagSet)
if (err != nil) != tt.wantErr {
t.Errorf("loadConfigWithSettings() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != nil || tt.want != nil {
if (got == nil && tt.want != nil) || (got != nil && tt.want == nil) {
t.Errorf("loadConfigWithSettings() = is nil %v, want nil %v", got == nil, tt.want == nil)
} else if got.DataDirectory != *tt.want {
t.Errorf("loadConfigWithSettings() = %v, want %v", got.DataDirectory, *tt.want)
}
}
})
}
}
func TestStartStopDoesNotCausePanic(t *testing.T) {
t.Parallel()
tempDir := t.TempDir()
botOne, err := NewFromSettings(&Settings{
ConfigFile: config.TestFile,
CoreSettings: CoreSettings{EnableDryRun: true},
DataDir: tempDir,
}, nil)
if err != nil {
t.Fatal(err)
}
botOne.Settings.EnableGRPCProxy = false
for i := range botOne.Config.Exchanges {
if botOne.Config.Exchanges[i].Name != testExchange {
// there is no need to load all exchanges for this test
botOne.Config.Exchanges[i].Enabled = false
}
}
if err = botOne.Start(); err != nil {
t.Error(err)
}
botOne.Stop()
}
var enableExperimentalTest = false
func TestStartStopTwoDoesNotCausePanic(t *testing.T) {
t.Parallel()
if !enableExperimentalTest {
t.Skip("test is functional, however does not need to be included in go test runs")
}
tempDir := t.TempDir()
tempDir2 := t.TempDir()
botOne, err := NewFromSettings(&Settings{
ConfigFile: config.TestFile,
CoreSettings: CoreSettings{EnableDryRun: true},
DataDir: tempDir,
}, nil)
if err != nil {
t.Error(err)
}
botOne.Settings.EnableGRPCProxy = false
botTwo, err := NewFromSettings(&Settings{
ConfigFile: config.TestFile,
CoreSettings: CoreSettings{EnableDryRun: true},
DataDir: tempDir2,
}, nil)
if err != nil {
t.Error(err)
}
botTwo.Settings.EnableGRPCProxy = false
if err = botOne.Start(); err != nil {
t.Error(err)
}
if err = botTwo.Start(); err != nil {
t.Error(err)
}
botOne.Stop()
botTwo.Stop()
}
func TestGetExchangeByName(t *testing.T) {
t.Parallel()
_, err := (*ExchangeManager)(nil).GetExchangeByName("tehehe")
assert.ErrorIs(t, err, ErrNilSubsystem)
em := NewExchangeManager()
exch, err := em.NewExchangeByName(testExchange)
require.NoError(t, err)
exch.SetDefaults()
exch.SetEnabled(true)
err = em.Add(exch)
require.NoError(t, err)
e := &Engine{ExchangeManager: em}
if !exch.IsEnabled() {
t.Errorf("TestGetExchangeByName: Unexpected result")
}
exch.SetEnabled(false)
bfx, err := e.GetExchangeByName(testExchange)
if err != nil {
t.Fatal(err)
}
if bfx.IsEnabled() {
t.Errorf("TestGetExchangeByName: Unexpected result")
}
if exch.GetName() != testExchange {
t.Errorf("TestGetExchangeByName: Unexpected result")
}
_, err = e.GetExchangeByName("Asdasd")
assert.ErrorIs(t, err, ErrExchangeNotFound)
}
func TestUnloadExchange(t *testing.T) {
t.Parallel()
em := NewExchangeManager()
exch, err := em.NewExchangeByName(testExchange)
require.NoError(t, err)
exch.SetDefaults()
exch.SetEnabled(true)
err = em.Add(exch)
require.NoError(t, err)
e := &Engine{
ExchangeManager: em,
Config: &config.Config{Exchanges: []config.Exchange{{Name: testExchange}}},
}
err = e.UnloadExchange("asdf")
assert.ErrorIs(t, err, config.ErrExchangeNotFound)
err = e.UnloadExchange(testExchange)
if err != nil {
t.Errorf("TestUnloadExchange: Failed to get exchange. %s",
err)
}
err = e.UnloadExchange(testExchange)
assert.ErrorIs(t, err, ErrExchangeNotFound)
}
func TestDryRunParamInteraction(t *testing.T) {
t.Parallel()
bot := &Engine{
ExchangeManager: NewExchangeManager(),
Settings: Settings{},
Config: &config.Config{
Exchanges: []config.Exchange{
{
Name: testExchange,
WebsocketTrafficTimeout: time.Second,
},
},
},
}
err := bot.LoadExchange(testExchange)
assert.NoError(t, err, "LoadExchange should not error")
exchCfg, err := bot.Config.GetExchangeConfig(testExchange)
if err != nil {
t.Error(err)
}
if exchCfg.Verbose {
t.Error("verbose should have been disabled")
}
if err = bot.UnloadExchange(testExchange); err != nil {
t.Error(err)
}
// Now set dryrun mode to true,
// enable exchange verbose mode and verify that verbose mode
// will be set on Bitfinex
bot.Settings.EnableDryRun = true
bot.Settings.CheckParamInteraction = true
bot.Settings.EnableExchangeVerbose = true
err = bot.LoadExchange(testExchange)
assert.NoError(t, err, "LoadExchange should not error")
exchCfg, err = bot.Config.GetExchangeConfig(testExchange)
if err != nil {
t.Error(err)
}
if !bot.Settings.EnableDryRun ||
!exchCfg.Verbose {
t.Error("dryrun should be true and verbose should be true")
}
}
func TestFlagSetWith(t *testing.T) {
var isRunning bool
flags := make(FlagSet)
// Flag not set default to config
flags.WithBool("NOT SET", &isRunning, true)
if !isRunning {
t.Fatalf("received: '%v' but expected: '%v'", isRunning, true)
}
flags.WithBool("NOT SET", &isRunning, false)
if isRunning {
t.Fatalf("received: '%v' but expected: '%v'", isRunning, false)
}
flags["IS SET"] = true
isRunning = true
// Flag set true which will override config
flags.WithBool("IS SET", &isRunning, true)
if !isRunning {
t.Fatalf("received: '%v' but expected: '%v'", isRunning, true)
}
flags.WithBool("IS SET", &isRunning, false)
if !isRunning {
t.Fatalf("received: '%v' but expected: '%v'", isRunning, true)
}
flags["IS SET"] = true
isRunning = false
// Flag set false which will override config
flags.WithBool("IS SET", &isRunning, true)
if isRunning {
t.Fatalf("received: '%v' but expected: '%v'", isRunning, false)
}
flags.WithBool("IS SET", &isRunning, false)
if isRunning {
t.Fatalf("received: '%v' but expected: '%v'", isRunning, false)
}
}
func TestRegisterWebsocketDataHandler(t *testing.T) {
t.Parallel()
var e *Engine
err := e.RegisterWebsocketDataHandler(nil, false)
require.ErrorIs(t, err, errNilBot)
e = &Engine{WebsocketRoutineManager: &WebsocketRoutineManager{}}
err = e.RegisterWebsocketDataHandler(func(_ string, _ any) error { return nil }, false)
require.NoError(t, err)
}
func TestSetDefaultWebsocketDataHandler(t *testing.T) {
t.Parallel()
var e *Engine
err := e.SetDefaultWebsocketDataHandler()
require.ErrorIs(t, err, errNilBot)
e = &Engine{WebsocketRoutineManager: &WebsocketRoutineManager{}}
err = e.SetDefaultWebsocketDataHandler()
require.NoError(t, err)
}
func TestSettingsPrint(t *testing.T) {
t.Parallel()
var s *Settings
s.PrintLoadedSettings()
s = &Settings{}
s.PrintLoadedSettings()
}
var unsupportedDefaultConfigExchanges = []string{
"poloniex", // poloniex has dropped support for the API GCT has implemented //TODO: drop this when supported
}
func TestGetDefaultConfigurations(t *testing.T) {
t.Parallel()
em := NewExchangeManager()
for i := range exchange.Exchanges {
name := strings.ToLower(exchange.Exchanges[i])
t.Run(name, func(t *testing.T) {
t.Parallel()
exch, err := em.NewExchangeByName(name)
require.NoError(t, err, "NewExchangeByName must not error")
if isCITest() && slices.Contains(blockedCIExchanges, name) {
t.Skipf("skipping %s due to CI test restrictions", name)
}
if slices.Contains(unsupportedDefaultConfigExchanges, name) {
t.Skipf("skipping %s unsupported", name)
}
defaultCfg, err := exchange.GetDefaultConfig(t.Context(), exch)
require.NoError(t, err, "GetDefaultConfig must not error")
require.NotNil(t, defaultCfg)
assert.NotEmpty(t, defaultCfg.Name, "Name should not be empty")
assert.True(t, defaultCfg.Enabled, "Enabled should have the correct value")
if exch.SupportsWebsocket() {
assert.Positive(t, defaultCfg.WebsocketResponseCheckTimeout, "WebsocketResponseCheckTimeout should be positive")
assert.Positive(t, defaultCfg.WebsocketResponseMaxLimit, "WebsocketResponseMaxLimit should be positive")
assert.Positive(t, defaultCfg.WebsocketTrafficTimeout, "WebsocketTrafficTimeout should be positive")
}
require.NoError(t, exch.Setup(defaultCfg), "Setup must not error")
})
}
}
func TestSetupExchanges(t *testing.T) {
t.Parallel()
t.Run("No enabled exchanges", func(t *testing.T) {
t.Parallel()
e := &Engine{
Config: &config.Config{Exchanges: []config.Exchange{{Name: testExchange}}},
}
assert.ErrorIs(t, e.SetupExchanges(), ErrNoExchangesLoaded)
})
t.Run("EnableAllExchanges with specific exchanges set", func(t *testing.T) {
t.Parallel()
e := &Engine{
Config: &config.Config{},
Settings: Settings{
CoreSettings: CoreSettings{
EnableAllExchanges: true,
Exchanges: "Bitstamp,Bitfinex",
},
},
}
assert.EqualError(t, e.SetupExchanges(), "cannot enable all exchanges and specific exchanges concurrently")
})
t.Run("Settings dry run toggling", func(t *testing.T) {
t.Parallel()
e := &Engine{
Config: &config.Config{},
Settings: Settings{
CoreSettings: CoreSettings{
EnableAllPairs: true,
EnableAllExchanges: true,
},
ExchangeTuningSettings: ExchangeTuningSettings{
EnableExchangeVerbose: true,
EnableExchangeWebsocketSupport: true,
EnableExchangeAutoPairUpdates: true,
DisableExchangeAutoPairUpdates: true,
HTTPUserAgent: "test",
HTTPProxy: "test",
HTTPTimeout: 1,
EnableExchangeHTTPDebugging: true,
},
},
}
assert.ErrorIs(t, e.SetupExchanges(), ErrNoExchangesLoaded)
assert.False(t, e.Settings.EnableDryRun)
e.Settings.CheckParamInteraction = true
assert.ErrorIs(t, e.SetupExchanges(), ErrNoExchangesLoaded)
assert.True(t, e.Settings.EnableDryRun)
})
// Test that overridden exchange inputs are handled correctly
testCases := []struct {
name string
exchangeString string
expectedError string
}{
{"Invalid exchange pair", "bob|jill", "exchange bob|jill not found"},
{"Single invalid exchange", "bob", "exchange bob not found"},
{"Mixed valid and invalid exchanges", "bob,bitstamp", "exchange bob not found"},
{"Valid exchange", "BiTSTaMp", "no exchanges have been loaded"}, // Proper exchange name, but not loaded
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
e := &Engine{
Config: &config.Config{},
Settings: Settings{CoreSettings: CoreSettings{Exchanges: tc.exchangeString}},
}
assert.ErrorContains(t, e.SetupExchanges(), tc.expectedError)
})
}
t.Run("Two valid exchanges with exchanges flag toggled", func(t *testing.T) {
t.Parallel()
e := &Engine{Config: &config.Config{}}
exchLoader := func(exch exchange.IBotExchange) {
exch.SetDefaults()
exch.GetBase().Features.Supports.RESTCapabilities.AutoPairUpdates = false
cfg, err := exchange.GetDefaultConfig(t.Context(), exch)
require.NoError(t, err)
e.Config.Exchanges = append(e.Config.Exchanges, *cfg)
}
e.ExchangeManager = NewExchangeManager()
exchLoader(new(bitstamp.Exchange))
exchLoader(new(bitfinex.Exchange))
assert.ElementsMatch(t, []string{"Bitstamp", "Bitfinex"}, e.Config.GetEnabledExchanges())
t.Run("Load specific exchange", func(t *testing.T) {
e.Settings.Exchanges = "BiTfInEx"
assert.NoError(t, e.SetupExchanges(), "SetupExchanges with a valid exchange should not error")
exchanges, err := e.ExchangeManager.GetExchanges()
require.NoError(t, err)
require.Len(t, exchanges, 1)
assert.Equal(t, "Bitfinex", exchanges[0].GetName())
})
t.Run("Load all enabled exchanges", func(t *testing.T) {
e.Settings.Exchanges = ""
assert.NoError(t, e.SetupExchanges(), "SetupExchanges with all enabled exchanges should not error")
exchanges, err := e.ExchangeManager.GetExchanges()
require.NoError(t, err)
require.Len(t, exchanges, 2)
exchangeNames := []string{exchanges[0].GetName(), exchanges[1].GetName()}
assert.ElementsMatch(t, []string{"Bitstamp", "Bitfinex"}, exchangeNames)
})
})
}