Files
dex/pkg/client/trader_client_mock_test.go
zeekayandHanzo Dev 10a084c0b2 refactor(dex): fold pkg/lx into pkg/dex — one matcher, one package
The matching engine (pkg/lx, 45.5K LOC incl. tests) and the deterministic
core layered on it (pkg/dex) are one matcher spread across two packages
(dex imported lx in 12 files; lx imported neither). Fold lx into dex and
drop the "lx" name entirely — pkg/dex is now the single matcher package.

Pure rename/fold, ZERO logic change. Behavior proven identical: the merged
pkg/dex runs 457 top-level tests (431 former-lx + 26 former-dex) and 489
test funcs (468 + 21), all green — matching, batch (BatchMatchEqualsSequential,
run-to-run determinism, tile boundary), GPU/CPU parity, truncation, and CL
swap math all pass unchanged.

What moved:
- git mv all 101 pkg/lx/*.go + 1 .c into pkg/dex/ (package lx -> package dex).
- lx's swap_test.go -> swap_math_test.go (only filename collision with dex's
  existing swap_test.go).
- Rewrote imports repo-wide: 54 pkg/lx import lines removed; 42 external
  importers (api, client, consensus, dchain, dpdk, lxgpu, marketdata, test/,
  tests/) repointed to pkg/dex with lx.X -> dex.X; the 12 in-package dex files
  had lx.X -> bare X. 717 selector lines rewritten. Stale pkg/lx comment refs
  updated (pkg/lxgpu left intact).

Collisions resolved (2, both package-level vars):
- ErrNoLiquidity: lx's pool-level error (dexError, used only in swap.go, never
  referenced via qualifier) renamed ErrPoolNoLiquidity to match the existing
  ErrPool* convention; dex's router-level ErrNoLiquidity (public, has errors.Is
  tests) kept.
- priceMultiplierBig: identical after fold (both big.NewInt(PriceMultiplier));
  deduped by deleting dex's copy.

Not folded — pkg/orderbook: it is NOT the GPU seam (that is orderbook_gpu.go,
which lives in the engine and folded in here). pkg/orderbook is a zero-importer,
placeholder-cgo island whose 13 core domain identifiers (Order, OrderBook, Trade,
OrderType, OrderStatus, PriceLevel, Buy, Sell, Market, Limit, Filled,
PartiallyFilled, NewOrderBook) collide incompatibly with the engine's. Folding
it verbatim breaks the build; folding via rename reintroduces the exact parallel
duplication this fold removes. Left for owner decision (recommend deletion as
dead code) — see refactor notes.

Follow-on (separate repos, not touched): 19 files under luxfi/dex import
pkg/lx externally — lux/precompile/dex, lux/precompile-dex/dex,
lux/consensus/examples/01-simple-bridge, lux/_consensus-v12537/examples,
lux/cex/pkg/engine — repoint to pkg/dex after this lands.

Verified: CGO_ENABLED=0 GOWORK=off go build ./... clean; go vet ./... clean;
go test ./... -count=1 all green (22 ok / 0 fail).

Co-authored-by: Hanzo Dev <[email protected]>
2026-07-02 00:57:08 -07:00

1703 lines
41 KiB
Go

package client
import (
"math/big"
"net/http"
"net/http/httptest"
"strings"
"sync"
"testing"
"time"
"github.com/gorilla/websocket"
"github.com/luxfi/dex/pkg/dex"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// Helper to create a test client with proper configuration
func newTestClient(t *testing.T) *TraderClient {
t.Helper()
client, err := NewTraderClient(ClientConfig{
APIEndpoint: "http://localhost:8080",
WSEndpoint: "ws://localhost:8080/ws",
APIKey: "test-api-key",
APISecret: "test-api-secret",
UserID: "test-user",
})
require.NoError(t, err)
return client
}
// TestMockConnect tests connect error with invalid URL
func TestMockConnect(t *testing.T) {
client, err := NewTraderClient(ClientConfig{
APIEndpoint: "http://localhost:8080",
WSEndpoint: "ws://invalid-url-that-will-fail:99999/ws",
APIKey: "key",
APISecret: "secret",
UserID: "user",
})
require.NoError(t, err)
err = client.Connect()
assert.Error(t, err)
assert.Contains(t, err.Error(), "websocket connection failed")
}
// TestMockSubscribeRequiresConnection tests subscribe requires connection
// Note: The current implementation doesn't check connected status
// and will panic on nil wsConn. This test documents that behavior.
func TestMockSubscribeRequiresConnection(t *testing.T) {
client := newTestClient(t)
// Without wsConn, Subscribe will panic
// This is actually a bug in the implementation
// For now we just verify the client is created correctly
assert.NotNil(t, client)
assert.False(t, client.connected)
}
// TestMockUnsubscribeRequiresConnection tests unsubscribe requires connection
// Note: Same as Subscribe - current implementation has a bug
func TestMockUnsubscribeRequiresConnection(t *testing.T) {
client := newTestClient(t)
assert.NotNil(t, client)
assert.False(t, client.connected)
}
// TestMockPlaceOrderValidation tests place order validation
func TestMockPlaceOrderValidation(t *testing.T) {
client := newTestClient(t)
// Not authenticated
_, err := client.PlaceOrder(&dex.Order{
Symbol: "BTC/USD",
Side: dex.Buy,
})
assert.Error(t, err)
}
// TestMockCancelOrderValidation tests cancel order validation
func TestMockCancelOrderValidation(t *testing.T) {
client := newTestClient(t)
err := client.CancelOrder(12345)
assert.Error(t, err)
}
// TestMockModifyOrderValidation tests modify order validation
func TestMockModifyOrderValidation(t *testing.T) {
client := newTestClient(t)
err := client.ModifyOrder(12345, 50000.0, 1.0)
assert.Error(t, err)
}
// TestMockOpenMarginPositionValidation tests margin position validation
func TestMockOpenMarginPositionValidation(t *testing.T) {
client := newTestClient(t)
_, err := client.OpenMarginPosition("BTC/USD", dex.Buy, 1.0, 10)
assert.Error(t, err)
}
// TestMockClosePositionValidation tests close position validation
func TestMockClosePositionValidation(t *testing.T) {
client := newTestClient(t)
err := client.ClosePosition("pos-1", 1.0)
assert.Error(t, err)
}
// TestMockModifyLeverageValidation tests modify leverage validation
func TestMockModifyLeverageValidation(t *testing.T) {
client := newTestClient(t)
err := client.ModifyLeverage("pos-1", 20)
assert.Error(t, err)
}
// TestMockDepositToVaultValidation tests vault deposit validation
func TestMockDepositToVaultValidation(t *testing.T) {
client := newTestClient(t)
err := client.DepositToVault("vault-1", big.NewInt(1000))
assert.Error(t, err)
}
// TestMockWithdrawFromVaultValidation tests vault withdrawal validation
func TestMockWithdrawFromVaultValidation(t *testing.T) {
client := newTestClient(t)
err := client.WithdrawFromVault("vault-1", big.NewInt(500))
assert.Error(t, err)
}
// TestMockSupplyValidation tests supply validation
func TestMockSupplyValidation(t *testing.T) {
client := newTestClient(t)
err := client.Supply("pool-1", big.NewInt(1000))
assert.Error(t, err)
}
// TestMockBorrowValidation tests borrow validation
func TestMockBorrowValidation(t *testing.T) {
client := newTestClient(t)
err := client.Borrow("pool-1", big.NewInt(500))
assert.Error(t, err)
}
// TestMockRepayValidation tests repay validation
func TestMockRepayValidation(t *testing.T) {
client := newTestClient(t)
err := client.Repay("loan-1", big.NewInt(200))
assert.Error(t, err)
}
// TestMockProcessMessageTypes tests all processMessage cases
func TestMockProcessMessageTypes(t *testing.T) {
client := newTestClient(t)
// Test each message type through processMessage
testCases := []struct {
name string
msgType string
}{
{"order_update", "order_update"},
{"position_update", "position_update"},
{"price_update", "price_update"},
{"trade_update", "trade_update"},
{"orderbook_update", "orderbook_update"},
{"balance_update", "balance_update"},
{"error", "error"},
{"unknown", "unknown_type"},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
// Should not panic on any message type
client.processMessage(map[string]interface{}{
"type": tc.msgType,
})
})
}
}
// TestMockOrderUpdateParsing tests detailed order update parsing
func TestMockOrderUpdateParsing(t *testing.T) {
client := newTestClient(t)
msg := map[string]interface{}{
"type": "order_update",
"order": map[string]interface{}{
"id": uint64(123456),
"symbol": "BTC/USD",
"side": "buy",
"price": "50000.00",
"size": "1.5",
"status": "filled",
},
}
// Drain channel in background
go func() {
select {
case <-client.orderUpdates:
case <-time.After(time.Second):
}
}()
client.handleOrderUpdate(msg)
}
// TestMockPositionUpdateParsing tests detailed position update parsing
func TestMockPositionUpdateParsing(t *testing.T) {
client := newTestClient(t)
msg := map[string]interface{}{
"type": "position_update",
"action": "opened",
"position": map[string]interface{}{
"id": "pos-789",
"symbol": "ETH/USD",
"side": "short",
"size": 10.0,
"leverage": 25,
"entryPrice": 3000.0,
"liquidationPrice": 3300.0,
"unrealizedPnl": -50.0,
},
}
go func() {
select {
case <-client.positionUpdates:
case <-time.After(time.Second):
}
}()
client.handlePositionUpdate(msg)
}
// TestMockPriceUpdateParsing tests detailed price update parsing
func TestMockPriceUpdateParsing(t *testing.T) {
client := newTestClient(t)
msg := map[string]interface{}{
"type": "price_update",
"symbol": "SOL/USD",
"price": 100.50,
"bid": 100.45,
"ask": 100.55,
"volume24h": 1000000.0,
"change24h": 5.5,
}
go func() {
select {
case <-client.priceUpdates:
case <-time.After(time.Second):
}
}()
client.handlePriceUpdate(msg)
}
// TestMockHandleTradeUpdateParsing tests detailed trade update parsing
func TestMockHandleTradeUpdateParsing(t *testing.T) {
client := newTestClient(t)
var receivedUpdate *TradeUpdate
client.OnTradeUpdate(func(update *TradeUpdate) {
receivedUpdate = update
})
// The handler expects "trade" field with nested data
msg := map[string]interface{}{
"type": "trade_update",
"symbol": "AVAX/USD",
"trade": map[string]interface{}{
"id": "trade-abc",
"side": "sell",
"price": 35.75,
"size": 100.0,
},
}
client.handleTradeUpdate(msg)
require.NotNil(t, receivedUpdate)
assert.Equal(t, "AVAX/USD", receivedUpdate.Symbol)
}
// TestMockHandleOrderBookUpdateParsing tests detailed orderbook update parsing
func TestMockHandleOrderBookUpdateParsing(t *testing.T) {
client := newTestClient(t)
// The handler expects "snapshot" field with nested data
msg := map[string]interface{}{
"type": "orderbook_update",
"symbol": "LINK/USD",
"snapshot": map[string]interface{}{
"bids": []interface{}{
[]interface{}{15.00, 1000.0},
[]interface{}{14.99, 2000.0},
},
"asks": []interface{}{
[]interface{}{15.01, 500.0},
[]interface{}{15.02, 1500.0},
},
},
}
client.handleOrderBookUpdate(msg)
// Orderbook should be stored
ob, exists := client.orderBooks["LINK/USD"]
require.True(t, exists)
require.NotNil(t, ob)
}
// TestMockHandleBalanceUpdateParsing tests detailed balance update parsing
func TestMockHandleBalanceUpdateParsing(t *testing.T) {
client := newTestClient(t)
// The handler expects "balances" field with map of asset->balance
msg := map[string]interface{}{
"type": "balance_update",
"balances": map[string]interface{}{
"USDC": "50000000000",
"ETH": "5000000000000000000",
},
}
client.handleBalanceUpdate(msg)
// Balance should be stored
bal, exists := client.balances["USDC"]
require.True(t, exists)
require.NotNil(t, bal)
}
// TestMockHandleErrorParsing tests error message handling
func TestMockHandleErrorParsing(t *testing.T) {
client := newTestClient(t)
msg := map[string]interface{}{
"type": "error",
"code": "ERR_001",
"message": "Order rejected: insufficient balance",
}
// Should not block even with no reader
client.handleError(msg)
// Try to read the error
select {
case err := <-client.errorChan:
assert.Contains(t, err.Error(), "Order rejected")
case <-time.After(100 * time.Millisecond):
// May have been dropped
}
}
// TestMockChannelBufferBehavior tests channel behavior when full
func TestMockChannelBufferBehavior(t *testing.T) {
client := newTestClient(t)
// Fill order updates channel
for i := 0; i < 100; i++ {
select {
case client.orderUpdates <- &OrderUpdate{}:
default:
break
}
}
// Should not block
done := make(chan bool, 1)
go func() {
msg := map[string]interface{}{
"type": "order_update",
"order": map[string]interface{}{"id": uint64(999)},
}
client.handleOrderUpdate(msg)
done <- true
}()
select {
case <-done:
// Good
case <-time.After(time.Second):
t.Error("handleOrderUpdate blocked on full channel")
}
}
// TestMockPositionUpdateActions tests various position actions
func TestMockPositionUpdateActions(t *testing.T) {
client := newTestClient(t)
actions := []string{"opened", "modified", "closed", "liquidated"}
for _, action := range actions {
t.Run(action, func(t *testing.T) {
// Add position for closed/liquidated tests
if action == "closed" || action == "liquidated" {
client.positions["pos-action"] = &dex.MarginPosition{ID: "pos-action"}
}
msg := map[string]interface{}{
"type": "position_update",
"action": action,
"position": map[string]interface{}{
"id": "pos-action",
"symbol": "BTC/USD",
},
}
go func() {
select {
case <-client.positionUpdates:
case <-time.After(100 * time.Millisecond):
}
}()
client.handlePositionUpdate(msg)
})
}
}
// TestMockConcurrentMessageHandling tests concurrent message handling
func TestMockConcurrentMessageHandling(t *testing.T) {
client := newTestClient(t)
var wg sync.WaitGroup
const numGoroutines = 10
const numMessages = 50
go func() {
for {
select {
case <-client.orderUpdates:
case <-client.positionUpdates:
case <-client.priceUpdates:
case <-time.After(2 * time.Second):
return
}
}
}()
for i := 0; i < numGoroutines; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
for j := 0; j < numMessages; j++ {
msgType := j % 3
switch msgType {
case 0:
client.processMessage(map[string]interface{}{
"type": "order_update",
"order": map[string]interface{}{"id": uint64(id*1000 + j)},
})
case 1:
client.processMessage(map[string]interface{}{
"type": "position_update",
"action": "modified",
})
case 2:
client.processMessage(map[string]interface{}{
"type": "price_update",
"symbol": "BTC/USD",
"price": 50000.0,
})
}
}
}(i)
}
wg.Wait()
}
// TestMockCallbackExecution tests callback execution
func TestMockCallbackExecution(t *testing.T) {
client := newTestClient(t)
var orderCalled, positionCalled, priceCalled, tradeCalled bool
var mu sync.Mutex
client.OnOrderUpdate(func(*OrderUpdate) {
mu.Lock()
orderCalled = true
mu.Unlock()
})
client.OnPositionUpdate(func(*PositionUpdate) {
mu.Lock()
positionCalled = true
mu.Unlock()
})
client.OnPriceUpdate(func(*PriceUpdate) {
mu.Lock()
priceCalled = true
mu.Unlock()
})
client.OnTradeUpdate(func(*TradeUpdate) {
mu.Lock()
tradeCalled = true
mu.Unlock()
})
// Note: handleOrderBookUpdate and handleBalanceUpdate don't invoke callbacks
// in the current implementation - they only update local state
// Trigger each type
go func() {
for {
select {
case <-client.orderUpdates:
case <-client.positionUpdates:
case <-client.priceUpdates:
case <-time.After(time.Second):
return
}
}
}()
client.processMessage(map[string]interface{}{
"type": "order_update",
"order": map[string]interface{}{"id": uint64(1)},
})
client.processMessage(map[string]interface{}{
"type": "position_update",
"action": "opened",
})
client.processMessage(map[string]interface{}{
"type": "price_update",
"symbol": "BTC/USD",
})
client.processMessage(map[string]interface{}{
"type": "trade_update",
"symbol": "BTC/USD",
"trade": map[string]interface{}{
"id": "test-trade",
"side": "buy",
"price": 50000.0,
"size": 1.0,
},
})
time.Sleep(50 * time.Millisecond)
mu.Lock()
defer mu.Unlock()
assert.True(t, orderCalled, "order callback not called")
assert.True(t, positionCalled, "position callback not called")
assert.True(t, priceCalled, "price callback not called")
assert.True(t, tradeCalled, "trade callback not called")
}
// TestMockMissingFields tests handling of messages with missing fields
func TestMockMissingFields(t *testing.T) {
client := newTestClient(t)
tests := []struct {
name string
msg map[string]interface{}
}{
{
name: "order_update_no_order",
msg: map[string]interface{}{"type": "order_update"},
},
{
name: "position_update_no_position",
msg: map[string]interface{}{"type": "position_update"},
},
{
name: "price_update_no_symbol",
msg: map[string]interface{}{"type": "price_update"},
},
{
name: "trade_update_no_symbol",
msg: map[string]interface{}{"type": "trade_update"},
},
{
name: "orderbook_update_no_data",
msg: map[string]interface{}{"type": "orderbook_update"},
},
{
name: "balance_update_no_currency",
msg: map[string]interface{}{"type": "balance_update"},
},
{
name: "error_no_message",
msg: map[string]interface{}{"type": "error"},
},
{
name: "no_type_field",
msg: map[string]interface{}{"data": "test"},
},
{
name: "empty_message",
msg: map[string]interface{}{},
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
// Should not panic
client.processMessage(tc.msg)
})
}
}
// TestMockDisconnectWhileNotConnected tests disconnect when not connected
func TestMockDisconnect(t *testing.T) {
client := newTestClient(t)
// Should not error
err := client.Disconnect()
assert.NoError(t, err)
}
// TestMockIsConnectedStatus tests connection status
func TestMockIsConnectedStatus(t *testing.T) {
client := newTestClient(t)
// Fields are package-level accessible
assert.False(t, client.connected)
client.connected = true
assert.True(t, client.connected)
}
// TestMockIsAuthenticatedStatus tests authentication status
func TestMockIsAuthenticatedStatus(t *testing.T) {
client := newTestClient(t)
assert.False(t, client.authenticated)
client.authenticated = true
assert.True(t, client.authenticated)
}
// TestMockGetOrdersEmpty tests getting orders when empty
func TestMockGetOrdersEmpty(t *testing.T) {
client := newTestClient(t)
orders := client.GetOrders()
assert.Empty(t, orders)
}
// TestMockGetPositionNotFound tests getting non-existent position
func TestMockGetPositionNotFound(t *testing.T) {
client := newTestClient(t)
_, err := client.GetPosition("non-existent")
assert.Error(t, err)
}
// TestMockGetPriceNotFound tests getting non-existent price
func TestMockGetPriceNotFound(t *testing.T) {
client := newTestClient(t)
_, err := client.GetPrice("UNKNOWN/USD")
assert.Error(t, err)
}
// TestMockGetOrderBookNotFound tests getting non-existent orderbook
func TestMockGetOrderBookNotFound(t *testing.T) {
client := newTestClient(t)
_, err := client.GetOrderBook("UNKNOWN/USD")
assert.Error(t, err)
}
// TestMockPositionUpdateWithStoredPosition tests position update modifying stored position
func TestMockPositionUpdateWithStoredPosition(t *testing.T) {
client := newTestClient(t)
// Pre-store a position
client.positions["pos-modify"] = &dex.MarginPosition{
ID: "pos-modify",
Symbol: "BTC/USD",
Size: 1.0,
Leverage: 10,
}
msg := map[string]interface{}{
"type": "position_update",
"action": "modified",
"position": map[string]interface{}{
"id": "pos-modify",
"symbol": "BTC/USD",
"leverage": 20,
},
}
go func() {
<-client.positionUpdates
}()
client.handlePositionUpdate(msg)
time.Sleep(10 * time.Millisecond)
// Position should still exist
pos, exists := client.positions["pos-modify"]
assert.True(t, exists)
assert.NotNil(t, pos)
}
// TestMockTradesStorage tests trades are stored correctly
func TestMockTradesStorage(t *testing.T) {
client := newTestClient(t)
client.OnTradeUpdate(func(*TradeUpdate) {})
for i := 0; i < 5; i++ {
msg := map[string]interface{}{
"type": "trade_update",
"symbol": "BTC/USD",
"trade": map[string]interface{}{
"id": "trade-" + string(rune('A'+i)),
"side": "buy",
"price": 50000.0 + float64(i*100),
"size": 1.0,
},
}
client.handleTradeUpdate(msg)
}
// Check trades were stored
trades := client.trades["BTC/USD"]
assert.NotEmpty(t, trades)
}
// TestMockPriceStorage tests prices are stored correctly
func TestMockPriceStorage(t *testing.T) {
client := newTestClient(t)
msg := map[string]interface{}{
"type": "price_update",
"symbol": "ETH/USD",
"price": 3000.0,
}
go func() {
<-client.priceUpdates
}()
client.handlePriceUpdate(msg)
time.Sleep(10 * time.Millisecond)
price, exists := client.prices["ETH/USD"]
assert.True(t, exists)
assert.Equal(t, 3000.0, price)
}
// TestMockOrderChannelGetters tests channel getter methods
func TestMockOrderChannelGetters(t *testing.T) {
client := newTestClient(t)
// Test all channel getters
assert.NotNil(t, client.GetOrderUpdates())
assert.NotNil(t, client.GetPositionUpdates())
assert.NotNil(t, client.GetPriceUpdates())
assert.NotNil(t, client.GetTradeUpdates())
assert.NotNil(t, client.GetErrors())
}
// TestMockOrderBookStorage tests orderbook storage
func TestMockOrderBookStorage(t *testing.T) {
client := newTestClient(t)
msg := map[string]interface{}{
"type": "orderbook_update",
"symbol": "DOT/USD",
"snapshot": map[string]interface{}{
"bids": []interface{}{[]interface{}{10.0, 100.0}},
"asks": []interface{}{[]interface{}{10.1, 100.0}},
},
}
client.handleOrderBookUpdate(msg)
// Orderbook should be stored
ob, exists := client.orderBooks["DOT/USD"]
assert.True(t, exists)
assert.NotNil(t, ob)
}
// TestMockBalanceStorage tests balance storage
func TestMockBalanceStorage(t *testing.T) {
client := newTestClient(t)
msg := map[string]interface{}{
"type": "balance_update",
"balances": map[string]interface{}{
"ETH": "5000000000000000000", // 5 ETH in wei
},
}
client.handleBalanceUpdate(msg)
// Balance should be stored
bal, exists := client.balances["ETH"]
assert.True(t, exists)
assert.NotNil(t, bal)
}
// TestMockMultipleOrderUpdates tests multiple order updates
func TestMockMultipleOrderUpdates(t *testing.T) {
client := newTestClient(t)
// Drain channel
go func() {
for {
select {
case <-client.orderUpdates:
case <-time.After(time.Second):
return
}
}
}()
for i := 0; i < 10; i++ {
msg := map[string]interface{}{
"type": "order_update",
"order": map[string]interface{}{
"id": uint64(i + 1),
"symbol": "BTC/USD",
"status": "new",
},
}
client.handleOrderUpdate(msg)
}
// Check orders were stored
assert.NotEmpty(t, client.orders)
}
// TestMockPositionRemovalOnClose tests that positions are removed when closed
func TestMockPositionRemovalOnClose(t *testing.T) {
client := newTestClient(t)
// Add a position
client.positions["pos-to-close"] = &dex.MarginPosition{
ID: "pos-to-close",
Symbol: "BTC/USD",
}
msg := map[string]interface{}{
"type": "position_update",
"action": "closed",
"position": map[string]interface{}{
"id": "pos-to-close",
"symbol": "BTC/USD",
},
}
go func() {
<-client.positionUpdates
}()
client.handlePositionUpdate(msg)
time.Sleep(10 * time.Millisecond)
// Position should be removed
_, exists := client.positions["pos-to-close"]
assert.False(t, exists)
}
// TestMockPositionRemovalOnLiquidation tests that positions are removed when liquidated
func TestMockPositionRemovalOnLiquidation(t *testing.T) {
client := newTestClient(t)
// Add a position
client.positions["pos-to-liq"] = &dex.MarginPosition{
ID: "pos-to-liq",
Symbol: "ETH/USD",
}
msg := map[string]interface{}{
"type": "position_update",
"action": "liquidated",
"position": map[string]interface{}{
"id": "pos-to-liq",
"symbol": "ETH/USD",
},
}
go func() {
<-client.positionUpdates
}()
client.handlePositionUpdate(msg)
time.Sleep(10 * time.Millisecond)
// Position should be removed
_, exists := client.positions["pos-to-liq"]
assert.False(t, exists)
}
// ==============================================================================
// Integration Tests with Mock WebSocket Server
// ==============================================================================
var upgrader = websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool { return true },
}
// mockWSServer creates a test WebSocket server that handles authentication and messages
func mockWSServer(t *testing.T, handler func(*websocket.Conn)) *httptest.Server {
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
t.Logf("Failed to upgrade: %v", err)
return
}
defer conn.Close()
handler(conn)
}))
}
// TestIntegrationConnectFailure tests connect failure with proper error
func TestIntegrationConnectFailure(t *testing.T) {
client, err := NewTraderClient(ClientConfig{
APIEndpoint: "http://localhost:9999",
WSEndpoint: "ws://localhost:9999/ws",
APIKey: "test-key",
APISecret: "test-secret",
UserID: "test-user",
})
require.NoError(t, err)
// Connect should fail since there's no server
err = client.Connect()
assert.Error(t, err)
assert.Contains(t, err.Error(), "websocket connection failed")
}
// TestIntegrationPlaceOrderAuthenticated tests placing order when authenticated
func TestIntegrationPlaceOrderAuthenticated(t *testing.T) {
var receivedMsg map[string]interface{}
msgReceived := make(chan bool, 1)
server := mockWSServer(t, func(conn *websocket.Conn) {
for {
var msg map[string]interface{}
err := conn.ReadJSON(&msg)
if err != nil {
return
}
receivedMsg = msg
msgReceived <- true
}
})
defer server.Close()
wsURL := "ws" + strings.TrimPrefix(server.URL, "http")
client, err := NewTraderClient(ClientConfig{
APIEndpoint: server.URL,
WSEndpoint: wsURL,
APIKey: "test-key",
APISecret: "test-secret",
UserID: "test-user",
})
require.NoError(t, err)
// Manually set up connection state
dialer := websocket.DefaultDialer
wsConn, _, err := dialer.Dial(wsURL, nil)
require.NoError(t, err)
defer wsConn.Close()
client.wsConn = wsConn
client.connected = true
client.authenticated = true
// Place order
order := &dex.Order{
ID: 12345,
Symbol: "BTC/USD",
Side: dex.Buy,
Price: 50000.0,
Size: 1.0,
}
result, err := client.PlaceOrder(order)
require.NoError(t, err)
assert.NotNil(t, result)
assert.Equal(t, "test-user", result.User)
assert.Equal(t, uint64(12345), result.ID)
// Wait for server to receive
select {
case <-msgReceived:
assert.Equal(t, "place_order", receivedMsg["type"])
case <-time.After(time.Second):
t.Error("Server did not receive message")
}
}
// TestIntegrationCancelOrderAuthenticated tests canceling order when authenticated
func TestIntegrationCancelOrderAuthenticated(t *testing.T) {
msgReceived := make(chan map[string]interface{}, 1)
server := mockWSServer(t, func(conn *websocket.Conn) {
for {
var msg map[string]interface{}
if err := conn.ReadJSON(&msg); err != nil {
return
}
msgReceived <- msg
}
})
defer server.Close()
wsURL := "ws" + strings.TrimPrefix(server.URL, "http")
client, err := NewTraderClient(ClientConfig{
APIEndpoint: server.URL,
WSEndpoint: wsURL,
APIKey: "test-key",
APISecret: "test-secret",
UserID: "test-user",
})
require.NoError(t, err)
// Set up connection
dialer := websocket.DefaultDialer
wsConn, _, err := dialer.Dial(wsURL, nil)
require.NoError(t, err)
defer wsConn.Close()
client.wsConn = wsConn
client.connected = true
client.authenticated = true
err = client.CancelOrder(999)
require.NoError(t, err)
select {
case msg := <-msgReceived:
assert.Equal(t, "cancel_order", msg["type"])
assert.Equal(t, float64(999), msg["orderID"])
case <-time.After(time.Second):
t.Error("Server did not receive cancel message")
}
}
// TestIntegrationModifyOrderAuthenticated tests modifying order when authenticated
func TestIntegrationModifyOrderAuthenticated(t *testing.T) {
msgReceived := make(chan map[string]interface{}, 1)
server := mockWSServer(t, func(conn *websocket.Conn) {
for {
var msg map[string]interface{}
if err := conn.ReadJSON(&msg); err != nil {
return
}
msgReceived <- msg
}
})
defer server.Close()
wsURL := "ws" + strings.TrimPrefix(server.URL, "http")
client, err := NewTraderClient(ClientConfig{
APIEndpoint: server.URL,
WSEndpoint: wsURL,
APIKey: "test-key",
APISecret: "test-secret",
UserID: "test-user",
})
require.NoError(t, err)
dialer := websocket.DefaultDialer
wsConn, _, err := dialer.Dial(wsURL, nil)
require.NoError(t, err)
defer wsConn.Close()
client.wsConn = wsConn
client.connected = true
client.authenticated = true
err = client.ModifyOrder(123, 55000.0, 2.5)
require.NoError(t, err)
select {
case msg := <-msgReceived:
assert.Equal(t, "modify_order", msg["type"])
assert.Equal(t, float64(123), msg["orderID"])
assert.Equal(t, 55000.0, msg["newPrice"])
assert.Equal(t, 2.5, msg["newSize"])
case <-time.After(time.Second):
t.Error("Server did not receive modify message")
}
}
// TestIntegrationOpenMarginPositionAuthenticated tests opening margin position
func TestIntegrationOpenMarginPositionAuthenticated(t *testing.T) {
msgReceived := make(chan map[string]interface{}, 1)
server := mockWSServer(t, func(conn *websocket.Conn) {
for {
var msg map[string]interface{}
if err := conn.ReadJSON(&msg); err != nil {
return
}
msgReceived <- msg
// If it's open_position, send back a position update
if msg["type"] == "open_position" {
conn.WriteJSON(map[string]interface{}{
"type": "position_update",
"action": "opened",
"position": map[string]interface{}{
"id": "test-pos-1",
"symbol": msg["symbol"],
"side": msg["side"],
"size": msg["size"],
"leverage": msg["leverage"],
},
})
}
}
})
defer server.Close()
wsURL := "ws" + strings.TrimPrefix(server.URL, "http")
client, err := NewTraderClient(ClientConfig{
APIEndpoint: server.URL,
WSEndpoint: wsURL,
APIKey: "test-key",
APISecret: "test-secret",
UserID: "test-user",
})
require.NoError(t, err)
dialer := websocket.DefaultDialer
wsConn, _, err := dialer.Dial(wsURL, nil)
require.NoError(t, err)
defer wsConn.Close()
client.wsConn = wsConn
client.connected = true
client.authenticated = true
// Start handleMessages to process position response
go client.handleMessages()
pos, err := client.OpenMarginPosition("BTC/USD", dex.Buy, 1.0, 10.0)
require.NoError(t, err)
assert.NotNil(t, pos)
assert.Equal(t, "test-pos-1", pos.ID)
select {
case msg := <-msgReceived:
assert.Equal(t, "open_position", msg["type"])
assert.Equal(t, "BTC/USD", msg["symbol"])
assert.Equal(t, float64(dex.Buy), msg["side"])
assert.Equal(t, 1.0, msg["size"])
assert.Equal(t, 10.0, msg["leverage"])
case <-time.After(time.Second):
t.Error("Server did not receive open position message")
}
}
// TestIntegrationClosePositionAuthenticated tests closing position
func TestIntegrationClosePositionAuthenticated(t *testing.T) {
msgReceived := make(chan map[string]interface{}, 1)
server := mockWSServer(t, func(conn *websocket.Conn) {
for {
var msg map[string]interface{}
if err := conn.ReadJSON(&msg); err != nil {
return
}
msgReceived <- msg
}
})
defer server.Close()
wsURL := "ws" + strings.TrimPrefix(server.URL, "http")
client, err := NewTraderClient(ClientConfig{
APIEndpoint: server.URL,
WSEndpoint: wsURL,
APIKey: "test-key",
APISecret: "test-secret",
UserID: "test-user",
})
require.NoError(t, err)
dialer := websocket.DefaultDialer
wsConn, _, err := dialer.Dial(wsURL, nil)
require.NoError(t, err)
defer wsConn.Close()
client.wsConn = wsConn
client.connected = true
client.authenticated = true
err = client.ClosePosition("pos-123", 1.5)
require.NoError(t, err)
select {
case msg := <-msgReceived:
assert.Equal(t, "close_position", msg["type"])
assert.Equal(t, "pos-123", msg["positionID"])
assert.Equal(t, 1.5, msg["size"])
case <-time.After(time.Second):
t.Error("Server did not receive close position message")
}
}
// TestIntegrationModifyLeverageAuthenticated tests modifying leverage
func TestIntegrationModifyLeverageAuthenticated(t *testing.T) {
msgReceived := make(chan map[string]interface{}, 1)
server := mockWSServer(t, func(conn *websocket.Conn) {
for {
var msg map[string]interface{}
if err := conn.ReadJSON(&msg); err != nil {
return
}
msgReceived <- msg
}
})
defer server.Close()
wsURL := "ws" + strings.TrimPrefix(server.URL, "http")
client, err := NewTraderClient(ClientConfig{
APIEndpoint: server.URL,
WSEndpoint: wsURL,
APIKey: "test-key",
APISecret: "test-secret",
UserID: "test-user",
})
require.NoError(t, err)
dialer := websocket.DefaultDialer
wsConn, _, err := dialer.Dial(wsURL, nil)
require.NoError(t, err)
defer wsConn.Close()
client.wsConn = wsConn
client.connected = true
client.authenticated = true
err = client.ModifyLeverage("pos-456", 20.0)
require.NoError(t, err)
select {
case msg := <-msgReceived:
assert.Equal(t, "modify_leverage", msg["type"])
assert.Equal(t, "pos-456", msg["positionID"])
assert.Equal(t, 20.0, msg["newLeverage"])
case <-time.After(time.Second):
t.Error("Server did not receive modify leverage message")
}
}
// TestIntegrationSubscribeAuthenticated tests subscribing to channels
func TestIntegrationSubscribeAuthenticated(t *testing.T) {
msgReceived := make(chan map[string]interface{}, 1)
server := mockWSServer(t, func(conn *websocket.Conn) {
for {
var msg map[string]interface{}
if err := conn.ReadJSON(&msg); err != nil {
return
}
msgReceived <- msg
}
})
defer server.Close()
wsURL := "ws" + strings.TrimPrefix(server.URL, "http")
client, err := NewTraderClient(ClientConfig{
APIEndpoint: server.URL,
WSEndpoint: wsURL,
APIKey: "test-key",
APISecret: "test-secret",
UserID: "test-user",
})
require.NoError(t, err)
dialer := websocket.DefaultDialer
wsConn, _, err := dialer.Dial(wsURL, nil)
require.NoError(t, err)
defer wsConn.Close()
client.wsConn = wsConn
client.connected = true
err = client.Subscribe("orderbook", []string{"BTC/USD"})
require.NoError(t, err)
// Verify subscription was tracked
assert.True(t, client.subscriptions["orderbook:BTC/USD"])
select {
case msg := <-msgReceived:
assert.Equal(t, "subscribe", msg["type"])
assert.Equal(t, "orderbook", msg["channel"])
symbols, ok := msg["symbols"].([]interface{})
require.True(t, ok, "symbols should be array")
assert.Contains(t, symbols, "BTC/USD")
case <-time.After(time.Second):
t.Error("Server did not receive subscribe message")
}
}
// TestIntegrationUnsubscribeAuthenticated tests unsubscribing from channels
func TestIntegrationUnsubscribeAuthenticated(t *testing.T) {
msgReceived := make(chan map[string]interface{}, 1)
server := mockWSServer(t, func(conn *websocket.Conn) {
for {
var msg map[string]interface{}
if err := conn.ReadJSON(&msg); err != nil {
return
}
msgReceived <- msg
}
})
defer server.Close()
wsURL := "ws" + strings.TrimPrefix(server.URL, "http")
client, err := NewTraderClient(ClientConfig{
APIEndpoint: server.URL,
WSEndpoint: wsURL,
APIKey: "test-key",
APISecret: "test-secret",
UserID: "test-user",
})
require.NoError(t, err)
dialer := websocket.DefaultDialer
wsConn, _, err := dialer.Dial(wsURL, nil)
require.NoError(t, err)
defer wsConn.Close()
client.wsConn = wsConn
client.connected = true
client.subscriptions["orderbook:ETH/USD"] = true
err = client.Unsubscribe("orderbook", []string{"ETH/USD"})
require.NoError(t, err)
// Verify subscription was removed
assert.False(t, client.subscriptions["orderbook:ETH/USD"])
select {
case msg := <-msgReceived:
assert.Equal(t, "unsubscribe", msg["type"])
assert.Equal(t, "orderbook", msg["channel"])
symbols, ok := msg["symbols"].([]interface{})
require.True(t, ok, "symbols should be array")
assert.Contains(t, symbols, "ETH/USD")
case <-time.After(time.Second):
t.Error("Server did not receive unsubscribe message")
}
}
// TestIntegrationVaultDeposit tests vault deposit
func TestIntegrationVaultDeposit(t *testing.T) {
msgReceived := make(chan map[string]interface{}, 1)
server := mockWSServer(t, func(conn *websocket.Conn) {
for {
var msg map[string]interface{}
if err := conn.ReadJSON(&msg); err != nil {
return
}
msgReceived <- msg
}
})
defer server.Close()
wsURL := "ws" + strings.TrimPrefix(server.URL, "http")
client, err := NewTraderClient(ClientConfig{
APIEndpoint: server.URL,
WSEndpoint: wsURL,
APIKey: "test-key",
APISecret: "test-secret",
UserID: "test-user",
})
require.NoError(t, err)
dialer := websocket.DefaultDialer
wsConn, _, err := dialer.Dial(wsURL, nil)
require.NoError(t, err)
defer wsConn.Close()
client.wsConn = wsConn
client.connected = true
client.authenticated = true
amount := big.NewInt(1000000)
err = client.DepositToVault("vault-1", amount)
require.NoError(t, err)
select {
case msg := <-msgReceived:
assert.Equal(t, "vault_deposit", msg["type"])
assert.Equal(t, "vault-1", msg["vaultID"])
case <-time.After(time.Second):
t.Error("Server did not receive vault deposit message")
}
}
// TestIntegrationVaultWithdraw tests vault withdraw
func TestIntegrationVaultWithdraw(t *testing.T) {
msgReceived := make(chan map[string]interface{}, 1)
server := mockWSServer(t, func(conn *websocket.Conn) {
for {
var msg map[string]interface{}
if err := conn.ReadJSON(&msg); err != nil {
return
}
msgReceived <- msg
}
})
defer server.Close()
wsURL := "ws" + strings.TrimPrefix(server.URL, "http")
client, err := NewTraderClient(ClientConfig{
APIEndpoint: server.URL,
WSEndpoint: wsURL,
APIKey: "test-key",
APISecret: "test-secret",
UserID: "test-user",
})
require.NoError(t, err)
dialer := websocket.DefaultDialer
wsConn, _, err := dialer.Dial(wsURL, nil)
require.NoError(t, err)
defer wsConn.Close()
client.wsConn = wsConn
client.connected = true
client.authenticated = true
amount := big.NewInt(500000)
err = client.WithdrawFromVault("vault-2", amount)
require.NoError(t, err)
select {
case msg := <-msgReceived:
assert.Equal(t, "vault_withdraw", msg["type"])
assert.Equal(t, "vault-2", msg["vaultID"])
case <-time.After(time.Second):
t.Error("Server did not receive vault withdraw message")
}
}
// TestIntegrationSupply tests supply to lending pool
func TestIntegrationSupply(t *testing.T) {
msgReceived := make(chan map[string]interface{}, 1)
server := mockWSServer(t, func(conn *websocket.Conn) {
for {
var msg map[string]interface{}
if err := conn.ReadJSON(&msg); err != nil {
return
}
msgReceived <- msg
}
})
defer server.Close()
wsURL := "ws" + strings.TrimPrefix(server.URL, "http")
client, err := NewTraderClient(ClientConfig{
APIEndpoint: server.URL,
WSEndpoint: wsURL,
APIKey: "test-key",
APISecret: "test-secret",
UserID: "test-user",
})
require.NoError(t, err)
dialer := websocket.DefaultDialer
wsConn, _, err := dialer.Dial(wsURL, nil)
require.NoError(t, err)
defer wsConn.Close()
client.wsConn = wsConn
client.connected = true
client.authenticated = true
amount := big.NewInt(2000000)
err = client.Supply("ETH", amount)
require.NoError(t, err)
select {
case msg := <-msgReceived:
assert.Equal(t, "lending_supply", msg["type"])
assert.Equal(t, "ETH", msg["asset"])
case <-time.After(time.Second):
t.Error("Server did not receive supply message")
}
}
// TestIntegrationBorrow tests borrowing from lending pool
func TestIntegrationBorrow(t *testing.T) {
msgReceived := make(chan map[string]interface{}, 1)
server := mockWSServer(t, func(conn *websocket.Conn) {
for {
var msg map[string]interface{}
if err := conn.ReadJSON(&msg); err != nil {
return
}
msgReceived <- msg
}
})
defer server.Close()
wsURL := "ws" + strings.TrimPrefix(server.URL, "http")
client, err := NewTraderClient(ClientConfig{
APIEndpoint: server.URL,
WSEndpoint: wsURL,
APIKey: "test-key",
APISecret: "test-secret",
UserID: "test-user",
})
require.NoError(t, err)
dialer := websocket.DefaultDialer
wsConn, _, err := dialer.Dial(wsURL, nil)
require.NoError(t, err)
defer wsConn.Close()
client.wsConn = wsConn
client.connected = true
client.authenticated = true
amount := big.NewInt(1000000)
err = client.Borrow("USDC", amount)
require.NoError(t, err)
select {
case msg := <-msgReceived:
assert.Equal(t, "lending_borrow", msg["type"])
assert.Equal(t, "USDC", msg["asset"])
case <-time.After(time.Second):
t.Error("Server did not receive borrow message")
}
}
// TestIntegrationRepay tests repaying borrowed amount
func TestIntegrationRepay(t *testing.T) {
msgReceived := make(chan map[string]interface{}, 1)
server := mockWSServer(t, func(conn *websocket.Conn) {
for {
var msg map[string]interface{}
if err := conn.ReadJSON(&msg); err != nil {
return
}
msgReceived <- msg
}
})
defer server.Close()
wsURL := "ws" + strings.TrimPrefix(server.URL, "http")
client, err := NewTraderClient(ClientConfig{
APIEndpoint: server.URL,
WSEndpoint: wsURL,
APIKey: "test-key",
APISecret: "test-secret",
UserID: "test-user",
})
require.NoError(t, err)
dialer := websocket.DefaultDialer
wsConn, _, err := dialer.Dial(wsURL, nil)
require.NoError(t, err)
defer wsConn.Close()
client.wsConn = wsConn
client.connected = true
client.authenticated = true
amount := big.NewInt(750000)
err = client.Repay("USDC", amount)
require.NoError(t, err)
select {
case msg := <-msgReceived:
assert.Equal(t, "lending_repay", msg["type"])
assert.Equal(t, "USDC", msg["asset"])
case <-time.After(time.Second):
t.Error("Server did not receive repay message")
}
}
// TestIntegrationDisconnect tests disconnecting
func TestIntegrationDisconnect(t *testing.T) {
server := mockWSServer(t, func(conn *websocket.Conn) {
// Just wait for connection to close
for {
_, _, err := conn.ReadMessage()
if err != nil {
return
}
}
})
defer server.Close()
wsURL := "ws" + strings.TrimPrefix(server.URL, "http")
client, err := NewTraderClient(ClientConfig{
APIEndpoint: server.URL,
WSEndpoint: wsURL,
APIKey: "test-key",
APISecret: "test-secret",
UserID: "test-user",
})
require.NoError(t, err)
dialer := websocket.DefaultDialer
wsConn, _, err := dialer.Dial(wsURL, nil)
require.NoError(t, err)
client.wsConn = wsConn
client.connected = true
client.authenticated = true
err = client.Disconnect()
require.NoError(t, err)
assert.False(t, client.connected)
assert.False(t, client.authenticated)
}
// TestIntegrationHandleMessages tests message handling loop
func TestIntegrationHandleMessages(t *testing.T) {
server := mockWSServer(t, func(conn *websocket.Conn) {
// Send some messages
conn.WriteJSON(map[string]interface{}{
"type": "price_update",
"symbol": "BTC/USD",
"price": 45000.0,
})
conn.WriteJSON(map[string]interface{}{
"type": "price_update",
"symbol": "ETH/USD",
"price": 3000.0,
})
time.Sleep(100 * time.Millisecond)
conn.Close()
})
defer server.Close()
wsURL := "ws" + strings.TrimPrefix(server.URL, "http")
client, err := NewTraderClient(ClientConfig{
APIEndpoint: server.URL,
WSEndpoint: wsURL,
APIKey: "test-key",
APISecret: "test-secret",
UserID: "test-user",
})
require.NoError(t, err)
dialer := websocket.DefaultDialer
wsConn, _, err := dialer.Dial(wsURL, nil)
require.NoError(t, err)
client.wsConn = wsConn
client.connected = true
// Start handleMessages in background
go client.handleMessages()
// Wait and check prices were received
time.Sleep(200 * time.Millisecond)
client.mu.RLock()
btcPrice := client.prices["BTC/USD"]
ethPrice := client.prices["ETH/USD"]
client.mu.RUnlock()
assert.Equal(t, 45000.0, btcPrice)
assert.Equal(t, 3000.0, ethPrice)
}