mirror of
https://github.com/d0zingcat/gocryptotrader.git
synced 2026-05-13 23:16:45 +00:00
exchanges/websocket: Implement subscription configuration (#1394)
* Websockets: Move Subscription to its own package This allows the small type to be imported from both `config` and from `stream` without an import cycle, so we don't have to repeat ourselves * Subs: Renamed Currency to Pair This was being mis-used through much of the code, and since we're already touching everything, we might as well fix it * Websockets: Add Subscription configuration * Binance: Add subscription configuration * Kucoin: Subscription configuration * Simplify GenerateDefaultSubs * Improve TestGenSubs coverage * Test Candle Sub generation * Support Candle intervals * Full responsibility for formatting Channel name on GenerateDefaultSubs OR consumer of Subscribe * Simplify generatePayloads as a result * Fix test coverage of asset types in processMarketSnapshot * Exchanges: Abstract ParallelChanOp * Tests: Generic ws mock instances * Kucoin: Fix intermittent conflict in test currs Use isolated test instance for `TestGetOpenInterest`. `TestGetOpenInterest` would occassionally change pairs before GenerateDefault Subs.
This commit is contained in:
159
internal/testing/exchange/exchange.go
Normal file
159
internal/testing/exchange/exchange.go
Normal file
@@ -0,0 +1,159 @@
|
||||
package exchange
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
"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/mock"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/sharedtestvalues"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/subscription"
|
||||
)
|
||||
|
||||
// TestInstance takes an empty exchange instance and loads config for it from testdata/configtest and connects a NewTestWebsocket
|
||||
func TestInstance(e exchange.IBotExchange) error {
|
||||
cfg := config.GetConfig()
|
||||
err := cfg.LoadConfig("../../testdata/configtest.json", true)
|
||||
if err != nil {
|
||||
return fmt.Errorf("LoadConfig() error: %w", err)
|
||||
}
|
||||
parts := strings.Split(fmt.Sprintf("%T", e), ".")
|
||||
if len(parts) != 2 {
|
||||
return errors.New("unexpected parts splitting exchange type name")
|
||||
}
|
||||
eName := parts[1]
|
||||
exchConf, err := cfg.GetExchangeConfig(eName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("GetExchangeConfig(`%s`) error: %w", eName, err)
|
||||
}
|
||||
e.SetDefaults()
|
||||
b := e.GetBase()
|
||||
b.Websocket = sharedtestvalues.NewTestWebsocket()
|
||||
err = e.Setup(exchConf)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Setup() error: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// httpMockFile is a consistent path under each exchange to find the mock server definitions
|
||||
const httpMockFile = "testdata/http.json"
|
||||
|
||||
// MockHTTPInstance takes an existing Exchange instance and attaches it to a new http server
|
||||
// It is expected to be run once, since http requests do not often tangle with each other
|
||||
func MockHTTPInstance(e exchange.IBotExchange) error {
|
||||
serverDetails, newClient, err := mock.NewVCRServer(httpMockFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("mock server error %s", err)
|
||||
}
|
||||
b := e.GetBase()
|
||||
b.SkipAuthCheck = true
|
||||
err = b.SetHTTPClient(newClient)
|
||||
if err != nil {
|
||||
return fmt.Errorf("mock server error %s", err)
|
||||
}
|
||||
endpointMap := b.API.Endpoints.GetURLMap()
|
||||
for k := range endpointMap {
|
||||
err = b.API.Endpoints.SetRunning(k, serverDetails)
|
||||
if err != nil {
|
||||
return fmt.Errorf("mock server error %s", err)
|
||||
}
|
||||
}
|
||||
log.Printf(sharedtestvalues.MockTesting, e.GetName())
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var upgrader = websocket.Upgrader{}
|
||||
|
||||
type wsMockFunc func(msg []byte, w *websocket.Conn) error
|
||||
|
||||
// MockWSInstance creates a new Exchange instance with a mock WS instance and HTTP server
|
||||
// It accepts an exchange package type argument and a mock WS function
|
||||
// It is expected to be run from any WS tests which need a specific response function
|
||||
func MockWSInstance[T any, PT interface {
|
||||
*T
|
||||
exchange.IBotExchange
|
||||
}](tb testing.TB, m wsMockFunc) *T {
|
||||
tb.Helper()
|
||||
|
||||
e := PT(new(T))
|
||||
require.NoError(tb, TestInstance(e), "TestInstance setup should not error")
|
||||
|
||||
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { wsMockWrapper(tb, w, r, m) }))
|
||||
|
||||
b := e.GetBase()
|
||||
b.SkipAuthCheck = true
|
||||
err := b.API.Endpoints.SetRunning("RestSpotURL", s.URL)
|
||||
require.NoError(tb, err, "Endpoints.SetRunning should not error for RestSpotURL")
|
||||
for _, auth := range []bool{true, false} {
|
||||
err = b.Websocket.SetWebsocketURL("ws"+strings.TrimPrefix(s.URL, "http"), auth, true)
|
||||
require.NoErrorf(tb, err, "SetWebsocketURL should not error for auth: %v", auth)
|
||||
}
|
||||
|
||||
b.Features.Subscriptions = []*subscription.Subscription{}
|
||||
err = b.Websocket.Connect()
|
||||
require.NoError(tb, err, "Connect should not error")
|
||||
|
||||
return e
|
||||
}
|
||||
|
||||
// wsMockWrapper handles upgrading an initial HTTP request to WS, and then runs a for loop calling the mock func on each input
|
||||
func wsMockWrapper(tb testing.TB, w http.ResponseWriter, r *http.Request, m wsMockFunc) {
|
||||
tb.Helper()
|
||||
// TODO: This needs to move once this branch includes #1358, probably to use a new mock HTTP instance for kraken
|
||||
if strings.Contains(r.URL.Path, "GetWebSocketsToken") {
|
||||
_, err := w.Write([]byte(`{"result":{"token":"mockAuth"}}`))
|
||||
require.NoError(tb, err, "Write should not error")
|
||||
return
|
||||
}
|
||||
c, err := upgrader.Upgrade(w, r, nil)
|
||||
require.NoError(tb, err, "Upgrade connection should not error")
|
||||
defer c.Close()
|
||||
for {
|
||||
_, p, err := c.ReadMessage()
|
||||
require.NoError(tb, err, "ReadMessage should not error")
|
||||
|
||||
err = m(p, c)
|
||||
assert.NoError(tb, err, "WS Mock Function should not error")
|
||||
}
|
||||
}
|
||||
|
||||
var setupWsMutex sync.Mutex
|
||||
var setupWsOnce = make(map[exchange.IBotExchange]bool)
|
||||
|
||||
// SetupWs is a helper function to connect both auth and normal websockets
|
||||
// It will skip the test if websockets are not enabled
|
||||
// It's up to the test to skip if it requires creds, though
|
||||
func SetupWs(tb testing.TB, e exchange.IBotExchange) {
|
||||
tb.Helper()
|
||||
|
||||
setupWsMutex.Lock()
|
||||
defer setupWsMutex.Unlock()
|
||||
|
||||
if _, ok := setupWsOnce[e]; ok {
|
||||
return
|
||||
}
|
||||
|
||||
b := e.GetBase()
|
||||
if !b.Websocket.IsEnabled() {
|
||||
tb.Skip("Websocket not enabled")
|
||||
}
|
||||
if b.Websocket.IsConnected() {
|
||||
return
|
||||
}
|
||||
err := b.Websocket.Connect()
|
||||
require.NoError(tb, err, "WsConnect should not error")
|
||||
|
||||
setupWsOnce[e] = true
|
||||
}
|
||||
Reference in New Issue
Block a user