Files
gocryptotrader/exchanges/subscription/template_test.go
Gareth Kirwan d069ff2bf4 Subscriptions: Fix no enabled pairs for one asset (#1792)
If one asset is enabled but has no enabled pairs, we should ignore that
asset, even for non-pair related subscriptions.
That matches the existing code, but wasn't happening in the context of
asset.All subscriptions with just one asset in this state.
If a user wanted to have non-pair subscriptions, they should use
asset.Empty. Would expect other breakages with no pairs enabled, too.

Note: No enabled pairs for an enabled asset is a transient issue which
can occur due to enableOnePair racing against no available pairs. The
second run, available pairs would be populated and enableOnePair would
work. This situation could persist due to running with --dry or
containerised, though.

Fixes:
`
Okx websocket: subscription failure, myOrders all : subscription template did not generate the expected number of pair records for spread: Got 1;
Expected 0
`

Relates to #1420
2025-03-06 12:08:08 +11:00

152 lines
7.0 KiB
Go

package subscription
import (
"errors"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/thrasher-corp/gocryptotrader/common"
"github.com/thrasher-corp/gocryptotrader/currency"
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
"github.com/thrasher-corp/gocryptotrader/exchanges/kline"
)
// TestExpandTemplates exercises ExpandTemplates
func TestExpandTemplates(t *testing.T) {
t.Parallel()
e := newMockEx()
e.tpl = "subscriptions.tmpl"
// Functionality tests
l := List{
{Channel: "single-channel"},
{Channel: "expand-assets", Asset: asset.All, Interval: kline.FifteenMin},
{Channel: "expand-pairs", Asset: asset.All, Levels: 1},
{Channel: "expand-pairs", Asset: asset.Spot, Levels: 2},
{Channel: "single-channel", QualifiedChannel: "just one sub already processed"},
{Channel: "update-asset-pairs", Asset: asset.All},
{Channel: "expand-pairs", Asset: asset.Spot, Pairs: e.pairs[asset.Spot][0:2], Levels: 3},
{Channel: "batching", Asset: asset.Spot},
{Channel: "single-channel", Authenticated: true},
}
got, err := l.ExpandTemplates(e)
require.NoError(t, err, "ExpandTemplates must not error")
exp := List{
{Channel: "single-channel", QualifiedChannel: "single-channel"},
{Channel: "expand-assets", QualifiedChannel: "spot-expand-assets@15m", Asset: asset.Spot, Pairs: e.pairs[asset.Spot], Interval: kline.FifteenMin},
{Channel: "expand-assets", QualifiedChannel: "future-expand-assets@15m", Asset: asset.Futures, Pairs: e.pairs[asset.Futures], Interval: kline.FifteenMin},
{Channel: "single-channel", QualifiedChannel: "just one sub already processed"},
{Channel: "update-asset-pairs", QualifiedChannel: "spot-btcusdt-update-asset-pairs", Asset: asset.Spot, Pairs: currency.Pairs{btcusdtPair}},
{Channel: "expand-pairs", QualifiedChannel: "spot-USDTBTC-expand-pairs@3", Asset: asset.Spot, Pairs: e.pairs[asset.Spot][1:2], Levels: 3},
{Channel: "expand-pairs", QualifiedChannel: "spot-USDCETH-expand-pairs@3", Asset: asset.Spot, Pairs: e.pairs[asset.Spot][:1], Levels: 3},
}
for a, pairs := range e.pairs {
if a == asset.Index { // Not IsAssetWebsocketEnabled
continue
}
for _, p := range common.SortStrings(pairs) {
pStr := p.Swap().String()
if a == asset.Spot {
exp = append(exp, List{
{Channel: "expand-pairs", QualifiedChannel: "spot-" + pStr + "-expand-pairs@1", Asset: a, Pairs: currency.Pairs{p}, Levels: 1},
&Subscription{Channel: "expand-pairs", QualifiedChannel: "spot-" + pStr + "-expand-pairs@2", Asset: a, Pairs: currency.Pairs{p}, Levels: 2},
}...)
} else {
exp = append(exp,
&Subscription{Channel: "expand-pairs", QualifiedChannel: "future-" + pStr + "-expand-pairs@1", Asset: a, Pairs: currency.Pairs{p}, Levels: 1},
)
}
}
}
for _, b := range common.Batch(common.SortStrings(e.pairs[asset.Spot]), 3) {
exp = append(exp, &Subscription{Channel: "batching", QualifiedChannel: "spot-" + b.Join() + "-batching", Asset: asset.Spot, Pairs: b})
}
if !equalLists(t, exp, got) {
t.FailNow() // If the first list isn't equal testing it again will duplicate test failures
}
e.auth = true
got, err = l.ExpandTemplates(e)
require.NoError(t, err, "ExpandTemplates must not error")
exp = append(exp, &Subscription{Channel: "single-channel", QualifiedChannel: "single-channel-authed"})
equalLists(t, exp, got)
// Test with just one asset to ensure asset.All works, and disabled assets don't error
e.assets = e.assets[:1]
l = List{
{Channel: "expand-assets", Asset: asset.All, Interval: kline.OneHour},
{Channel: "expand-pairs", Asset: asset.All, Levels: 4},
{Channel: "single-channel", Asset: asset.Futures},
}
got, err = l.ExpandTemplates(e)
require.NoError(t, err, "ExpandTemplates must not error")
exp = List{
{Channel: "expand-assets", QualifiedChannel: "spot-expand-assets@1h", Asset: asset.Spot, Pairs: e.pairs[asset.Spot], Interval: kline.OneHour},
}
for _, p := range e.pairs[asset.Spot] {
exp = append(exp, List{
{Channel: "expand-pairs", QualifiedChannel: "spot-" + p.Swap().String() + "-expand-pairs@4", Asset: asset.Spot, Pairs: currency.Pairs{p}, Levels: 4},
}...)
}
equalLists(t, exp, got)
// Users can specify pairs which aren't available, even across diverse assets
// Use-case: Coinbasepro user sub for futures BTC-USD would return all BTC pairs and all USD pairs, even though BTC-USD might not be enabled or available
p := currency.Pairs{currency.NewPairWithDelimiter("BEAR", "PEAR", "🐻")}
got, err = List{{Channel: "expand-pairs", Asset: asset.All, Pairs: p}}.ExpandTemplates(e)
require.NoError(t, err, "Must not error with fictional pairs")
exp = List{{Channel: "expand-pairs", QualifiedChannel: "spot-PEARBEAR-expand-pairs@0", Asset: asset.Spot, Pairs: p}}
equalLists(t, exp, got)
// Error cases
_, err = List{{Channel: "nil"}}.ExpandTemplates(e)
assert.ErrorIs(t, err, errInvalidTemplate, "Should get correct error on nil template")
e.tpl = "errors.tmpl"
_, err = List{{Channel: "error1"}}.ExpandTemplates(e)
assert.ErrorContains(t, err, "wrong number of args for String", "Should error correctly with execution error")
_, err = List{{Channel: "empty-content", Asset: asset.Spot}}.ExpandTemplates(e)
assert.ErrorIs(t, err, errNoTemplateContent, "Should error correctly when no content generated")
assert.ErrorContains(t, err, "empty-content", "Should error correctly when no content generated")
_, err = List{{Channel: "error2", Asset: asset.All}}.ExpandTemplates(e)
assert.ErrorIs(t, err, errAssetRecords, "Should error correctly when invalid number of asset entries")
_, err = List{{Channel: "error3", Asset: asset.Spot}}.ExpandTemplates(e)
assert.ErrorIs(t, err, errPairRecords, "Should error correctly when invalid number of pair entries")
_, err = List{{Channel: "error4", Asset: asset.Spot}}.ExpandTemplates(e)
assert.ErrorIs(t, err, errTooManyBatchSizePerAsset, "Should error correctly when too many BatchSize directives")
_, err = List{{Channel: "error5", Asset: asset.Spot}}.ExpandTemplates(e)
assert.ErrorIs(t, err, common.ErrTypeAssertFailure, "Should error correctly when batch size isn't an int")
e.tpl = "parse-error.tmpl"
e.tpl = "parse-error.tmpl"
_, err = l.ExpandTemplates(e)
assert.ErrorContains(t, err, "function \"explode\" not defined", "Should error correctly on unparsable template")
e.errFormat = errors.New("the planet Krypton is gone")
_, err = l.ExpandTemplates(e)
assert.ErrorIs(t, err, e.errFormat, "Should error correctly on GetPairFormat")
e.errPairs = errors.New("bad parenting from Jor-El")
_, err = l.ExpandTemplates(e)
assert.ErrorIs(t, err, e.errPairs, "Should error correctly on GetEnabledPairs")
l = List{{Channel: "single-channel", QualifiedChannel: "already happy"}}
got, err = l.ExpandTemplates(e)
require.NoError(t, err)
require.Len(t, got, 1, "Must get back the one sub")
assert.Equal(t, "already happy", l[0].QualifiedChannel, "Should get back the one sub")
assert.NotSame(t, &got, &l, "Should get back a different actual list")
_, err = List{{Channel: "nil"}}.ExpandTemplates(e)
assert.ErrorIs(t, err, errInvalidTemplate, "Should get correct error on nil template")
}