mirror of
https://github.com/luxfi/dex.git
synced 2026-09-24 00:26:31 +00:00
Added comprehensive unit and integration tests for TraderClient: Unit tests (without WebSocket): - Message handlers (order, position, price, trade, orderbook, balance, error) - State management (orders, positions, prices, trades storage) - Callback registration and execution - Channel getters and update channels - Missing field handling Integration tests (with mock WebSocket server): - PlaceOrder, CancelOrder, ModifyOrder (authenticated path) - OpenMarginPosition with server response - ClosePosition, ModifyLeverage - Subscribe/Unsubscribe with symbols array - VaultDeposit, VaultWithdraw - Lending operations (Supply, Borrow, Repay) - Disconnect - handleMessages loop Coverage improved from 64.2% to 88.2% of statements.
1703 lines
41 KiB
Go
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/lx"
|
|
"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(&lx.Order{
|
|
Symbol: "BTC/USD",
|
|
Side: lx.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", lx.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"] = &lx.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"] = &lx.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"] = &lx.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"] = &lx.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 := &lx.Order{
|
|
ID: 12345,
|
|
Symbol: "BTC/USD",
|
|
Side: lx.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", lx.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(lx.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)
|
|
}
|