Files
dex/pkg/client/trader_client_test.go
T
zeekay 8eadd71bcc fix: resolve infinite loop bug in orderbook insert operations
- Fixed insertBid/insertAsk infinite loop when encountering orphaned order IDs
- Added pos++ before continue to ensure loop termination with stale entries
- Added comprehensive edge case tests for orphaned/nil orders in orderbook
- Added extensive tests for pkg/client handlers and channel accessors
- Added tests for websocket server in pkg/api

Coverage improvements:
- pkg/orderbook: 86.5% → 90.5%
- pkg/client: 47.7% → 64.2%

This fixes a critical bug where orphaned entries in bids/asks lists could
cause infinite loops during order insertion.
2025-12-11 14:57:12 -08:00

1540 lines
35 KiB
Go

package client
import (
"fmt"
"math/big"
"testing"
"time"
"github.com/luxfi/dex/pkg/lx"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestNewTraderClient(t *testing.T) {
config := ClientConfig{
APIEndpoint: "https://api.lux.exchange",
WSEndpoint: "wss://ws.lux.exchange",
APIKey: "test_key",
APISecret: "test_secret",
UserID: "user123",
}
client, err := NewTraderClient(config)
require.NoError(t, err)
assert.NotNil(t, client)
assert.Equal(t, "https://api.lux.exchange", client.apiEndpoint)
assert.Equal(t, "wss://ws.lux.exchange", client.wsEndpoint)
assert.Equal(t, "test_key", client.apiKey)
assert.Equal(t, "test_secret", client.apiSecret)
assert.Equal(t, "user123", client.userID)
assert.False(t, client.connected)
assert.False(t, client.authenticated)
}
func TestNewTraderClientMissingEndpoints(t *testing.T) {
tests := []struct {
name string
config ClientConfig
}{
{
name: "MissingAPIEndpoint",
config: ClientConfig{WSEndpoint: "wss://ws.test.com"},
},
{
name: "MissingWSEndpoint",
config: ClientConfig{APIEndpoint: "https://api.test.com"},
},
{
name: "BothMissing",
config: ClientConfig{},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
client, err := NewTraderClient(tt.config)
assert.Error(t, err)
assert.Nil(t, client)
assert.Contains(t, err.Error(), "endpoints required")
})
}
}
func TestTraderClientInitialState(t *testing.T) {
config := ClientConfig{
APIEndpoint: "https://api.test.com",
WSEndpoint: "wss://ws.test.com",
}
client, err := NewTraderClient(config)
require.NoError(t, err)
// Check maps are initialized
assert.NotNil(t, client.positions)
assert.NotNil(t, client.orders)
assert.NotNil(t, client.balances)
assert.NotNil(t, client.orderBooks)
assert.NotNil(t, client.prices)
assert.NotNil(t, client.trades)
assert.NotNil(t, client.subscriptions)
assert.NotNil(t, client.callbacks)
// Check channels are initialized
assert.NotNil(t, client.orderUpdates)
assert.NotNil(t, client.positionUpdates)
assert.NotNil(t, client.priceUpdates)
assert.NotNil(t, client.tradeUpdates)
assert.NotNil(t, client.errorChan)
// Check context
assert.NotNil(t, client.ctx)
assert.NotNil(t, client.cancel)
}
func TestOrderUpdateStruct(t *testing.T) {
update := OrderUpdate{
Order: nil, // Would be *lx.Order in real use
Status: 0, // Would be lx.OrderStatus
Timestamp: time.Now(),
Message: "Order filled",
}
assert.Equal(t, "Order filled", update.Message)
assert.False(t, update.Timestamp.IsZero())
}
func TestPositionUpdateStruct(t *testing.T) {
update := PositionUpdate{
Position: nil,
Action: "opened",
Timestamp: time.Now(),
}
assert.Equal(t, "opened", update.Action)
assert.False(t, update.Timestamp.IsZero())
}
func TestPriceUpdateStruct(t *testing.T) {
update := PriceUpdate{
Symbol: "BTC/USDC",
Price: 50000.0,
Bid: 49990.0,
Ask: 50010.0,
Volume: 1000.0,
Timestamp: time.Now(),
}
assert.Equal(t, "BTC/USDC", update.Symbol)
assert.Equal(t, 50000.0, update.Price)
assert.Equal(t, 49990.0, update.Bid)
assert.Equal(t, 50010.0, update.Ask)
assert.Equal(t, 1000.0, update.Volume)
}
func TestTradeUpdateStruct(t *testing.T) {
update := TradeUpdate{
Trade: nil,
Symbol: "ETH/USDC",
Side: "buy",
Timestamp: time.Now(),
}
assert.Equal(t, "ETH/USDC", update.Symbol)
assert.Equal(t, "buy", update.Side)
}
func TestClientConfigStruct(t *testing.T) {
config := ClientConfig{
APIEndpoint: "https://api.example.com",
WSEndpoint: "wss://ws.example.com",
APIKey: "key123",
APISecret: "secret456",
UserID: "user789",
}
assert.Equal(t, "https://api.example.com", config.APIEndpoint)
assert.Equal(t, "wss://ws.example.com", config.WSEndpoint)
assert.Equal(t, "key123", config.APIKey)
assert.Equal(t, "secret456", config.APISecret)
assert.Equal(t, "user789", config.UserID)
}
func TestClientChannelBufferSizes(t *testing.T) {
config := ClientConfig{
APIEndpoint: "https://api.test.com",
WSEndpoint: "wss://ws.test.com",
}
client, err := NewTraderClient(config)
require.NoError(t, err)
// Check channel capacities
assert.Equal(t, 100, cap(client.orderUpdates))
assert.Equal(t, 100, cap(client.positionUpdates))
assert.Equal(t, 1000, cap(client.priceUpdates))
assert.Equal(t, 1000, cap(client.tradeUpdates))
assert.Equal(t, 10, cap(client.errorChan))
}
func TestDisconnectNotConnected(t *testing.T) {
config := ClientConfig{
APIEndpoint: "https://api.test.com",
WSEndpoint: "wss://ws.test.com",
}
client, err := NewTraderClient(config)
require.NoError(t, err)
// Disconnect when not connected should be no-op
err = client.Disconnect()
assert.NoError(t, err)
}
func TestConnectAlreadyConnected(t *testing.T) {
config := ClientConfig{
APIEndpoint: "https://api.test.com",
WSEndpoint: "wss://ws.test.com",
}
client, err := NewTraderClient(config)
require.NoError(t, err)
// Manually set connected state
client.mu.Lock()
client.connected = true
client.mu.Unlock()
// Connect when already connected should return error
err = client.Connect()
assert.Error(t, err)
assert.Contains(t, err.Error(), "already connected")
}
func TestPriceUpdateActions(t *testing.T) {
actions := []string{"opened", "modified", "closed", "liquidated"}
for _, action := range actions {
update := PositionUpdate{Action: action}
assert.Equal(t, action, update.Action)
}
}
func TestTradeSides(t *testing.T) {
sides := []string{"buy", "sell"}
for _, side := range sides {
update := TradeUpdate{Side: side}
assert.Equal(t, side, update.Side)
}
}
func TestGetBalanceEmpty(t *testing.T) {
config := ClientConfig{
APIEndpoint: "https://api.test.com",
WSEndpoint: "wss://ws.test.com",
}
client, err := NewTraderClient(config)
require.NoError(t, err)
// Get balance for non-existent asset returns 0
balance, err := client.GetBalance("BTC")
require.NoError(t, err)
assert.Equal(t, int64(0), balance.Int64())
}
func TestGetBalanceExists(t *testing.T) {
config := ClientConfig{
APIEndpoint: "https://api.test.com",
WSEndpoint: "wss://ws.test.com",
}
client, err := NewTraderClient(config)
require.NoError(t, err)
// Set balance directly
client.mu.Lock()
client.balances["ETH"] = big.NewInt(1000000000)
client.mu.Unlock()
balance, err := client.GetBalance("ETH")
require.NoError(t, err)
assert.Equal(t, int64(1000000000), balance.Int64())
}
func TestGetPositionNotFound(t *testing.T) {
config := ClientConfig{
APIEndpoint: "https://api.test.com",
WSEndpoint: "wss://ws.test.com",
}
client, err := NewTraderClient(config)
require.NoError(t, err)
pos, err := client.GetPosition("nonexistent")
assert.Error(t, err)
assert.Nil(t, pos)
assert.Contains(t, err.Error(), "not found")
}
func TestGetPositionExists(t *testing.T) {
config := ClientConfig{
APIEndpoint: "https://api.test.com",
WSEndpoint: "wss://ws.test.com",
}
client, err := NewTraderClient(config)
require.NoError(t, err)
// Add position directly
testPos := &lx.MarginPosition{
ID: "pos123",
Symbol: "BTC/USDC",
Leverage: 10,
}
client.mu.Lock()
client.positions["pos123"] = testPos
client.mu.Unlock()
pos, err := client.GetPosition("pos123")
require.NoError(t, err)
assert.Equal(t, "pos123", pos.ID)
assert.Equal(t, "BTC/USDC", pos.Symbol)
assert.Equal(t, float64(10), pos.Leverage)
}
func TestGetPositionsEmpty(t *testing.T) {
config := ClientConfig{
APIEndpoint: "https://api.test.com",
WSEndpoint: "wss://ws.test.com",
}
client, err := NewTraderClient(config)
require.NoError(t, err)
positions := client.GetPositions()
assert.Empty(t, positions)
}
func TestGetPositionsMultiple(t *testing.T) {
config := ClientConfig{
APIEndpoint: "https://api.test.com",
WSEndpoint: "wss://ws.test.com",
}
client, err := NewTraderClient(config)
require.NoError(t, err)
// Add positions
client.mu.Lock()
client.positions["pos1"] = &lx.MarginPosition{ID: "pos1", Symbol: "BTC/USDC"}
client.positions["pos2"] = &lx.MarginPosition{ID: "pos2", Symbol: "ETH/USDC"}
client.mu.Unlock()
positions := client.GetPositions()
assert.Len(t, positions, 2)
assert.NotNil(t, positions["pos1"])
assert.NotNil(t, positions["pos2"])
}
func TestGetOrdersEmpty(t *testing.T) {
config := ClientConfig{
APIEndpoint: "https://api.test.com",
WSEndpoint: "wss://ws.test.com",
}
client, err := NewTraderClient(config)
require.NoError(t, err)
orders := client.GetOrders()
assert.Empty(t, orders)
}
func TestGetOrdersMultiple(t *testing.T) {
config := ClientConfig{
APIEndpoint: "https://api.test.com",
WSEndpoint: "wss://ws.test.com",
}
client, err := NewTraderClient(config)
require.NoError(t, err)
// Add orders
client.mu.Lock()
client.orders[1] = &lx.Order{ID: 1, Symbol: "BTC/USDC"}
client.orders[2] = &lx.Order{ID: 2, Symbol: "ETH/USDC"}
client.orders[3] = &lx.Order{ID: 3, Symbol: "SOL/USDC"}
client.mu.Unlock()
orders := client.GetOrders()
assert.Len(t, orders, 3)
assert.NotNil(t, orders[1])
assert.NotNil(t, orders[2])
assert.NotNil(t, orders[3])
}
func TestGetPriceNotFound(t *testing.T) {
config := ClientConfig{
APIEndpoint: "https://api.test.com",
WSEndpoint: "wss://ws.test.com",
}
client, err := NewTraderClient(config)
require.NoError(t, err)
price, err := client.GetPrice("BTC/USDC")
assert.Error(t, err)
assert.Equal(t, float64(0), price)
assert.Contains(t, err.Error(), "no price")
}
func TestGetPriceExists(t *testing.T) {
config := ClientConfig{
APIEndpoint: "https://api.test.com",
WSEndpoint: "wss://ws.test.com",
}
client, err := NewTraderClient(config)
require.NoError(t, err)
// Set price directly
client.mu.Lock()
client.prices["BTC/USDC"] = 50000.0
client.mu.Unlock()
price, err := client.GetPrice("BTC/USDC")
require.NoError(t, err)
assert.Equal(t, 50000.0, price)
}
func TestGetOrderBookNotFound(t *testing.T) {
config := ClientConfig{
APIEndpoint: "https://api.test.com",
WSEndpoint: "wss://ws.test.com",
}
client, err := NewTraderClient(config)
require.NoError(t, err)
ob, err := client.GetOrderBook("BTC/USDC")
assert.Error(t, err)
assert.Nil(t, ob)
assert.Contains(t, err.Error(), "no order book")
}
func TestGetOrderBookExists(t *testing.T) {
config := ClientConfig{
APIEndpoint: "https://api.test.com",
WSEndpoint: "wss://ws.test.com",
}
client, err := NewTraderClient(config)
require.NoError(t, err)
// Set order book directly
testOB := &lx.OrderBookSnapshot{
Symbol: "ETH/USDC",
Bids: []lx.OrderLevel{{Price: 3000, Size: 10}},
Asks: []lx.OrderLevel{{Price: 3010, Size: 5}},
}
client.mu.Lock()
client.orderBooks["ETH/USDC"] = testOB
client.mu.Unlock()
ob, err := client.GetOrderBook("ETH/USDC")
require.NoError(t, err)
assert.Equal(t, "ETH/USDC", ob.Symbol)
assert.Len(t, ob.Bids, 1)
assert.Len(t, ob.Asks, 1)
}
func TestPlaceOrderNotAuthenticated(t *testing.T) {
config := ClientConfig{
APIEndpoint: "https://api.test.com",
WSEndpoint: "wss://ws.test.com",
}
client, err := NewTraderClient(config)
require.NoError(t, err)
order := &lx.Order{Symbol: "BTC/USDC", Price: 50000}
result, err := client.PlaceOrder(order)
assert.Error(t, err)
assert.Nil(t, result)
assert.Contains(t, err.Error(), "not authenticated")
}
func TestCancelOrderNotAuthenticated(t *testing.T) {
config := ClientConfig{
APIEndpoint: "https://api.test.com",
WSEndpoint: "wss://ws.test.com",
}
client, err := NewTraderClient(config)
require.NoError(t, err)
err = client.CancelOrder(12345)
assert.Error(t, err)
assert.Contains(t, err.Error(), "not authenticated")
}
func TestModifyOrderNotAuthenticated(t *testing.T) {
config := ClientConfig{
APIEndpoint: "https://api.test.com",
WSEndpoint: "wss://ws.test.com",
}
client, err := NewTraderClient(config)
require.NoError(t, err)
err = client.ModifyOrder(12345, 51000, 2.0)
assert.Error(t, err)
assert.Contains(t, err.Error(), "not authenticated")
}
func TestOpenMarginPositionNotAuthenticated(t *testing.T) {
config := ClientConfig{
APIEndpoint: "https://api.test.com",
WSEndpoint: "wss://ws.test.com",
}
client, err := NewTraderClient(config)
require.NoError(t, err)
pos, err := client.OpenMarginPosition("BTC/USDC", lx.Buy, 1.0, 10.0)
assert.Error(t, err)
assert.Nil(t, pos)
assert.Contains(t, err.Error(), "not authenticated")
}
func TestClosePositionNotAuthenticated(t *testing.T) {
config := ClientConfig{
APIEndpoint: "https://api.test.com",
WSEndpoint: "wss://ws.test.com",
}
client, err := NewTraderClient(config)
require.NoError(t, err)
err = client.ClosePosition("pos123", 1.0)
assert.Error(t, err)
assert.Contains(t, err.Error(), "not authenticated")
}
func TestModifyLeverageNotAuthenticated(t *testing.T) {
config := ClientConfig{
APIEndpoint: "https://api.test.com",
WSEndpoint: "wss://ws.test.com",
}
client, err := NewTraderClient(config)
require.NoError(t, err)
err = client.ModifyLeverage("pos123", 20.0)
assert.Error(t, err)
assert.Contains(t, err.Error(), "not authenticated")
}
func TestDepositToVaultNotAuthenticated(t *testing.T) {
config := ClientConfig{
APIEndpoint: "https://api.test.com",
WSEndpoint: "wss://ws.test.com",
}
client, err := NewTraderClient(config)
require.NoError(t, err)
err = client.DepositToVault("vault1", big.NewInt(1000))
assert.Error(t, err)
assert.Contains(t, err.Error(), "not authenticated")
}
func TestWithdrawFromVaultNotAuthenticated(t *testing.T) {
config := ClientConfig{
APIEndpoint: "https://api.test.com",
WSEndpoint: "wss://ws.test.com",
}
client, err := NewTraderClient(config)
require.NoError(t, err)
err = client.WithdrawFromVault("vault1", big.NewInt(500))
assert.Error(t, err)
assert.Contains(t, err.Error(), "not authenticated")
}
func TestSupplyNotAuthenticated(t *testing.T) {
config := ClientConfig{
APIEndpoint: "https://api.test.com",
WSEndpoint: "wss://ws.test.com",
}
client, err := NewTraderClient(config)
require.NoError(t, err)
err = client.Supply("USDC", big.NewInt(10000))
assert.Error(t, err)
assert.Contains(t, err.Error(), "not authenticated")
}
func TestBorrowNotAuthenticated(t *testing.T) {
config := ClientConfig{
APIEndpoint: "https://api.test.com",
WSEndpoint: "wss://ws.test.com",
}
client, err := NewTraderClient(config)
require.NoError(t, err)
err = client.Borrow("USDC", big.NewInt(5000))
assert.Error(t, err)
assert.Contains(t, err.Error(), "not authenticated")
}
func TestRepayNotAuthenticated(t *testing.T) {
config := ClientConfig{
APIEndpoint: "https://api.test.com",
WSEndpoint: "wss://ws.test.com",
}
client, err := NewTraderClient(config)
require.NoError(t, err)
err = client.Repay("USDC", big.NewInt(2000))
assert.Error(t, err)
assert.Contains(t, err.Error(), "not authenticated")
}
func TestOnOrderUpdateCallback(t *testing.T) {
config := ClientConfig{
APIEndpoint: "https://api.test.com",
WSEndpoint: "wss://ws.test.com",
}
client, err := NewTraderClient(config)
require.NoError(t, err)
var received *OrderUpdate
client.OnOrderUpdate(func(update *OrderUpdate) {
received = update
})
assert.NotNil(t, client.callbacks["order_update"])
// Test callback execution
testUpdate := &OrderUpdate{Message: "test order"}
client.callbacks["order_update"](testUpdate)
assert.Equal(t, "test order", received.Message)
}
func TestOnPositionUpdateCallback(t *testing.T) {
config := ClientConfig{
APIEndpoint: "https://api.test.com",
WSEndpoint: "wss://ws.test.com",
}
client, err := NewTraderClient(config)
require.NoError(t, err)
var received *PositionUpdate
client.OnPositionUpdate(func(update *PositionUpdate) {
received = update
})
assert.NotNil(t, client.callbacks["position_update"])
// Test callback execution
testUpdate := &PositionUpdate{Action: "opened"}
client.callbacks["position_update"](testUpdate)
assert.Equal(t, "opened", received.Action)
}
func TestOnPriceUpdateCallback(t *testing.T) {
config := ClientConfig{
APIEndpoint: "https://api.test.com",
WSEndpoint: "wss://ws.test.com",
}
client, err := NewTraderClient(config)
require.NoError(t, err)
var received *PriceUpdate
client.OnPriceUpdate(func(update *PriceUpdate) {
received = update
})
assert.NotNil(t, client.callbacks["price_update"])
// Test callback execution
testUpdate := &PriceUpdate{Symbol: "BTC/USDC", Price: 50000}
client.callbacks["price_update"](testUpdate)
assert.Equal(t, "BTC/USDC", received.Symbol)
assert.Equal(t, float64(50000), received.Price)
}
func TestOnTradeUpdateCallback(t *testing.T) {
config := ClientConfig{
APIEndpoint: "https://api.test.com",
WSEndpoint: "wss://ws.test.com",
}
client, err := NewTraderClient(config)
require.NoError(t, err)
var received *TradeUpdate
client.OnTradeUpdate(func(update *TradeUpdate) {
received = update
})
assert.NotNil(t, client.callbacks["trade_update"])
// Test callback execution
testUpdate := &TradeUpdate{Symbol: "ETH/USDC", Side: "buy"}
client.callbacks["trade_update"](testUpdate)
assert.Equal(t, "ETH/USDC", received.Symbol)
assert.Equal(t, "buy", received.Side)
}
func TestProcessMessageOrderUpdate(t *testing.T) {
config := ClientConfig{
APIEndpoint: "https://api.test.com",
WSEndpoint: "wss://ws.test.com",
}
client, err := NewTraderClient(config)
require.NoError(t, err)
msg := map[string]interface{}{
"type": "order_update",
"order": map[string]interface{}{
"id": float64(123),
"symbol": "BTC/USDC",
},
}
client.processMessage(msg)
// Check if order was stored
client.mu.RLock()
_, exists := client.orders[123]
client.mu.RUnlock()
assert.True(t, exists)
}
func TestProcessMessagePositionUpdate(t *testing.T) {
config := ClientConfig{
APIEndpoint: "https://api.test.com",
WSEndpoint: "wss://ws.test.com",
}
client, err := NewTraderClient(config)
require.NoError(t, err)
msg := map[string]interface{}{
"type": "position_update",
"action": "opened",
"position": map[string]interface{}{
"id": "pos456",
"symbol": "ETH/USDC",
},
}
client.processMessage(msg)
// Check if position was stored
client.mu.RLock()
_, exists := client.positions["pos456"]
client.mu.RUnlock()
assert.True(t, exists)
}
func TestProcessMessagePositionClosed(t *testing.T) {
config := ClientConfig{
APIEndpoint: "https://api.test.com",
WSEndpoint: "wss://ws.test.com",
}
client, err := NewTraderClient(config)
require.NoError(t, err)
// First add a position
client.mu.Lock()
client.positions["pos789"] = &lx.MarginPosition{ID: "pos789"}
client.mu.Unlock()
msg := map[string]interface{}{
"type": "position_update",
"action": "closed",
"position": map[string]interface{}{
"id": "pos789",
},
}
client.processMessage(msg)
// Check if position was removed
client.mu.RLock()
_, exists := client.positions["pos789"]
client.mu.RUnlock()
assert.False(t, exists)
}
func TestProcessMessagePriceUpdate(t *testing.T) {
config := ClientConfig{
APIEndpoint: "https://api.test.com",
WSEndpoint: "wss://ws.test.com",
}
client, err := NewTraderClient(config)
require.NoError(t, err)
msg := map[string]interface{}{
"type": "price_update",
"symbol": "SOL/USDC",
"price": 150.5,
}
client.processMessage(msg)
// Check if price was stored
client.mu.RLock()
price := client.prices["SOL/USDC"]
client.mu.RUnlock()
assert.Equal(t, 150.5, price)
}
func TestProcessMessageBalanceUpdate(t *testing.T) {
config := ClientConfig{
APIEndpoint: "https://api.test.com",
WSEndpoint: "wss://ws.test.com",
}
client, err := NewTraderClient(config)
require.NoError(t, err)
// handleBalanceUpdate expects "balances" map with string values
msg := map[string]interface{}{
"type": "balance_update",
"balances": map[string]interface{}{
"USDC": "1000000000000",
"ETH": "5000000000000000000",
},
}
client.processMessage(msg)
// Check if balances were stored
client.mu.RLock()
usdcBalance := client.balances["USDC"]
ethBalance := client.balances["ETH"]
client.mu.RUnlock()
assert.NotNil(t, usdcBalance)
assert.NotNil(t, ethBalance)
assert.Equal(t, "1000000000000", usdcBalance.String())
assert.Equal(t, "5000000000000000000", ethBalance.String())
}
func TestProcessMessageInvalidType(t *testing.T) {
config := ClientConfig{
APIEndpoint: "https://api.test.com",
WSEndpoint: "wss://ws.test.com",
}
client, err := NewTraderClient(config)
require.NoError(t, err)
msg := map[string]interface{}{
"invalid": "no type field",
}
// Should not panic
client.processMessage(msg)
}
func TestProcessMessageUnknownType(t *testing.T) {
config := ClientConfig{
APIEndpoint: "https://api.test.com",
WSEndpoint: "wss://ws.test.com",
}
client, err := NewTraderClient(config)
require.NoError(t, err)
msg := map[string]interface{}{
"type": "unknown_type",
}
// Should not panic
client.processMessage(msg)
}
func TestConcurrentMapAccess(t *testing.T) {
config := ClientConfig{
APIEndpoint: "https://api.test.com",
WSEndpoint: "wss://ws.test.com",
}
client, err := NewTraderClient(config)
require.NoError(t, err)
done := make(chan bool, 100)
// Concurrent writes
for i := 0; i < 50; i++ {
go func(idx int) {
client.mu.Lock()
client.prices[fmt.Sprintf("PAIR%d/USDC", idx)] = float64(idx * 100)
client.mu.Unlock()
done <- true
}(i)
}
// Concurrent reads
for i := 0; i < 50; i++ {
go func() {
_, _ = client.GetPrice("BTC/USDC")
done <- true
}()
}
// Wait for all
for i := 0; i < 100; i++ {
<-done
}
// Should have 50 prices
client.mu.RLock()
count := len(client.prices)
client.mu.RUnlock()
assert.Equal(t, 50, count)
}
func BenchmarkGetBalance(b *testing.B) {
config := ClientConfig{
APIEndpoint: "https://api.test.com",
WSEndpoint: "wss://ws.test.com",
}
client, _ := NewTraderClient(config)
client.balances["BTC"] = big.NewInt(1000000000)
b.ResetTimer()
for i := 0; i < b.N; i++ {
client.GetBalance("BTC")
}
}
func BenchmarkGetPrice(b *testing.B) {
config := ClientConfig{
APIEndpoint: "https://api.test.com",
WSEndpoint: "wss://ws.test.com",
}
client, _ := NewTraderClient(config)
client.prices["BTC/USDC"] = 50000.0
b.ResetTimer()
for i := 0; i < b.N; i++ {
client.GetPrice("BTC/USDC")
}
}
func BenchmarkProcessMessage(b *testing.B) {
config := ClientConfig{
APIEndpoint: "https://api.test.com",
WSEndpoint: "wss://ws.test.com",
}
client, _ := NewTraderClient(config)
msg := map[string]interface{}{
"type": "price_update",
"symbol": "BTC/USDC",
"price": 50000.0,
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
client.processMessage(msg)
}
}
// Test handleTradeUpdate
func TestHandleTradeUpdate(t *testing.T) {
config := ClientConfig{
APIEndpoint: "https://api.test.com",
WSEndpoint: "wss://ws.test.com",
}
client, err := NewTraderClient(config)
require.NoError(t, err)
msg := map[string]interface{}{
"type": "trade_update",
"symbol": "BTC/USDC",
"trade": map[string]interface{}{
"id": "trade123",
"price": 50000.0,
"quantity": 1.5,
},
}
client.handleTradeUpdate(msg)
// Check if trade was stored
client.mu.RLock()
trades := client.trades["BTC/USDC"]
client.mu.RUnlock()
assert.Len(t, trades, 1)
}
// Test handleTradeUpdate without symbol
func TestHandleTradeUpdateNoSymbol(t *testing.T) {
config := ClientConfig{
APIEndpoint: "https://api.test.com",
WSEndpoint: "wss://ws.test.com",
}
client, err := NewTraderClient(config)
require.NoError(t, err)
msg := map[string]interface{}{
"type": "trade_update",
"trade": map[string]interface{}{
"id": "trade123",
"price": 50000.0,
"quantity": 1.5,
},
}
// Should not panic
client.handleTradeUpdate(msg)
}
// Test handleTradeUpdate with trade history limit
func TestHandleTradeUpdateHistoryLimit(t *testing.T) {
config := ClientConfig{
APIEndpoint: "https://api.test.com",
WSEndpoint: "wss://ws.test.com",
}
client, err := NewTraderClient(config)
require.NoError(t, err)
// Add more than 100 trades
for i := 0; i < 105; i++ {
msg := map[string]interface{}{
"type": "trade_update",
"symbol": "BTC/USDC",
"trade": map[string]interface{}{
"id": fmt.Sprintf("trade%d", i),
"price": float64(50000 + i),
},
}
client.handleTradeUpdate(msg)
}
// Check trade history is limited to 100
client.mu.RLock()
trades := client.trades["BTC/USDC"]
client.mu.RUnlock()
assert.LessOrEqual(t, len(trades), 100)
}
// Test handleOrderBookUpdate
func TestHandleOrderBookUpdate(t *testing.T) {
config := ClientConfig{
APIEndpoint: "https://api.test.com",
WSEndpoint: "wss://ws.test.com",
}
client, err := NewTraderClient(config)
require.NoError(t, err)
msg := map[string]interface{}{
"type": "orderbook_update",
"symbol": "ETH/USDC",
"snapshot": map[string]interface{}{
"symbol": "ETH/USDC",
"bids": []interface{}{},
"asks": []interface{}{},
},
}
client.handleOrderBookUpdate(msg)
// Check if order book was stored
client.mu.RLock()
ob := client.orderBooks["ETH/USDC"]
client.mu.RUnlock()
assert.NotNil(t, ob)
}
// Test handleOrderBookUpdate missing symbol
func TestHandleOrderBookUpdateNoSymbol(t *testing.T) {
config := ClientConfig{
APIEndpoint: "https://api.test.com",
WSEndpoint: "wss://ws.test.com",
}
client, err := NewTraderClient(config)
require.NoError(t, err)
msg := map[string]interface{}{
"type": "orderbook_update",
"snapshot": map[string]interface{}{
"bids": []interface{}{},
},
}
// Should not panic
client.handleOrderBookUpdate(msg)
}
// Test handleError
func TestHandleError(t *testing.T) {
config := ClientConfig{
APIEndpoint: "https://api.test.com",
WSEndpoint: "wss://ws.test.com",
}
client, err := NewTraderClient(config)
require.NoError(t, err)
msg := map[string]interface{}{
"type": "error",
"error": "test error message",
}
client.handleError(msg)
// Check error was sent to channel
select {
case err := <-client.errorChan:
assert.Contains(t, err.Error(), "test error message")
default:
t.Fatal("expected error in channel")
}
}
// Test handleError no error field
func TestHandleErrorNoField(t *testing.T) {
config := ClientConfig{
APIEndpoint: "https://api.test.com",
WSEndpoint: "wss://ws.test.com",
}
client, err := NewTraderClient(config)
require.NoError(t, err)
msg := map[string]interface{}{
"type": "error",
}
// Should not panic
client.handleError(msg)
}
// Test GetOrderUpdates channel access
func TestGetOrderUpdates(t *testing.T) {
config := ClientConfig{
APIEndpoint: "https://api.test.com",
WSEndpoint: "wss://ws.test.com",
}
client, err := NewTraderClient(config)
require.NoError(t, err)
ch := client.GetOrderUpdates()
assert.NotNil(t, ch)
// Send an update and verify
go func() {
client.orderUpdates <- &OrderUpdate{Message: "test"}
}()
select {
case update := <-ch:
assert.Equal(t, "test", update.Message)
case <-time.After(time.Second):
t.Fatal("timeout waiting for order update")
}
}
// Test GetPositionUpdates channel access
func TestGetPositionUpdates(t *testing.T) {
config := ClientConfig{
APIEndpoint: "https://api.test.com",
WSEndpoint: "wss://ws.test.com",
}
client, err := NewTraderClient(config)
require.NoError(t, err)
ch := client.GetPositionUpdates()
assert.NotNil(t, ch)
// Send an update and verify
go func() {
client.positionUpdates <- &PositionUpdate{Action: "opened"}
}()
select {
case update := <-ch:
assert.Equal(t, "opened", update.Action)
case <-time.After(time.Second):
t.Fatal("timeout waiting for position update")
}
}
// Test GetPriceUpdates channel access
func TestGetPriceUpdates(t *testing.T) {
config := ClientConfig{
APIEndpoint: "https://api.test.com",
WSEndpoint: "wss://ws.test.com",
}
client, err := NewTraderClient(config)
require.NoError(t, err)
ch := client.GetPriceUpdates()
assert.NotNil(t, ch)
// Send an update and verify
go func() {
client.priceUpdates <- &PriceUpdate{Symbol: "BTC/USDC", Price: 50000}
}()
select {
case update := <-ch:
assert.Equal(t, "BTC/USDC", update.Symbol)
assert.Equal(t, float64(50000), update.Price)
case <-time.After(time.Second):
t.Fatal("timeout waiting for price update")
}
}
// Test GetTradeUpdates channel access
func TestGetTradeUpdates(t *testing.T) {
config := ClientConfig{
APIEndpoint: "https://api.test.com",
WSEndpoint: "wss://ws.test.com",
}
client, err := NewTraderClient(config)
require.NoError(t, err)
ch := client.GetTradeUpdates()
assert.NotNil(t, ch)
// Send an update and verify
go func() {
client.tradeUpdates <- &TradeUpdate{Symbol: "ETH/USDC", Side: "buy"}
}()
select {
case update := <-ch:
assert.Equal(t, "ETH/USDC", update.Symbol)
assert.Equal(t, "buy", update.Side)
case <-time.After(time.Second):
t.Fatal("timeout waiting for trade update")
}
}
// Test GetErrors channel access
func TestGetErrors(t *testing.T) {
config := ClientConfig{
APIEndpoint: "https://api.test.com",
WSEndpoint: "wss://ws.test.com",
}
client, err := NewTraderClient(config)
require.NoError(t, err)
ch := client.GetErrors()
assert.NotNil(t, ch)
// Send an error and verify
go func() {
client.errorChan <- fmt.Errorf("test error")
}()
select {
case err := <-ch:
assert.Contains(t, err.Error(), "test error")
case <-time.After(time.Second):
t.Fatal("timeout waiting for error")
}
}
// Test processMessage with trade_update type
func TestProcessMessageTradeUpdate(t *testing.T) {
config := ClientConfig{
APIEndpoint: "https://api.test.com",
WSEndpoint: "wss://ws.test.com",
}
client, err := NewTraderClient(config)
require.NoError(t, err)
msg := map[string]interface{}{
"type": "trade_update",
"symbol": "BTC/USDC",
"trade": map[string]interface{}{
"id": "trade1",
"price": 51000.0,
},
}
client.processMessage(msg)
// Check if trade was stored
client.mu.RLock()
trades := client.trades["BTC/USDC"]
client.mu.RUnlock()
assert.Len(t, trades, 1)
}
// Test processMessage with orderbook_update type
func TestProcessMessageOrderbookUpdate(t *testing.T) {
config := ClientConfig{
APIEndpoint: "https://api.test.com",
WSEndpoint: "wss://ws.test.com",
}
client, err := NewTraderClient(config)
require.NoError(t, err)
msg := map[string]interface{}{
"type": "orderbook_update",
"symbol": "SOL/USDC",
"snapshot": map[string]interface{}{
"symbol": "SOL/USDC",
"bids": []interface{}{},
"asks": []interface{}{},
},
}
client.processMessage(msg)
// Check if orderbook was stored
client.mu.RLock()
ob := client.orderBooks["SOL/USDC"]
client.mu.RUnlock()
assert.NotNil(t, ob)
}
// Test processMessage with error type
func TestProcessMessageError(t *testing.T) {
config := ClientConfig{
APIEndpoint: "https://api.test.com",
WSEndpoint: "wss://ws.test.com",
}
client, err := NewTraderClient(config)
require.NoError(t, err)
msg := map[string]interface{}{
"type": "error",
"error": "some server error",
}
client.processMessage(msg)
// Check error was sent to channel
select {
case err := <-client.errorChan:
assert.Contains(t, err.Error(), "some server error")
default:
t.Fatal("expected error in channel")
}
}
// Test handlePriceUpdate with bid/ask
func TestHandlePriceUpdateWithBidAsk(t *testing.T) {
config := ClientConfig{
APIEndpoint: "https://api.test.com",
WSEndpoint: "wss://ws.test.com",
}
client, err := NewTraderClient(config)
require.NoError(t, err)
msg := map[string]interface{}{
"type": "price_update",
"symbol": "BTC/USDC",
"price": 50000.0,
"bid": 49990.0,
"ask": 50010.0,
"volume": 1000.0,
}
client.handlePriceUpdate(msg)
// Check if price was stored
client.mu.RLock()
price := client.prices["BTC/USDC"]
client.mu.RUnlock()
assert.Equal(t, 50000.0, price)
}
// Test handlePriceUpdate callback
func TestHandlePriceUpdateWithCallback(t *testing.T) {
config := ClientConfig{
APIEndpoint: "https://api.test.com",
WSEndpoint: "wss://ws.test.com",
}
client, err := NewTraderClient(config)
require.NoError(t, err)
var received *PriceUpdate
client.OnPriceUpdate(func(update *PriceUpdate) {
received = update
})
msg := map[string]interface{}{
"type": "price_update",
"symbol": "ETH/USDC",
"price": 3000.0,
"bid": 2999.0,
"ask": 3001.0,
}
client.handlePriceUpdate(msg)
// Check callback was called
require.NotNil(t, received)
assert.Equal(t, "ETH/USDC", received.Symbol)
assert.Equal(t, 3000.0, received.Price)
assert.Equal(t, 2999.0, received.Bid)
assert.Equal(t, 3001.0, received.Ask)
}
// Test handleOrderUpdate with callback
func TestHandleOrderUpdateWithCallback(t *testing.T) {
config := ClientConfig{
APIEndpoint: "https://api.test.com",
WSEndpoint: "wss://ws.test.com",
}
client, err := NewTraderClient(config)
require.NoError(t, err)
var received *OrderUpdate
client.OnOrderUpdate(func(update *OrderUpdate) {
received = update
})
msg := map[string]interface{}{
"type": "order_update",
"status": float64(1),
"order": map[string]interface{}{
"id": float64(456),
"symbol": "BTC/USDC",
"price": 51000.0,
"quantity": 1.0,
},
}
client.handleOrderUpdate(msg)
// Check callback was called
require.NotNil(t, received)
assert.NotNil(t, received.Order)
}
// Test handlePositionUpdate with callback
func TestHandlePositionUpdateWithCallback(t *testing.T) {
config := ClientConfig{
APIEndpoint: "https://api.test.com",
WSEndpoint: "wss://ws.test.com",
}
client, err := NewTraderClient(config)
require.NoError(t, err)
var received *PositionUpdate
client.OnPositionUpdate(func(update *PositionUpdate) {
received = update
})
msg := map[string]interface{}{
"type": "position_update",
"action": "modified",
"position": map[string]interface{}{
"id": "pos999",
"symbol": "BTC/USDC",
"leverage": 5.0,
},
}
client.handlePositionUpdate(msg)
// Check callback was called
require.NotNil(t, received)
assert.Equal(t, "modified", received.Action)
}
// Test handleTradeUpdate with callback
func TestHandleTradeUpdateWithCallback(t *testing.T) {
config := ClientConfig{
APIEndpoint: "https://api.test.com",
WSEndpoint: "wss://ws.test.com",
}
client, err := NewTraderClient(config)
require.NoError(t, err)
var received *TradeUpdate
client.OnTradeUpdate(func(update *TradeUpdate) {
received = update
})
msg := map[string]interface{}{
"type": "trade_update",
"symbol": "SOL/USDC",
"trade": map[string]interface{}{
"id": "tradeXYZ",
"price": 150.0,
"quantity": 10.0,
},
}
client.handleTradeUpdate(msg)
// Check callback was called
require.NotNil(t, received)
assert.Equal(t, "SOL/USDC", received.Symbol)
}
// Test position update with liquidated action
func TestHandlePositionUpdateLiquidated(t *testing.T) {
config := ClientConfig{
APIEndpoint: "https://api.test.com",
WSEndpoint: "wss://ws.test.com",
}
client, err := NewTraderClient(config)
require.NoError(t, err)
// Add a position first
client.mu.Lock()
client.positions["liqPos"] = &lx.MarginPosition{ID: "liqPos"}
client.mu.Unlock()
msg := map[string]interface{}{
"type": "position_update",
"action": "liquidated",
"position": map[string]interface{}{
"id": "liqPos",
},
}
client.handlePositionUpdate(msg)
// Check if position was removed (liquidated = removed)
client.mu.RLock()
_, exists := client.positions["liqPos"]
client.mu.RUnlock()
assert.False(t, exists)
}