mirror of
https://github.com/luxfi/dex.git
synced 2026-09-24 00:26:31 +00:00
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]>
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/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)
|
|
}
|