Skip to content

Commit

Permalink
Tests: Generic ws mock instances
Browse files Browse the repository at this point in the history
  • Loading branch information
gbjk committed Jan 22, 2024
1 parent 1ee9e64 commit 94da21f
Showing 1 changed file with 159 additions and 0 deletions.
159 changes: 159 additions & 0 deletions internal/testing/exchange/exchange.go
Original file line number Diff line number Diff line change
@@ -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
}

0 comments on commit 94da21f

Please sign in to comment.