Files
gocryptotrader/engine/helpers_test.go
Rauno Ots 991dfed705 Engine: Refactor use of global Bot (#563)
* refactor use of global Bot

* make the dependency on having an existing running Engine more obvious
* use explicit dependency of engine in RPCServer
* reduce static dependencies in rpcserver
* improve helpers

* revert bad document update, add check for nil error in test

* add basic start stop test

* fix race condition in storage

* skip the test because of race conditions

* fix typo

* add empty line
2020-09-23 17:41:59 +10:00

918 lines
20 KiB
Go

package engine
import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"math/big"
"net"
"os"
"path/filepath"
"testing"
"time"
"github.com/thrasher-corp/gocryptotrader/common"
"github.com/thrasher-corp/gocryptotrader/common/file"
"github.com/thrasher-corp/gocryptotrader/config"
"github.com/thrasher-corp/gocryptotrader/currency"
"github.com/thrasher-corp/gocryptotrader/exchanges/account"
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
"github.com/thrasher-corp/gocryptotrader/exchanges/orderbook"
"github.com/thrasher-corp/gocryptotrader/exchanges/stats"
"github.com/thrasher-corp/gocryptotrader/exchanges/ticker"
)
var (
helperTestLoaded = false
)
func SetupTestHelpers(t *testing.T) *Engine {
if !helperTestLoaded {
if Bot == nil {
Bot = new(Engine)
}
Bot.Config = &config.Cfg
err := Bot.Config.LoadConfig(config.TestFile, true)
if err != nil {
t.Fatalf("SetupTest: Failed to load config: %s", err)
}
err = Bot.Config.RetrieveConfigCurrencyPairs(true, asset.Spot)
if err != nil {
t.Fatalf("Failed to retrieve config currency pairs. %s", err)
}
helperTestLoaded = true
}
if Bot.GetExchangeByName(testExchange) == nil {
err := Bot.LoadExchange(testExchange, false, nil)
if err != nil {
t.Fatalf("SetupTest: Failed to load exchange: %s", err)
}
}
if Bot.GetExchangeByName(fakePassExchange) == nil {
err := addPassingFakeExchange(testExchange)
if err != nil {
t.Fatalf("SetupTest: Failed to load exchange: %s", err)
}
}
return Bot
}
func TestGetExchangeOTPs(t *testing.T) {
bot := SetupTestHelpers(t)
_, err := bot.GetExchangeOTPs()
if err == nil {
t.Fatal("Expected err with no exchange OTP secrets set")
}
bfxCfg, err := bot.Config.GetExchangeConfig("Bitfinex")
if err != nil {
t.Fatal(err)
}
bCfg, err := bot.Config.GetExchangeConfig("Bitstamp")
if err != nil {
t.Fatal(err)
}
bfxCfg.API.Credentials.OTPSecret = "JBSWY3DPEHPK3PXP"
bCfg.API.Credentials.OTPSecret = "JBSWY3DPEHPK3PXP"
result, err := bot.GetExchangeOTPs()
if err != nil {
t.Fatal(err)
}
if len(result) != 2 {
t.Fatal("Expected 2 OTP results")
}
bfxCfg.API.Credentials.OTPSecret = "°"
result, err = bot.GetExchangeOTPs()
if err != nil {
t.Fatal(err)
}
if len(result) != 1 {
t.Fatal("Expected 1 OTP code with invalid OTP Secret")
}
// Flush settings
bfxCfg.API.Credentials.OTPSecret = ""
bCfg.API.Credentials.OTPSecret = ""
}
func TestGetExchangeoOTPByName(t *testing.T) {
bot := SetupTestHelpers(t)
_, err := bot.GetExchangeoOTPByName("Bitstamp")
if err == nil {
t.Fatal("Expected err with no exchange OTP secrets set")
}
bCfg, err := bot.Config.GetExchangeConfig("Bitstamp")
if err != nil {
t.Fatal(err)
}
bCfg.API.Credentials.OTPSecret = "JBSWY3DPEHPK3PXP"
result, err := bot.GetExchangeoOTPByName("Bitstamp")
if err != nil {
t.Fatal(err)
}
if result == "" {
t.Fatal("Expected valid OTP code")
}
// Flush setting
bCfg.API.Credentials.OTPSecret = ""
}
func TestGetAuthAPISupportedExchanges(t *testing.T) {
e := SetupTestHelpers(t)
if result := e.GetAuthAPISupportedExchanges(); len(result) != 1 {
t.Fatal("Unexpected result", result)
}
}
func TestIsOnline(t *testing.T) {
e := SetupTestHelpers(t)
if r := e.IsOnline(); r {
t.Fatal("Unexpected result")
}
if err := e.ConnectionManager.Start(&e.Config.ConnectionMonitor); err != nil {
t.Fatal(err)
}
tick := time.NewTicker(time.Second * 5)
defer tick.Stop()
for {
select {
case <-tick.C:
t.Fatal("Test timeout")
default:
if e.IsOnline() {
if err := e.ConnectionManager.Stop(); err != nil {
t.Fatal("unable to shutdown connection manager")
}
return
}
}
}
}
func TestGetAvailableExchanges(t *testing.T) {
e := SetupTestHelpers(t)
if r := len(e.GetAvailableExchanges()); r == 0 {
t.Error("Expected len > 0")
}
}
func TestGetSpecificAvailablePairs(t *testing.T) {
e := SetupTestHelpers(t)
assetType := asset.Spot
result := e.GetSpecificAvailablePairs(true, true, true, false, assetType)
btsusd, err := currency.NewPairFromStrings("BTC", "USD")
if err != nil {
t.Fatal(err)
}
if !result.Contains(btsusd, true) {
t.Fatal("Unexpected result")
}
btcusdt, err := currency.NewPairFromStrings("BTC", "USDT")
if err != nil {
t.Fatal(err)
}
if !result.Contains(btcusdt, false) {
t.Fatal("Unexpected result")
}
result = e.GetSpecificAvailablePairs(true, true, false, false, assetType)
if result.Contains(btcusdt, false) {
t.Fatal("Unexpected result")
}
ltcbtc, err := currency.NewPairFromStrings("LTC", "BTC")
if err != nil {
t.Fatal(err)
}
result = e.GetSpecificAvailablePairs(true, false, false, true, assetType)
if !result.Contains(ltcbtc, false) {
t.Fatal("Unexpected result")
}
}
func TestIsRelatablePairs(t *testing.T) {
SetupTestHelpers(t)
xbtusd, err := currency.NewPairFromStrings("XBT", "USD")
if err != nil {
t.Fatal(err)
}
btcusd, err := currency.NewPairFromStrings("BTC", "USD")
if err != nil {
t.Fatal(err)
}
// Test relational pairs with similar names
result := IsRelatablePairs(xbtusd, btcusd, false)
if !result {
t.Fatal("Unexpected result")
}
// Test relational pairs with similar names reversed
result = IsRelatablePairs(btcusd, xbtusd, false)
if !result {
t.Fatal("Unexpected result")
}
btcusdt, err := currency.NewPairFromStrings("BTC", "USDT")
if err != nil {
t.Fatal(err)
}
// Test relational pairs with similar names but with Tether support disabled
result = IsRelatablePairs(xbtusd, btcusdt, false)
if result {
t.Fatal("Unexpected result")
}
xbtusdt, err := currency.NewPairFromStrings("XBT", "USDT")
if err != nil {
t.Fatal(err)
}
// Test relational pairs with similar names but with Tether support enabled
result = IsRelatablePairs(xbtusdt, btcusd, true)
if !result {
t.Fatal("Unexpected result")
}
aeusdt, err := currency.NewPairFromStrings("AE", "USDT")
if err != nil {
t.Fatal(err)
}
usdtae, err := currency.NewPairDelimiter("USDT-AE", "-")
if err != nil {
t.Fatal(err)
}
// Test relational pairs with different ordering, a delimiter and with
// Tether support enabled
result = IsRelatablePairs(aeusdt, usdtae, true)
if !result {
t.Fatal("Unexpected result")
}
// Test relational pairs with different ordering, a delimiter and with
// Tether support disabled
result = IsRelatablePairs(aeusdt, usdtae, false)
if !result {
t.Fatal("Unexpected result")
}
xbteur, err := currency.NewPairFromStrings("XBT", "EUR")
if err != nil {
t.Fatal(err)
}
btcaud, err := currency.NewPairFromStrings("BTC", "AUD")
if err != nil {
t.Fatal(err)
}
// Test relationl pairs with similar names and different fiat currencies
result = IsRelatablePairs(xbteur, btcaud, false)
if !result {
t.Fatal("Unexpected result")
}
usdbtc, err := currency.NewPairFromStrings("USD", "BTC")
if err != nil {
t.Fatal(err)
}
btceur, err := currency.NewPairFromStrings("BTC", "EUR")
if err != nil {
t.Fatal(err)
}
// Test relationl pairs with similar names, different fiat currencies and
// with different ordering
result = IsRelatablePairs(usdbtc, btceur, false)
if !result { // Is this really expected result???
t.Fatal("Unexpected result")
}
// Test relationl pairs with similar names, different fiat currencies and
// with Tether enabled
result = IsRelatablePairs(usdbtc, btcusdt, true)
if !result {
t.Fatal("Unexpected result")
}
ltcbtc, err := currency.NewPairFromStrings("LTC", "BTC")
if err != nil {
t.Fatal(err)
}
btcltc, err := currency.NewPairFromStrings("BTC", "LTC")
if err != nil {
t.Fatal(err)
}
// Test relationl crypto pairs with similar names
result = IsRelatablePairs(ltcbtc, btcltc, false)
if !result {
t.Fatal("Unexpected result")
}
ltceth, err := currency.NewPairFromStrings("LTC", "ETH")
if err != nil {
t.Fatal(err)
}
btceth, err := currency.NewPairFromStrings("BTC", "ETH")
if err != nil {
t.Fatal(err)
}
// Test relationl crypto pairs with similar different pairs
result = IsRelatablePairs(ltceth, btceth, false)
if result {
t.Fatal("Unexpected result")
}
// Test relationl crypto pairs with similar different pairs and with USDT
// enabled
usdtusd, err := currency.NewPairFromStrings("USDT", "USD")
if err != nil {
t.Fatal(err)
}
result = IsRelatablePairs(usdtusd, btcusd, true)
if result {
t.Fatal("Unexpected result")
}
xbtltc, err := currency.NewPairFromStrings("XBT", "LTC")
if err != nil {
t.Fatal(err)
}
// Test relationl crypto pairs with with similar names
result = IsRelatablePairs(xbtltc, btcltc, false)
if !result {
t.Fatal("Unexpected result")
}
ltcxbt, err := currency.NewPairFromStrings("LTC", "XBT")
if err != nil {
t.Fatal(err)
}
// Test relationl crypto pairs with different ordering and similar names
result = IsRelatablePairs(ltcxbt, btcltc, false)
if !result {
t.Fatal("Unexpected result")
}
// Test edge case between two pairs when currency translations were causing
// non-relational pairs to be relatable
eurusd, err := currency.NewPairFromStrings("EUR", "USD")
if err != nil {
t.Fatal(err)
}
result = IsRelatablePairs(eurusd, btcusd, false)
if result {
t.Fatal("Unexpected result")
}
}
func TestGetRelatableCryptocurrencies(t *testing.T) {
SetupTestHelpers(t)
btcltc, err := currency.NewPairFromStrings("BTC", "LTC")
if err != nil {
t.Fatal(err)
}
btcbtc, err := currency.NewPairFromStrings("BTC", "BTC")
if err != nil {
t.Fatal(err)
}
ltcltc, err := currency.NewPairFromStrings("LTC", "LTC")
if err != nil {
t.Fatal(err)
}
btceth, err := currency.NewPairFromStrings("BTC", "ETH")
if err != nil {
t.Fatal(err)
}
p := GetRelatableCryptocurrencies(btcltc)
if p.Contains(btcltc, true) {
t.Fatal("Unexpected result")
}
if p.Contains(btcbtc, true) {
t.Fatal("Unexpected result")
}
if p.Contains(ltcltc, true) {
t.Fatal("Unexpected result")
}
if !p.Contains(btceth, true) {
t.Fatal("Unexpected result")
}
p = GetRelatableCryptocurrencies(btcltc)
if p.Contains(btcltc, true) {
t.Fatal("Unexpected result")
}
if p.Contains(btcbtc, true) {
t.Fatal("Unexpected result")
}
if p.Contains(ltcltc, true) {
t.Fatal("Unexpected result")
}
if !p.Contains(btceth, true) {
t.Fatal("Unexpected result")
}
}
func TestGetRelatableFiatCurrencies(t *testing.T) {
SetupTestHelpers(t)
btsusd, err := currency.NewPairFromStrings("BTC", "USD")
if err != nil {
t.Fatal(err)
}
btceur, err := currency.NewPairFromStrings("BTC", "EUR")
if err != nil {
t.Fatal(err)
}
p := GetRelatableFiatCurrencies(btsusd)
if !p.Contains(btceur, true) {
t.Fatal("Unexpected result")
}
btczar, err := currency.NewPairFromStrings("BTC", "ZAR")
if err != nil {
t.Fatal(err)
}
p = GetRelatableFiatCurrencies(btsusd)
if !p.Contains(btczar, true) {
t.Fatal("Unexpected result")
}
}
func TestMapCurrenciesByExchange(t *testing.T) {
e := SetupTestHelpers(t)
var pairs = []currency.Pair{
currency.NewPair(currency.BTC, currency.USD),
currency.NewPair(currency.BTC, currency.EUR),
}
result := e.MapCurrenciesByExchange(pairs, true, asset.Spot)
pairs, ok := result["Bitstamp"]
if !ok {
t.Fatal("Unexpected result")
}
if len(pairs) != 2 {
t.Fatal("Unexpected result")
}
}
func TestGetExchangeNamesByCurrency(t *testing.T) {
e := SetupTestHelpers(t)
assetType := asset.Spot
btsusd, err := currency.NewPairFromStrings("BTC", "USD")
if err != nil {
t.Fatal(err)
}
btcjpy, err := currency.NewPairFromStrings("BTC", "JPY")
if err != nil {
t.Fatal(err)
}
blahjpy, err := currency.NewPairFromStrings("blah", "JPY")
if err != nil {
t.Fatal(err)
}
result := e.GetExchangeNamesByCurrency(btsusd,
true,
assetType)
if !common.StringDataCompare(result, "Bitstamp") {
t.Fatal("Unexpected result")
}
result = e.GetExchangeNamesByCurrency(btcjpy,
true,
assetType)
if !common.StringDataCompare(result, "Bitflyer") {
t.Fatal("Unexpected result")
}
result = e.GetExchangeNamesByCurrency(blahjpy,
true,
assetType)
if len(result) > 0 {
t.Fatal("Unexpected result")
}
}
func TestGetSpecificOrderbook(t *testing.T) {
e := SetupTestHelpers(t)
e.LoadExchange("Bitstamp", false, nil)
var bids []orderbook.Item
bids = append(bids, orderbook.Item{Price: 1000, Amount: 1})
base := orderbook.Base{
Pair: currency.NewPair(currency.BTC, currency.USD),
Bids: bids,
ExchangeName: "Bitstamp",
AssetType: asset.Spot,
}
err := base.Process()
if err != nil {
t.Fatal("Unexpected result", err)
}
btsusd, err := currency.NewPairFromStrings("BTC", "USD")
if err != nil {
t.Fatal(err)
}
ob, err := e.GetSpecificOrderbook(btsusd, "Bitstamp", asset.Spot)
if err != nil {
t.Fatal(err)
}
if ob.Bids[0].Price != 1000 {
t.Fatal("Unexpected result")
}
ethltc, err := currency.NewPairFromStrings("ETH", "LTC")
if err != nil {
t.Fatal(err)
}
_, err = e.GetSpecificOrderbook(ethltc, "Bitstamp", asset.Spot)
if err == nil {
t.Fatal("Unexpected result")
}
e.UnloadExchange("Bitstamp")
}
func TestGetSpecificTicker(t *testing.T) {
e := SetupTestHelpers(t)
e.LoadExchange("Bitstamp", false, nil)
p, err := currency.NewPairFromStrings("BTC", "USD")
if err != nil {
t.Fatal(err)
}
err = ticker.ProcessTicker(&ticker.Price{
Pair: p,
Last: 1000,
AssetType: asset.Spot,
ExchangeName: "Bitstamp"})
if err != nil {
t.Fatal("ProcessTicker error", err)
}
tick, err := e.GetSpecificTicker(p, "Bitstamp", asset.Spot)
if err != nil {
t.Fatal(err)
}
if tick.Last != 1000 {
t.Fatal("Unexpected result")
}
ethltc, err := currency.NewPairFromStrings("ETH", "LTC")
if err != nil {
t.Fatal(err)
}
_, err = e.GetSpecificTicker(ethltc, "Bitstamp", asset.Spot)
if err == nil {
t.Fatal("Unexpected result")
}
e.UnloadExchange("Bitstamp")
}
func TestGetCollatedExchangeAccountInfoByCoin(t *testing.T) {
SetupTestHelpers(t)
var exchangeInfo []account.Holdings
var bitfinexHoldings account.Holdings
bitfinexHoldings.Exchange = "Bitfinex"
bitfinexHoldings.Accounts = append(bitfinexHoldings.Accounts,
account.SubAccount{
Currencies: []account.Balance{
{
CurrencyName: currency.BTC,
TotalValue: 100,
Hold: 0,
},
},
})
exchangeInfo = append(exchangeInfo, bitfinexHoldings)
var bitstampHoldings account.Holdings
bitstampHoldings.Exchange = "Bitstamp"
bitstampHoldings.Accounts = append(bitstampHoldings.Accounts,
account.SubAccount{
Currencies: []account.Balance{
{
CurrencyName: currency.LTC,
TotalValue: 100,
Hold: 0,
},
{
CurrencyName: currency.BTC,
TotalValue: 100,
Hold: 0,
},
},
})
exchangeInfo = append(exchangeInfo, bitstampHoldings)
result := GetCollatedExchangeAccountInfoByCoin(exchangeInfo)
if len(result) == 0 {
t.Fatal("Unexpected result")
}
amount, ok := result[currency.BTC]
if !ok {
t.Fatal("Expected currency was not found in result map")
}
if amount.TotalValue != 200 {
t.Fatal("Unexpected result")
}
_, ok = result[currency.ETH]
if ok {
t.Fatal("Unexpected result")
}
}
func TestGetExchangeHighestPriceByCurrencyPair(t *testing.T) {
SetupTestHelpers(t)
p, err := currency.NewPairFromStrings("BTC", "USD")
if err != nil {
t.Fatal(err)
}
stats.Add("Bitfinex", p, asset.Spot, 1000, 10000)
stats.Add("Bitstamp", p, asset.Spot, 1337, 10000)
exchangeName, err := GetExchangeHighestPriceByCurrencyPair(p, asset.Spot)
if err != nil {
t.Error(err)
}
if exchangeName != "Bitstamp" {
t.Error("Unexpected result")
}
btcaud, err := currency.NewPairFromStrings("BTC", "AUD")
if err != nil {
t.Fatal(err)
}
_, err = GetExchangeHighestPriceByCurrencyPair(btcaud, asset.Spot)
if err == nil {
t.Error("Unexpected result")
}
}
func TestGetExchangeLowestPriceByCurrencyPair(t *testing.T) {
SetupTestHelpers(t)
p, err := currency.NewPairFromStrings("BTC", "USD")
if err != nil {
t.Fatal(err)
}
stats.Add("Bitfinex", p, asset.Spot, 1000, 10000)
stats.Add("Bitstamp", p, asset.Spot, 1337, 10000)
exchangeName, err := GetExchangeLowestPriceByCurrencyPair(p, asset.Spot)
if err != nil {
t.Error(err)
}
if exchangeName != "Bitfinex" {
t.Error("Unexpected result")
}
btcaud, err := currency.NewPairFromStrings("BTC", "AUD")
if err != nil {
t.Fatal(err)
}
_, err = GetExchangeLowestPriceByCurrencyPair(btcaud, asset.Spot)
if err == nil {
t.Error("Unexpected reuslt")
}
}
func TestGetCryptocurrenciesByExchange(t *testing.T) {
e := SetupTestHelpers(t)
_, err := e.GetCryptocurrenciesByExchange("Bitfinex", false, false, asset.Spot)
if err != nil {
t.Fatalf("Err %s", err)
}
}
func TestGetExchangeNames(t *testing.T) {
bot := SetupTestHelpers(t)
if e := bot.GetExchangeNames(true); len(e) == 0 {
t.Error("exchange names should be populated")
}
if err := bot.UnloadExchange(testExchange); err != nil {
t.Fatal(err)
}
if e := bot.GetExchangeNames(true); common.StringDataCompare(e, testExchange) {
t.Error("Bitstamp should be missing")
}
if e := bot.GetExchangeNames(false); len(e) != len(bot.Config.Exchanges) {
t.Errorf("Expected %v Received %v", len(e), len(bot.Config.Exchanges))
}
}
func mockCert(derType string, notAfter time.Time) ([]byte, error) {
privKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
return nil, err
}
serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
if err != nil {
return nil, err
}
host, err := os.Hostname()
if err != nil {
return nil, err
}
dnsNames := []string{host}
if host != "localhost" {
dnsNames = append(dnsNames, "localhost")
}
if notAfter.IsZero() {
notAfter = time.Now().Add(time.Hour * 24 * 365)
}
template := x509.Certificate{
SerialNumber: serialNumber,
Subject: pkix.Name{
Organization: []string{"gocryptotrader"},
CommonName: host,
},
NotBefore: time.Now(),
NotAfter: notAfter,
IsCA: true,
BasicConstraintsValid: true,
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
IPAddresses: []net.IP{
net.ParseIP("127.0.0.1"),
net.ParseIP("::1"),
},
DNSNames: dnsNames,
}
derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &privKey.PublicKey, privKey)
if err != nil {
return nil, err
}
if derType == "" {
derType = "CERTIFICATE"
}
certData := pem.EncodeToMemory(&pem.Block{Type: derType, Bytes: derBytes})
if certData == nil {
return nil, err
}
return certData, nil
}
func TestVerifyCert(t *testing.T) {
t.Parallel()
tester := []struct {
PEMType string
CreateBypass bool
NotAfter time.Time
ErrorExpected error
}{
{
ErrorExpected: nil,
},
{
CreateBypass: true,
ErrorExpected: errCertDataIsNil,
},
{
PEMType: "MEOW",
ErrorExpected: errCertTypeInvalid,
},
{
NotAfter: time.Now().Add(-time.Hour),
ErrorExpected: errCertExpired,
},
}
for x := range tester {
var cert []byte
var err error
if !tester[x].CreateBypass {
cert, err = mockCert(tester[x].PEMType, tester[x].NotAfter)
if err != nil {
t.Errorf("test %d unexpected error: %s", x, err)
continue
}
}
err = verifyCert(cert)
if err != tester[x].ErrorExpected {
t.Fatalf("test %d expected %v, got %v", x, tester[x].ErrorExpected, err)
}
}
}
func TestCheckAndGenCerts(t *testing.T) {
t.Parallel()
tempDir := filepath.Join(os.TempDir(), "gct-temp-tls")
cleanup := func() {
if err := os.RemoveAll(tempDir); err != nil {
t.Errorf("unable to remove temp dir %s, manual deletion required", tempDir)
}
}
if err := genCert(tempDir); err != nil {
cleanup()
t.Fatal(err)
}
defer cleanup()
if err := checkCerts(tempDir); err != nil {
t.Fatal(err)
}
// Now delete cert.pem and test regeneration of cert/key files
certFile := filepath.Join(tempDir, "cert.pem")
if err := os.Remove(certFile); err != nil {
t.Fatal(err)
}
if err := checkCerts(tempDir); err != nil {
t.Fatal(err)
}
// Now call checkCerts to test an expired cert
certData, err := mockCert("", time.Now().Add(-time.Hour))
if err != nil {
t.Fatal(err)
}
err = file.Write(certFile, certData)
if err != nil {
t.Fatal(err)
}
if err = checkCerts(tempDir); err != nil {
t.Fatal(err)
}
}