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]>
2449 lines
56 KiB
Go
2449 lines
56 KiB
Go
package api
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/luxfi/dex/pkg/dex"
|
|
"github.com/luxfi/log"
|
|
"github.com/stretchr/testify/assert"
|
|
)
|
|
|
|
// --- IPRateLimiter Tests ---
|
|
|
|
func TestIPRateLimiter_NewIPRateLimiter(t *testing.T) {
|
|
rl := NewIPRateLimiter(100, time.Second)
|
|
assert.NotNil(t, rl)
|
|
assert.NotNil(t, rl.requests)
|
|
assert.Equal(t, 100, rl.limit)
|
|
assert.Equal(t, time.Second, rl.window)
|
|
}
|
|
|
|
func TestIPRateLimiter_Allow_NewEntry(t *testing.T) {
|
|
rl := NewIPRateLimiter(10, time.Second)
|
|
|
|
// First request from a new IP
|
|
assert.True(t, rl.Allow("192.168.1.1"))
|
|
|
|
// Entry should exist
|
|
rl.mu.RLock()
|
|
entry, exists := rl.requests["192.168.1.1"]
|
|
rl.mu.RUnlock()
|
|
assert.True(t, exists)
|
|
assert.Equal(t, 1, entry.count)
|
|
}
|
|
|
|
func TestIPRateLimiter_Allow_ExistingEntry(t *testing.T) {
|
|
rl := NewIPRateLimiter(10, time.Second)
|
|
|
|
// Multiple requests from same IP
|
|
for i := 0; i < 5; i++ {
|
|
assert.True(t, rl.Allow("192.168.1.1"))
|
|
}
|
|
|
|
rl.mu.RLock()
|
|
entry := rl.requests["192.168.1.1"]
|
|
rl.mu.RUnlock()
|
|
assert.Equal(t, 5, entry.count)
|
|
}
|
|
|
|
func TestIPRateLimiter_Allow_RateLimitExceeded(t *testing.T) {
|
|
rl := NewIPRateLimiter(3, time.Second)
|
|
|
|
// First 3 requests should pass
|
|
assert.True(t, rl.Allow("192.168.1.1"))
|
|
assert.True(t, rl.Allow("192.168.1.1"))
|
|
assert.True(t, rl.Allow("192.168.1.1"))
|
|
|
|
// 4th should be rate limited
|
|
assert.False(t, rl.Allow("192.168.1.1"))
|
|
}
|
|
|
|
func TestIPRateLimiter_Allow_WindowReset(t *testing.T) {
|
|
rl := NewIPRateLimiter(2, 50*time.Millisecond)
|
|
|
|
// Exhaust limit
|
|
assert.True(t, rl.Allow("192.168.1.1"))
|
|
assert.True(t, rl.Allow("192.168.1.1"))
|
|
assert.False(t, rl.Allow("192.168.1.1"))
|
|
|
|
// Wait for window to pass
|
|
time.Sleep(60 * time.Millisecond)
|
|
|
|
// Should be allowed again
|
|
assert.True(t, rl.Allow("192.168.1.1"))
|
|
}
|
|
|
|
func TestIPRateLimiter_Allow_MultipleIPs(t *testing.T) {
|
|
rl := NewIPRateLimiter(2, time.Second)
|
|
|
|
// IP1 hits limit
|
|
assert.True(t, rl.Allow("192.168.1.1"))
|
|
assert.True(t, rl.Allow("192.168.1.1"))
|
|
assert.False(t, rl.Allow("192.168.1.1"))
|
|
|
|
// IP2 should still work
|
|
assert.True(t, rl.Allow("192.168.1.2"))
|
|
assert.True(t, rl.Allow("192.168.1.2"))
|
|
assert.False(t, rl.Allow("192.168.1.2"))
|
|
}
|
|
|
|
func TestIPRateLimiter_Cleanup_EntriesRemoved(t *testing.T) {
|
|
rl := NewIPRateLimiter(10, 30*time.Millisecond)
|
|
|
|
// Add some requests
|
|
rl.Allow("192.168.1.1")
|
|
rl.Allow("192.168.1.2")
|
|
|
|
// Wait for cleanup to run
|
|
time.Sleep(70 * time.Millisecond)
|
|
|
|
// Entries should be cleaned up (expired)
|
|
rl.mu.RLock()
|
|
_, exists1 := rl.requests["192.168.1.1"]
|
|
_, exists2 := rl.requests["192.168.1.2"]
|
|
rl.mu.RUnlock()
|
|
|
|
// After cleanup window, entries should be removed
|
|
assert.False(t, exists1, "Entry 1 should be cleaned up")
|
|
assert.False(t, exists2, "Entry 2 should be cleaned up")
|
|
}
|
|
|
|
// --- getClientIP Tests ---
|
|
|
|
func TestGetClientIP_XForwardedFor_SingleIP(t *testing.T) {
|
|
req := httptest.NewRequest("POST", "/", nil)
|
|
req.Header.Set("X-Forwarded-For", "203.0.113.195")
|
|
|
|
ip := getClientIP(req)
|
|
assert.Equal(t, "203.0.113.195", ip)
|
|
}
|
|
|
|
func TestGetClientIP_XForwardedFor_MultipleIPs(t *testing.T) {
|
|
req := httptest.NewRequest("POST", "/", nil)
|
|
req.Header.Set("X-Forwarded-For", "203.0.113.195, 70.41.3.18, 150.172.238.178")
|
|
|
|
ip := getClientIP(req)
|
|
assert.Equal(t, "203.0.113.195", ip) // First IP in chain
|
|
}
|
|
|
|
func TestGetClientIP_RemoteAddr_WithPort(t *testing.T) {
|
|
req := httptest.NewRequest("POST", "/", nil)
|
|
req.RemoteAddr = "192.168.1.100:12345"
|
|
|
|
ip := getClientIP(req)
|
|
assert.Equal(t, "192.168.1.100", ip)
|
|
}
|
|
|
|
func TestGetClientIP_RemoteAddr_IPv6(t *testing.T) {
|
|
req := httptest.NewRequest("POST", "/", nil)
|
|
req.RemoteAddr = "[::1]:12345"
|
|
|
|
ip := getClientIP(req)
|
|
assert.Equal(t, "::1", ip)
|
|
}
|
|
|
|
func TestGetClientIP_RemoteAddr_NoPort(t *testing.T) {
|
|
req := httptest.NewRequest("POST", "/", nil)
|
|
req.RemoteAddr = "192.168.1.100" // Invalid format but should fallback
|
|
|
|
ip := getClientIP(req)
|
|
assert.Equal(t, "192.168.1.100", ip)
|
|
}
|
|
|
|
func TestGetClientIP_EmptyXForwardedFor(t *testing.T) {
|
|
req := httptest.NewRequest("POST", "/", nil)
|
|
req.Header.Set("X-Forwarded-For", "")
|
|
req.RemoteAddr = "192.168.1.100:12345"
|
|
|
|
ip := getClientIP(req)
|
|
assert.Equal(t, "192.168.1.100", ip)
|
|
}
|
|
|
|
// --- JSONRPCServer Rate Limiting Tests ---
|
|
|
|
func TestJSONRPCServer_RateLimiting_Exceeded(t *testing.T) {
|
|
orderBook := dex.NewOrderBook("TEST")
|
|
level, _ := log.ToLevel("error")
|
|
logger := log.NewTestLogger(level)
|
|
|
|
// Create server with low rate limit for testing
|
|
server := &JSONRPCServer{
|
|
orderBook: orderBook,
|
|
logger: logger,
|
|
rateLimiter: NewIPRateLimiter(2, time.Second),
|
|
}
|
|
|
|
reqBody := `{"jsonrpc":"2.0","method":"orderbook.getBestBid","params":{},"id":1}`
|
|
|
|
// First two requests should pass
|
|
for i := 0; i < 2; i++ {
|
|
req := httptest.NewRequest("POST", "/rpc", bytes.NewBufferString(reqBody))
|
|
req.RemoteAddr = "192.168.1.1:12345"
|
|
w := httptest.NewRecorder()
|
|
server.ServeHTTP(w, req)
|
|
assert.Equal(t, http.StatusOK, w.Code, "Request %d should succeed", i+1)
|
|
}
|
|
|
|
// Third request should be rate limited
|
|
req := httptest.NewRequest("POST", "/rpc", bytes.NewBufferString(reqBody))
|
|
req.RemoteAddr = "192.168.1.1:12345"
|
|
w := httptest.NewRecorder()
|
|
server.ServeHTTP(w, req)
|
|
assert.Equal(t, http.StatusTooManyRequests, w.Code)
|
|
assert.Equal(t, "1", w.Header().Get("Retry-After"))
|
|
}
|
|
|
|
func TestJSONRPCServer_RateLimiting_DifferentIPs(t *testing.T) {
|
|
orderBook := dex.NewOrderBook("TEST")
|
|
level, _ := log.ToLevel("error")
|
|
logger := log.NewTestLogger(level)
|
|
|
|
server := &JSONRPCServer{
|
|
orderBook: orderBook,
|
|
logger: logger,
|
|
rateLimiter: NewIPRateLimiter(1, time.Second),
|
|
}
|
|
|
|
reqBody := `{"jsonrpc":"2.0","method":"orderbook.getBestBid","params":{},"id":1}`
|
|
|
|
// IP1 makes request
|
|
req1 := httptest.NewRequest("POST", "/rpc", bytes.NewBufferString(reqBody))
|
|
req1.RemoteAddr = "192.168.1.1:12345"
|
|
w1 := httptest.NewRecorder()
|
|
server.ServeHTTP(w1, req1)
|
|
assert.Equal(t, http.StatusOK, w1.Code)
|
|
|
|
// IP1 is now limited
|
|
req2 := httptest.NewRequest("POST", "/rpc", bytes.NewBufferString(reqBody))
|
|
req2.RemoteAddr = "192.168.1.1:12345"
|
|
w2 := httptest.NewRecorder()
|
|
server.ServeHTTP(w2, req2)
|
|
assert.Equal(t, http.StatusTooManyRequests, w2.Code)
|
|
|
|
// IP2 should still work
|
|
req3 := httptest.NewRequest("POST", "/rpc", bytes.NewBufferString(reqBody))
|
|
req3.RemoteAddr = "192.168.1.2:12345"
|
|
w3 := httptest.NewRecorder()
|
|
server.ServeHTTP(w3, req3)
|
|
assert.Equal(t, http.StatusOK, w3.Code)
|
|
}
|
|
|
|
// --- Additional processMessage coverage tests ---
|
|
|
|
func TestProcessMessage_WithRequestID(t *testing.T) {
|
|
server := newTestWebSocketServer()
|
|
client := &Client{
|
|
ID: "test-client",
|
|
UserID: "user123",
|
|
send: make(chan []byte, 256),
|
|
subscriptions: make(map[string]bool),
|
|
authenticated: true,
|
|
lastActivity: time.Now(),
|
|
rateLimiter: NewRateLimiter(100, time.Minute),
|
|
}
|
|
|
|
// Test with request_id
|
|
msg := map[string]interface{}{
|
|
"type": "ping",
|
|
"request_id": "test-request-123",
|
|
}
|
|
|
|
server.processMessage(client, msg)
|
|
|
|
select {
|
|
case <-client.send:
|
|
// Response received
|
|
case <-time.After(time.Second):
|
|
t.Fatal("Expected response")
|
|
}
|
|
}
|
|
|
|
// --- Additional handleAuth coverage ---
|
|
|
|
func TestHandleAuthSuccess(t *testing.T) {
|
|
server := newTestWebSocketServer()
|
|
client := &Client{
|
|
ID: "test-client",
|
|
send: make(chan []byte, 256),
|
|
subscriptions: make(map[string]bool),
|
|
authenticated: false,
|
|
lastActivity: time.Now(),
|
|
rateLimiter: NewRateLimiter(100, time.Minute),
|
|
}
|
|
|
|
msg := map[string]interface{}{
|
|
"apiKey": "valid_key",
|
|
"apiSecret": "valid_secret",
|
|
}
|
|
|
|
server.handleAuth(client, msg, "auth123")
|
|
|
|
// Should receive auth_success followed by initial data
|
|
timeout := time.After(2 * time.Second)
|
|
authSuccess := false
|
|
for !authSuccess {
|
|
select {
|
|
case <-client.send:
|
|
if client.authenticated {
|
|
authSuccess = true
|
|
}
|
|
case <-timeout:
|
|
break
|
|
}
|
|
}
|
|
|
|
assert.True(t, client.authenticated)
|
|
assert.Equal(t, "user123", client.UserID)
|
|
}
|
|
|
|
// --- handlePlaceOrder edge cases ---
|
|
|
|
func TestHandlePlaceOrder_OrderDataNotMap(t *testing.T) {
|
|
server := newTestWebSocketServer()
|
|
client := &Client{
|
|
ID: "test-client",
|
|
UserID: "user123",
|
|
send: make(chan []byte, 256),
|
|
subscriptions: make(map[string]bool),
|
|
authenticated: true,
|
|
lastActivity: time.Now(),
|
|
rateLimiter: NewRateLimiter(100, time.Minute),
|
|
}
|
|
|
|
msg := map[string]interface{}{
|
|
"type": "place_order",
|
|
"order": "not-a-map",
|
|
}
|
|
|
|
server.handlePlaceOrder(client, msg, "req123")
|
|
|
|
select {
|
|
case <-client.send:
|
|
// Error received
|
|
case <-time.After(time.Second):
|
|
t.Fatal("Expected error response")
|
|
}
|
|
}
|
|
|
|
func TestHandlePlaceOrder_MissingSymbol(t *testing.T) {
|
|
server := newTestWebSocketServer()
|
|
client := &Client{
|
|
ID: "test-client",
|
|
UserID: "user123",
|
|
send: make(chan []byte, 256),
|
|
subscriptions: make(map[string]bool),
|
|
authenticated: true,
|
|
lastActivity: time.Now(),
|
|
rateLimiter: NewRateLimiter(100, time.Minute),
|
|
}
|
|
|
|
msg := map[string]interface{}{
|
|
"type": "place_order",
|
|
"order": map[string]interface{}{
|
|
"side": "buy",
|
|
"type": "limit",
|
|
"price": 100.0,
|
|
"size": 10.0,
|
|
},
|
|
}
|
|
|
|
server.handlePlaceOrder(client, msg, "req123")
|
|
|
|
select {
|
|
case <-client.send:
|
|
// Error received
|
|
case <-time.After(time.Second):
|
|
t.Fatal("Expected error response")
|
|
}
|
|
}
|
|
|
|
func TestHandlePlaceOrder_MissingSide(t *testing.T) {
|
|
server := newTestWebSocketServer()
|
|
client := &Client{
|
|
ID: "test-client",
|
|
UserID: "user123",
|
|
send: make(chan []byte, 256),
|
|
subscriptions: make(map[string]bool),
|
|
authenticated: true,
|
|
lastActivity: time.Now(),
|
|
rateLimiter: NewRateLimiter(100, time.Minute),
|
|
}
|
|
|
|
msg := map[string]interface{}{
|
|
"type": "place_order",
|
|
"order": map[string]interface{}{
|
|
"symbol": "BTC/USD",
|
|
"type": "limit",
|
|
"price": 100.0,
|
|
"size": 10.0,
|
|
},
|
|
}
|
|
|
|
server.handlePlaceOrder(client, msg, "req123")
|
|
|
|
select {
|
|
case <-client.send:
|
|
// Error received
|
|
case <-time.After(time.Second):
|
|
t.Fatal("Expected error response")
|
|
}
|
|
}
|
|
|
|
func TestHandlePlaceOrder_MissingType(t *testing.T) {
|
|
server := newTestWebSocketServer()
|
|
client := &Client{
|
|
ID: "test-client",
|
|
UserID: "user123",
|
|
send: make(chan []byte, 256),
|
|
subscriptions: make(map[string]bool),
|
|
authenticated: true,
|
|
lastActivity: time.Now(),
|
|
rateLimiter: NewRateLimiter(100, time.Minute),
|
|
}
|
|
|
|
msg := map[string]interface{}{
|
|
"type": "place_order",
|
|
"order": map[string]interface{}{
|
|
"symbol": "BTC/USD",
|
|
"side": "buy",
|
|
"price": 100.0,
|
|
"size": 10.0,
|
|
},
|
|
}
|
|
|
|
server.handlePlaceOrder(client, msg, "req123")
|
|
|
|
select {
|
|
case <-client.send:
|
|
// Error received
|
|
case <-time.After(time.Second):
|
|
t.Fatal("Expected error response")
|
|
}
|
|
}
|
|
|
|
func TestHandlePlaceOrder_MissingPrice(t *testing.T) {
|
|
server := newTestWebSocketServer()
|
|
client := &Client{
|
|
ID: "test-client",
|
|
UserID: "user123",
|
|
send: make(chan []byte, 256),
|
|
subscriptions: make(map[string]bool),
|
|
authenticated: true,
|
|
lastActivity: time.Now(),
|
|
rateLimiter: NewRateLimiter(100, time.Minute),
|
|
}
|
|
|
|
msg := map[string]interface{}{
|
|
"type": "place_order",
|
|
"order": map[string]interface{}{
|
|
"symbol": "BTC/USD",
|
|
"side": "buy",
|
|
"type": "limit",
|
|
"size": 10.0,
|
|
},
|
|
}
|
|
|
|
server.handlePlaceOrder(client, msg, "req123")
|
|
|
|
select {
|
|
case <-client.send:
|
|
// Error received
|
|
case <-time.After(time.Second):
|
|
t.Fatal("Expected error response")
|
|
}
|
|
}
|
|
|
|
func TestHandlePlaceOrder_MissingSize(t *testing.T) {
|
|
server := newTestWebSocketServer()
|
|
client := &Client{
|
|
ID: "test-client",
|
|
UserID: "user123",
|
|
send: make(chan []byte, 256),
|
|
subscriptions: make(map[string]bool),
|
|
authenticated: true,
|
|
lastActivity: time.Now(),
|
|
rateLimiter: NewRateLimiter(100, time.Minute),
|
|
}
|
|
|
|
msg := map[string]interface{}{
|
|
"type": "place_order",
|
|
"order": map[string]interface{}{
|
|
"symbol": "BTC/USD",
|
|
"side": "buy",
|
|
"type": "limit",
|
|
"price": 100.0,
|
|
},
|
|
}
|
|
|
|
server.handlePlaceOrder(client, msg, "req123")
|
|
|
|
select {
|
|
case <-client.send:
|
|
// Error received
|
|
case <-time.After(time.Second):
|
|
t.Fatal("Expected error response")
|
|
}
|
|
}
|
|
|
|
// --- handleCancelOrder edge cases ---
|
|
|
|
func TestHandleCancelOrder_InvalidOrderIDFormat(t *testing.T) {
|
|
server := newTestWebSocketServer()
|
|
client := &Client{
|
|
ID: "test-client",
|
|
UserID: "user123",
|
|
send: make(chan []byte, 256),
|
|
subscriptions: make(map[string]bool),
|
|
authenticated: true,
|
|
lastActivity: time.Now(),
|
|
rateLimiter: NewRateLimiter(100, time.Minute),
|
|
}
|
|
|
|
msg := map[string]interface{}{
|
|
"type": "cancel_order",
|
|
"orderID": "not-a-number",
|
|
}
|
|
|
|
server.handleCancelOrder(client, msg, "req123")
|
|
|
|
select {
|
|
case <-client.send:
|
|
// Error received
|
|
case <-time.After(time.Second):
|
|
t.Fatal("Expected error response")
|
|
}
|
|
}
|
|
|
|
func TestHandleCancelOrder_MissingOrderID(t *testing.T) {
|
|
server := newTestWebSocketServer()
|
|
client := &Client{
|
|
ID: "test-client",
|
|
UserID: "user123",
|
|
send: make(chan []byte, 256),
|
|
subscriptions: make(map[string]bool),
|
|
authenticated: true,
|
|
lastActivity: time.Now(),
|
|
rateLimiter: NewRateLimiter(100, time.Minute),
|
|
}
|
|
|
|
msg := map[string]interface{}{
|
|
"type": "cancel_order",
|
|
}
|
|
|
|
server.handleCancelOrder(client, msg, "req123")
|
|
|
|
select {
|
|
case <-client.send:
|
|
// Error received
|
|
case <-time.After(time.Second):
|
|
t.Fatal("Expected error response")
|
|
}
|
|
}
|
|
|
|
// --- handleOpenPosition edge cases ---
|
|
|
|
func TestHandleOpenPosition_MissingSymbol(t *testing.T) {
|
|
server := newTestWebSocketServer()
|
|
client := &Client{
|
|
ID: "test-client",
|
|
UserID: "user123",
|
|
send: make(chan []byte, 256),
|
|
subscriptions: make(map[string]bool),
|
|
authenticated: true,
|
|
lastActivity: time.Now(),
|
|
rateLimiter: NewRateLimiter(100, time.Minute),
|
|
}
|
|
|
|
msg := map[string]interface{}{
|
|
"type": "open_position",
|
|
"side": "buy",
|
|
"size": 1.0,
|
|
"leverage": 10.0,
|
|
}
|
|
|
|
server.handleOpenPosition(client, msg, "req123")
|
|
|
|
select {
|
|
case <-client.send:
|
|
// Error received
|
|
case <-time.After(time.Second):
|
|
t.Fatal("Expected error response")
|
|
}
|
|
}
|
|
|
|
func TestHandleOpenPosition_MissingSide(t *testing.T) {
|
|
server := newTestWebSocketServer()
|
|
client := &Client{
|
|
ID: "test-client",
|
|
UserID: "user123",
|
|
send: make(chan []byte, 256),
|
|
subscriptions: make(map[string]bool),
|
|
authenticated: true,
|
|
lastActivity: time.Now(),
|
|
rateLimiter: NewRateLimiter(100, time.Minute),
|
|
}
|
|
|
|
msg := map[string]interface{}{
|
|
"type": "open_position",
|
|
"symbol": "BTC/USD",
|
|
"size": 1.0,
|
|
"leverage": 10.0,
|
|
}
|
|
|
|
server.handleOpenPosition(client, msg, "req123")
|
|
|
|
select {
|
|
case <-client.send:
|
|
// Error received
|
|
case <-time.After(time.Second):
|
|
t.Fatal("Expected error response")
|
|
}
|
|
}
|
|
|
|
func TestHandleOpenPosition_MissingSize(t *testing.T) {
|
|
server := newTestWebSocketServer()
|
|
client := &Client{
|
|
ID: "test-client",
|
|
UserID: "user123",
|
|
send: make(chan []byte, 256),
|
|
subscriptions: make(map[string]bool),
|
|
authenticated: true,
|
|
lastActivity: time.Now(),
|
|
rateLimiter: NewRateLimiter(100, time.Minute),
|
|
}
|
|
|
|
msg := map[string]interface{}{
|
|
"type": "open_position",
|
|
"symbol": "BTC/USD",
|
|
"side": "buy",
|
|
"leverage": 10.0,
|
|
}
|
|
|
|
server.handleOpenPosition(client, msg, "req123")
|
|
|
|
select {
|
|
case <-client.send:
|
|
// Error received
|
|
case <-time.After(time.Second):
|
|
t.Fatal("Expected error response")
|
|
}
|
|
}
|
|
|
|
func TestHandleOpenPosition_MissingLeverage(t *testing.T) {
|
|
server := newTestWebSocketServer()
|
|
client := &Client{
|
|
ID: "test-client",
|
|
UserID: "user123",
|
|
send: make(chan []byte, 256),
|
|
subscriptions: make(map[string]bool),
|
|
authenticated: true,
|
|
lastActivity: time.Now(),
|
|
rateLimiter: NewRateLimiter(100, time.Minute),
|
|
}
|
|
|
|
msg := map[string]interface{}{
|
|
"type": "open_position",
|
|
"symbol": "BTC/USD",
|
|
"side": "buy",
|
|
"size": 1.0,
|
|
}
|
|
|
|
server.handleOpenPosition(client, msg, "req123")
|
|
|
|
select {
|
|
case <-client.send:
|
|
// Error received
|
|
case <-time.After(time.Second):
|
|
t.Fatal("Expected error response")
|
|
}
|
|
}
|
|
|
|
// --- handleClosePosition edge cases ---
|
|
|
|
func TestHandleClosePosition_MissingPositionID(t *testing.T) {
|
|
server := newTestWebSocketServer()
|
|
client := &Client{
|
|
ID: "test-client",
|
|
UserID: "user123",
|
|
send: make(chan []byte, 256),
|
|
subscriptions: make(map[string]bool),
|
|
authenticated: true,
|
|
lastActivity: time.Now(),
|
|
rateLimiter: NewRateLimiter(100, time.Minute),
|
|
}
|
|
|
|
msg := map[string]interface{}{
|
|
"type": "close_position",
|
|
"size": 1.0,
|
|
}
|
|
|
|
server.handleClosePosition(client, msg, "req123")
|
|
|
|
select {
|
|
case <-client.send:
|
|
// Error received
|
|
case <-time.After(time.Second):
|
|
t.Fatal("Expected error response")
|
|
}
|
|
}
|
|
|
|
func TestHandleClosePosition_MissingSize(t *testing.T) {
|
|
server := newTestWebSocketServer()
|
|
client := &Client{
|
|
ID: "test-client",
|
|
UserID: "user123",
|
|
send: make(chan []byte, 256),
|
|
subscriptions: make(map[string]bool),
|
|
authenticated: true,
|
|
lastActivity: time.Now(),
|
|
rateLimiter: NewRateLimiter(100, time.Minute),
|
|
}
|
|
|
|
msg := map[string]interface{}{
|
|
"type": "close_position",
|
|
"positionID": "pos123",
|
|
}
|
|
|
|
server.handleClosePosition(client, msg, "req123")
|
|
|
|
select {
|
|
case <-client.send:
|
|
// Error received
|
|
case <-time.After(time.Second):
|
|
t.Fatal("Expected error response")
|
|
}
|
|
}
|
|
|
|
// --- handleVaultDeposit edge cases ---
|
|
|
|
func TestHandleVaultDeposit_MissingVaultID(t *testing.T) {
|
|
server := newTestWebSocketServer()
|
|
client := &Client{
|
|
ID: "test-client",
|
|
UserID: "user123",
|
|
send: make(chan []byte, 256),
|
|
subscriptions: make(map[string]bool),
|
|
authenticated: true,
|
|
lastActivity: time.Now(),
|
|
rateLimiter: NewRateLimiter(100, time.Minute),
|
|
}
|
|
|
|
msg := map[string]interface{}{
|
|
"type": "vault_deposit",
|
|
"amount": "1000",
|
|
}
|
|
|
|
server.handleVaultDeposit(client, msg, "req123")
|
|
|
|
select {
|
|
case <-client.send:
|
|
// Error received
|
|
case <-time.After(time.Second):
|
|
t.Fatal("Expected error response")
|
|
}
|
|
}
|
|
|
|
func TestHandleVaultDeposit_MissingAmount(t *testing.T) {
|
|
server := newTestWebSocketServer()
|
|
client := &Client{
|
|
ID: "test-client",
|
|
UserID: "user123",
|
|
send: make(chan []byte, 256),
|
|
subscriptions: make(map[string]bool),
|
|
authenticated: true,
|
|
lastActivity: time.Now(),
|
|
rateLimiter: NewRateLimiter(100, time.Minute),
|
|
}
|
|
|
|
msg := map[string]interface{}{
|
|
"type": "vault_deposit",
|
|
"vaultID": "vault123",
|
|
}
|
|
|
|
server.handleVaultDeposit(client, msg, "req123")
|
|
|
|
select {
|
|
case <-client.send:
|
|
// Error received
|
|
case <-time.After(time.Second):
|
|
t.Fatal("Expected error response")
|
|
}
|
|
}
|
|
|
|
func TestHandleVaultDeposit_InvalidAmountFormat(t *testing.T) {
|
|
server := newTestWebSocketServer()
|
|
client := &Client{
|
|
ID: "test-client",
|
|
UserID: "user123",
|
|
send: make(chan []byte, 256),
|
|
subscriptions: make(map[string]bool),
|
|
authenticated: true,
|
|
lastActivity: time.Now(),
|
|
rateLimiter: NewRateLimiter(100, time.Minute),
|
|
}
|
|
|
|
msg := map[string]interface{}{
|
|
"type": "vault_deposit",
|
|
"vaultID": "vault123",
|
|
"amount": "not-a-number",
|
|
}
|
|
|
|
server.handleVaultDeposit(client, msg, "req123")
|
|
|
|
select {
|
|
case <-client.send:
|
|
// Error received
|
|
case <-time.After(time.Second):
|
|
t.Fatal("Expected error response")
|
|
}
|
|
}
|
|
|
|
// --- handleGetBalances edge cases ---
|
|
|
|
func TestHandleGetBalances_NotAuthenticated(t *testing.T) {
|
|
server := newTestWebSocketServer()
|
|
client := &Client{
|
|
ID: "test-client",
|
|
send: make(chan []byte, 256),
|
|
subscriptions: make(map[string]bool),
|
|
authenticated: false,
|
|
lastActivity: time.Now(),
|
|
rateLimiter: NewRateLimiter(100, time.Minute),
|
|
}
|
|
|
|
server.handleGetBalances(client, "req123")
|
|
|
|
select {
|
|
case <-client.send:
|
|
// Error received
|
|
case <-time.After(time.Second):
|
|
t.Fatal("Expected error response")
|
|
}
|
|
}
|
|
|
|
// --- handleModifyLeverage edge cases ---
|
|
|
|
func TestHandleModifyLeverage_MissingPositionID(t *testing.T) {
|
|
server := newTestWebSocketServer()
|
|
client := &Client{
|
|
ID: "test-client",
|
|
UserID: "user123",
|
|
send: make(chan []byte, 256),
|
|
subscriptions: make(map[string]bool),
|
|
authenticated: true,
|
|
lastActivity: time.Now(),
|
|
rateLimiter: NewRateLimiter(100, time.Minute),
|
|
}
|
|
|
|
msg := map[string]interface{}{
|
|
"type": "modify_leverage",
|
|
"leverage": 10.0,
|
|
}
|
|
|
|
server.handleModifyLeverage(client, msg, "req123")
|
|
|
|
select {
|
|
case <-client.send:
|
|
// Error received
|
|
case <-time.After(time.Second):
|
|
t.Fatal("Expected error response")
|
|
}
|
|
}
|
|
|
|
func TestHandleModifyLeverage_MissingLeverage(t *testing.T) {
|
|
server := newTestWebSocketServer()
|
|
client := &Client{
|
|
ID: "test-client",
|
|
UserID: "user123",
|
|
send: make(chan []byte, 256),
|
|
subscriptions: make(map[string]bool),
|
|
authenticated: true,
|
|
lastActivity: time.Now(),
|
|
rateLimiter: NewRateLimiter(100, time.Minute),
|
|
}
|
|
|
|
msg := map[string]interface{}{
|
|
"type": "modify_leverage",
|
|
"position_id": "pos123",
|
|
}
|
|
|
|
server.handleModifyLeverage(client, msg, "req123")
|
|
|
|
select {
|
|
case <-client.send:
|
|
// Error received
|
|
case <-time.After(time.Second):
|
|
t.Fatal("Expected error response")
|
|
}
|
|
}
|
|
|
|
// --- handleVaultWithdraw edge cases ---
|
|
|
|
func TestHandleVaultWithdraw_MissingVaultID(t *testing.T) {
|
|
server := newTestWebSocketServer()
|
|
client := &Client{
|
|
ID: "test-client",
|
|
UserID: "user123",
|
|
send: make(chan []byte, 256),
|
|
subscriptions: make(map[string]bool),
|
|
authenticated: true,
|
|
lastActivity: time.Now(),
|
|
rateLimiter: NewRateLimiter(100, time.Minute),
|
|
}
|
|
|
|
msg := map[string]interface{}{
|
|
"type": "vault_withdraw",
|
|
"amount": "1000",
|
|
}
|
|
|
|
server.handleVaultWithdraw(client, msg, "req123")
|
|
|
|
select {
|
|
case <-client.send:
|
|
// Error received
|
|
case <-time.After(time.Second):
|
|
t.Fatal("Expected error response")
|
|
}
|
|
}
|
|
|
|
func TestHandleVaultWithdraw_MissingAmount(t *testing.T) {
|
|
server := newTestWebSocketServer()
|
|
client := &Client{
|
|
ID: "test-client",
|
|
UserID: "user123",
|
|
send: make(chan []byte, 256),
|
|
subscriptions: make(map[string]bool),
|
|
authenticated: true,
|
|
lastActivity: time.Now(),
|
|
rateLimiter: NewRateLimiter(100, time.Minute),
|
|
}
|
|
|
|
msg := map[string]interface{}{
|
|
"type": "vault_withdraw",
|
|
"vault_id": "vault123",
|
|
}
|
|
|
|
server.handleVaultWithdraw(client, msg, "req123")
|
|
|
|
select {
|
|
case <-client.send:
|
|
// Error received
|
|
case <-time.After(time.Second):
|
|
t.Fatal("Expected error response")
|
|
}
|
|
}
|
|
|
|
func TestHandleVaultWithdraw_InvalidAmountFormat(t *testing.T) {
|
|
server := newTestWebSocketServer()
|
|
client := &Client{
|
|
ID: "test-client",
|
|
UserID: "user123",
|
|
send: make(chan []byte, 256),
|
|
subscriptions: make(map[string]bool),
|
|
authenticated: true,
|
|
lastActivity: time.Now(),
|
|
rateLimiter: NewRateLimiter(100, time.Minute),
|
|
}
|
|
|
|
msg := map[string]interface{}{
|
|
"type": "vault_withdraw",
|
|
"vault_id": "vault123",
|
|
"amount": "not-a-number",
|
|
}
|
|
|
|
server.handleVaultWithdraw(client, msg, "req123")
|
|
|
|
select {
|
|
case <-client.send:
|
|
// Error received
|
|
case <-time.After(time.Second):
|
|
t.Fatal("Expected error response")
|
|
}
|
|
}
|
|
|
|
func TestHandleVaultWithdraw_ValidRequest(t *testing.T) {
|
|
server := newTestWebSocketServer()
|
|
client := &Client{
|
|
ID: "test-client",
|
|
UserID: "user123",
|
|
send: make(chan []byte, 256),
|
|
subscriptions: make(map[string]bool),
|
|
authenticated: true,
|
|
lastActivity: time.Now(),
|
|
rateLimiter: NewRateLimiter(100, time.Minute),
|
|
}
|
|
|
|
msg := map[string]interface{}{
|
|
"type": "vault_withdraw",
|
|
"vault_id": "vault123",
|
|
"amount": "1000000000",
|
|
}
|
|
|
|
server.handleVaultWithdraw(client, msg, "req123")
|
|
|
|
select {
|
|
case <-client.send:
|
|
// Response received
|
|
case <-time.After(time.Second):
|
|
t.Fatal("Expected response")
|
|
}
|
|
}
|
|
|
|
// --- handleLendingSupply edge cases ---
|
|
|
|
func TestHandleLendingSupply_MissingAsset(t *testing.T) {
|
|
server := newTestWebSocketServer()
|
|
client := &Client{
|
|
ID: "test-client",
|
|
UserID: "user123",
|
|
send: make(chan []byte, 256),
|
|
subscriptions: make(map[string]bool),
|
|
authenticated: true,
|
|
lastActivity: time.Now(),
|
|
rateLimiter: NewRateLimiter(100, time.Minute),
|
|
}
|
|
|
|
msg := map[string]interface{}{
|
|
"type": "lending_supply",
|
|
"amount": "1000",
|
|
}
|
|
|
|
server.handleLendingSupply(client, msg, "req123")
|
|
|
|
select {
|
|
case <-client.send:
|
|
// Error received
|
|
case <-time.After(time.Second):
|
|
t.Fatal("Expected error response")
|
|
}
|
|
}
|
|
|
|
func TestHandleLendingSupply_MissingAmount(t *testing.T) {
|
|
server := newTestWebSocketServer()
|
|
client := &Client{
|
|
ID: "test-client",
|
|
UserID: "user123",
|
|
send: make(chan []byte, 256),
|
|
subscriptions: make(map[string]bool),
|
|
authenticated: true,
|
|
lastActivity: time.Now(),
|
|
rateLimiter: NewRateLimiter(100, time.Minute),
|
|
}
|
|
|
|
msg := map[string]interface{}{
|
|
"type": "lending_supply",
|
|
"asset": "USDC",
|
|
}
|
|
|
|
server.handleLendingSupply(client, msg, "req123")
|
|
|
|
select {
|
|
case <-client.send:
|
|
// Error received
|
|
case <-time.After(time.Second):
|
|
t.Fatal("Expected error response")
|
|
}
|
|
}
|
|
|
|
func TestHandleLendingSupply_InvalidAmountFormat(t *testing.T) {
|
|
server := newTestWebSocketServer()
|
|
client := &Client{
|
|
ID: "test-client",
|
|
UserID: "user123",
|
|
send: make(chan []byte, 256),
|
|
subscriptions: make(map[string]bool),
|
|
authenticated: true,
|
|
lastActivity: time.Now(),
|
|
rateLimiter: NewRateLimiter(100, time.Minute),
|
|
}
|
|
|
|
msg := map[string]interface{}{
|
|
"type": "lending_supply",
|
|
"asset": "USDC",
|
|
"amount": "invalid",
|
|
}
|
|
|
|
server.handleLendingSupply(client, msg, "req123")
|
|
|
|
select {
|
|
case <-client.send:
|
|
// Error received
|
|
case <-time.After(time.Second):
|
|
t.Fatal("Expected error response")
|
|
}
|
|
}
|
|
|
|
// --- handleLendingBorrow edge cases ---
|
|
|
|
func TestHandleLendingBorrow_MissingAsset(t *testing.T) {
|
|
server := newTestWebSocketServer()
|
|
client := &Client{
|
|
ID: "test-client",
|
|
UserID: "user123",
|
|
send: make(chan []byte, 256),
|
|
subscriptions: make(map[string]bool),
|
|
authenticated: true,
|
|
lastActivity: time.Now(),
|
|
rateLimiter: NewRateLimiter(100, time.Minute),
|
|
}
|
|
|
|
msg := map[string]interface{}{
|
|
"type": "lending_borrow",
|
|
"amount": "1000",
|
|
}
|
|
|
|
server.handleLendingBorrow(client, msg, "req123")
|
|
|
|
select {
|
|
case <-client.send:
|
|
// Error received
|
|
case <-time.After(time.Second):
|
|
t.Fatal("Expected error response")
|
|
}
|
|
}
|
|
|
|
func TestHandleLendingBorrow_MissingAmount(t *testing.T) {
|
|
server := newTestWebSocketServer()
|
|
client := &Client{
|
|
ID: "test-client",
|
|
UserID: "user123",
|
|
send: make(chan []byte, 256),
|
|
subscriptions: make(map[string]bool),
|
|
authenticated: true,
|
|
lastActivity: time.Now(),
|
|
rateLimiter: NewRateLimiter(100, time.Minute),
|
|
}
|
|
|
|
msg := map[string]interface{}{
|
|
"type": "lending_borrow",
|
|
"asset": "USDC",
|
|
}
|
|
|
|
server.handleLendingBorrow(client, msg, "req123")
|
|
|
|
select {
|
|
case <-client.send:
|
|
// Error received
|
|
case <-time.After(time.Second):
|
|
t.Fatal("Expected error response")
|
|
}
|
|
}
|
|
|
|
func TestHandleLendingBorrow_InvalidAmountFormat(t *testing.T) {
|
|
server := newTestWebSocketServer()
|
|
client := &Client{
|
|
ID: "test-client",
|
|
UserID: "user123",
|
|
send: make(chan []byte, 256),
|
|
subscriptions: make(map[string]bool),
|
|
authenticated: true,
|
|
lastActivity: time.Now(),
|
|
rateLimiter: NewRateLimiter(100, time.Minute),
|
|
}
|
|
|
|
msg := map[string]interface{}{
|
|
"type": "lending_borrow",
|
|
"asset": "USDC",
|
|
"amount": "invalid",
|
|
}
|
|
|
|
server.handleLendingBorrow(client, msg, "req123")
|
|
|
|
select {
|
|
case <-client.send:
|
|
// Error received
|
|
case <-time.After(time.Second):
|
|
t.Fatal("Expected error response")
|
|
}
|
|
}
|
|
|
|
// --- handleLendingRepay edge cases ---
|
|
|
|
func TestHandleLendingRepay_MissingAsset(t *testing.T) {
|
|
server := newTestWebSocketServer()
|
|
client := &Client{
|
|
ID: "test-client",
|
|
UserID: "user123",
|
|
send: make(chan []byte, 256),
|
|
subscriptions: make(map[string]bool),
|
|
authenticated: true,
|
|
lastActivity: time.Now(),
|
|
rateLimiter: NewRateLimiter(100, time.Minute),
|
|
}
|
|
|
|
msg := map[string]interface{}{
|
|
"type": "lending_repay",
|
|
"amount": "1000",
|
|
"interest": "10",
|
|
}
|
|
|
|
server.handleLendingRepay(client, msg, "req123")
|
|
|
|
select {
|
|
case <-client.send:
|
|
// Error received
|
|
case <-time.After(time.Second):
|
|
t.Fatal("Expected error response")
|
|
}
|
|
}
|
|
|
|
func TestHandleLendingRepay_MissingAmount(t *testing.T) {
|
|
server := newTestWebSocketServer()
|
|
client := &Client{
|
|
ID: "test-client",
|
|
UserID: "user123",
|
|
send: make(chan []byte, 256),
|
|
subscriptions: make(map[string]bool),
|
|
authenticated: true,
|
|
lastActivity: time.Now(),
|
|
rateLimiter: NewRateLimiter(100, time.Minute),
|
|
}
|
|
|
|
msg := map[string]interface{}{
|
|
"type": "lending_repay",
|
|
"asset": "USDC",
|
|
"interest": "10",
|
|
}
|
|
|
|
server.handleLendingRepay(client, msg, "req123")
|
|
|
|
select {
|
|
case <-client.send:
|
|
// Error received
|
|
case <-time.After(time.Second):
|
|
t.Fatal("Expected error response")
|
|
}
|
|
}
|
|
|
|
func TestHandleLendingRepay_MissingInterest(t *testing.T) {
|
|
server := newTestWebSocketServer()
|
|
client := &Client{
|
|
ID: "test-client",
|
|
UserID: "user123",
|
|
send: make(chan []byte, 256),
|
|
subscriptions: make(map[string]bool),
|
|
authenticated: true,
|
|
lastActivity: time.Now(),
|
|
rateLimiter: NewRateLimiter(100, time.Minute),
|
|
}
|
|
|
|
msg := map[string]interface{}{
|
|
"type": "lending_repay",
|
|
"asset": "USDC",
|
|
"amount": "1000",
|
|
}
|
|
|
|
server.handleLendingRepay(client, msg, "req123")
|
|
|
|
select {
|
|
case <-client.send:
|
|
// Error received
|
|
case <-time.After(time.Second):
|
|
t.Fatal("Expected error response")
|
|
}
|
|
}
|
|
|
|
func TestHandleLendingRepay_InvalidAmountFormat(t *testing.T) {
|
|
server := newTestWebSocketServer()
|
|
client := &Client{
|
|
ID: "test-client",
|
|
UserID: "user123",
|
|
send: make(chan []byte, 256),
|
|
subscriptions: make(map[string]bool),
|
|
authenticated: true,
|
|
lastActivity: time.Now(),
|
|
rateLimiter: NewRateLimiter(100, time.Minute),
|
|
}
|
|
|
|
msg := map[string]interface{}{
|
|
"type": "lending_repay",
|
|
"asset": "USDC",
|
|
"amount": "invalid",
|
|
"interest": "10",
|
|
}
|
|
|
|
server.handleLendingRepay(client, msg, "req123")
|
|
|
|
select {
|
|
case <-client.send:
|
|
// Error received
|
|
case <-time.After(time.Second):
|
|
t.Fatal("Expected error response")
|
|
}
|
|
}
|
|
|
|
func TestHandleLendingRepay_InvalidInterestFormat(t *testing.T) {
|
|
server := newTestWebSocketServer()
|
|
client := &Client{
|
|
ID: "test-client",
|
|
UserID: "user123",
|
|
send: make(chan []byte, 256),
|
|
subscriptions: make(map[string]bool),
|
|
authenticated: true,
|
|
lastActivity: time.Now(),
|
|
rateLimiter: NewRateLimiter(100, time.Minute),
|
|
}
|
|
|
|
msg := map[string]interface{}{
|
|
"type": "lending_repay",
|
|
"asset": "USDC",
|
|
"amount": "1000",
|
|
"interest": "invalid",
|
|
}
|
|
|
|
server.handleLendingRepay(client, msg, "req123")
|
|
|
|
select {
|
|
case <-client.send:
|
|
// Error received
|
|
case <-time.After(time.Second):
|
|
t.Fatal("Expected error response")
|
|
}
|
|
}
|
|
|
|
func TestHandleLendingRepay_ValidRequest(t *testing.T) {
|
|
server := newTestWebSocketServer()
|
|
client := &Client{
|
|
ID: "test-client",
|
|
UserID: "user123",
|
|
send: make(chan []byte, 256),
|
|
subscriptions: make(map[string]bool),
|
|
authenticated: true,
|
|
lastActivity: time.Now(),
|
|
rateLimiter: NewRateLimiter(100, time.Minute),
|
|
}
|
|
|
|
msg := map[string]interface{}{
|
|
"type": "lending_repay",
|
|
"asset": "USDC",
|
|
"amount": "1000000000",
|
|
"interest": "10000000",
|
|
}
|
|
|
|
server.handleLendingRepay(client, msg, "req123")
|
|
|
|
select {
|
|
case <-client.send:
|
|
// Response received
|
|
case <-time.After(time.Second):
|
|
t.Fatal("Expected response")
|
|
}
|
|
}
|
|
|
|
// --- handleModifyOrder edge cases ---
|
|
|
|
func TestHandleModifyOrderVariants(t *testing.T) {
|
|
server := newTestWebSocketServer()
|
|
client := &Client{
|
|
ID: "test-client",
|
|
UserID: "user123",
|
|
send: make(chan []byte, 256),
|
|
subscriptions: make(map[string]bool),
|
|
authenticated: true,
|
|
lastActivity: time.Now(),
|
|
rateLimiter: NewRateLimiter(100, time.Minute),
|
|
}
|
|
|
|
testCases := []struct {
|
|
name string
|
|
msg map[string]interface{}
|
|
}{
|
|
{
|
|
name: "with orderID and newPrice/newSize",
|
|
msg: map[string]interface{}{
|
|
"type": "modify_order",
|
|
"orderID": float64(123),
|
|
"newPrice": 50000.0,
|
|
"newSize": 2.0,
|
|
},
|
|
},
|
|
{
|
|
name: "with order_id and price/size",
|
|
msg: map[string]interface{}{
|
|
"type": "modify_order",
|
|
"order_id": float64(456),
|
|
"price": 51000.0,
|
|
"size": 3.0,
|
|
},
|
|
},
|
|
{
|
|
name: "with only orderID",
|
|
msg: map[string]interface{}{
|
|
"type": "modify_order",
|
|
"orderID": float64(789),
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
server.handleModifyOrder(client, tc.msg, "req123")
|
|
|
|
select {
|
|
case <-client.send:
|
|
// Response received
|
|
case <-time.After(time.Second):
|
|
t.Fatal("Expected response")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// --- notifyLiquidation edge cases ---
|
|
|
|
func TestNotifyLiquidation_ClientFound_Coverage(t *testing.T) {
|
|
server := newTestWebSocketServer()
|
|
client := &Client{
|
|
ID: "test-client",
|
|
UserID: "user123",
|
|
send: make(chan []byte, 256),
|
|
subscriptions: make(map[string]bool),
|
|
authenticated: true,
|
|
lastActivity: time.Now(),
|
|
rateLimiter: NewRateLimiter(100, time.Minute),
|
|
}
|
|
|
|
// Register client
|
|
server.mu.Lock()
|
|
server.clients[client.ID] = client
|
|
server.mu.Unlock()
|
|
|
|
position := &dex.MarginPosition{
|
|
ID: "pos123",
|
|
Symbol: "BTC/USD",
|
|
Side: dex.Buy,
|
|
Size: 1.0,
|
|
EntryPrice: 50000,
|
|
MarkPrice: 45000,
|
|
Leverage: 10,
|
|
}
|
|
|
|
server.notifyLiquidation("user123", position)
|
|
|
|
// Should receive notification
|
|
select {
|
|
case <-client.send:
|
|
// Received
|
|
case <-time.After(time.Second):
|
|
t.Fatal("Expected notification")
|
|
}
|
|
}
|
|
|
|
func TestNotifyLiquidation_SellSide_Coverage(t *testing.T) {
|
|
server := newTestWebSocketServer()
|
|
client := &Client{
|
|
ID: "test-client",
|
|
UserID: "user123",
|
|
send: make(chan []byte, 256),
|
|
subscriptions: make(map[string]bool),
|
|
authenticated: true,
|
|
lastActivity: time.Now(),
|
|
rateLimiter: NewRateLimiter(100, time.Minute),
|
|
}
|
|
|
|
// Register client
|
|
server.mu.Lock()
|
|
server.clients[client.ID] = client
|
|
server.mu.Unlock()
|
|
|
|
position := &dex.MarginPosition{
|
|
ID: "pos123",
|
|
Symbol: "BTC/USD",
|
|
Side: dex.Sell,
|
|
Size: 1.0,
|
|
EntryPrice: 45000,
|
|
MarkPrice: 50000,
|
|
Leverage: 10,
|
|
}
|
|
|
|
server.notifyLiquidation("user123", position)
|
|
|
|
// Should receive notification
|
|
select {
|
|
case <-client.send:
|
|
// Received
|
|
case <-time.After(time.Second):
|
|
t.Fatal("Expected notification")
|
|
}
|
|
}
|
|
|
|
func TestNotifyLiquidation_ClientNotFound_Coverage(t *testing.T) {
|
|
server := newTestWebSocketServer()
|
|
|
|
position := &dex.MarginPosition{
|
|
ID: "pos123",
|
|
Symbol: "BTC/USD",
|
|
Side: dex.Buy,
|
|
Size: 1.0,
|
|
}
|
|
|
|
// Should not panic when client not found
|
|
server.notifyLiquidation("unknown_user", position)
|
|
}
|
|
|
|
// --- broadcastToSubscribers edge cases ---
|
|
|
|
func TestBroadcastToSubscribers_NoClients(t *testing.T) {
|
|
server := newTestWebSocketServer()
|
|
|
|
msg := Message{Type: "test", Timestamp: time.Now().Unix()}
|
|
|
|
// Should not panic
|
|
server.broadcastToSubscribers("test:channel", msg)
|
|
}
|
|
|
|
func TestBroadcastToSubscribers_FullChannel(t *testing.T) {
|
|
server := newTestWebSocketServer()
|
|
client := &Client{
|
|
ID: "test-client",
|
|
UserID: "user123",
|
|
send: make(chan []byte, 1), // Tiny buffer
|
|
subscriptions: map[string]bool{"test:channel": true},
|
|
authenticated: true,
|
|
lastActivity: time.Now(),
|
|
rateLimiter: NewRateLimiter(100, time.Minute),
|
|
}
|
|
|
|
server.mu.Lock()
|
|
server.clients[client.ID] = client
|
|
server.mu.Unlock()
|
|
|
|
// Fill the channel
|
|
client.send <- []byte("existing")
|
|
|
|
msg := Message{Type: "test", Timestamp: time.Now().Unix()}
|
|
|
|
// Should not block
|
|
server.broadcastToSubscribers("test:channel", msg)
|
|
|
|
// Drain
|
|
<-client.send
|
|
}
|
|
|
|
// --- Concurrent access tests ---
|
|
|
|
func TestIPRateLimiter_ConcurrentAccess(t *testing.T) {
|
|
rl := NewIPRateLimiter(1000, time.Second)
|
|
var wg sync.WaitGroup
|
|
|
|
for i := 0; i < 100; i++ {
|
|
wg.Add(1)
|
|
go func(ip int) {
|
|
defer wg.Done()
|
|
ipStr := fmt.Sprintf("192.168.1.%d", ip%256)
|
|
for j := 0; j < 10; j++ {
|
|
rl.Allow(ipStr)
|
|
}
|
|
}(i)
|
|
}
|
|
|
|
wg.Wait()
|
|
}
|
|
|
|
func TestServerMetrics_ConcurrentSnapshot(t *testing.T) {
|
|
metrics := NewServerMetrics()
|
|
var wg sync.WaitGroup
|
|
|
|
// Concurrent writes
|
|
for i := 0; i < 50; i++ {
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
metrics.mu.Lock()
|
|
metrics.ConnectionsTotal++
|
|
metrics.MessagesReceived++
|
|
metrics.mu.Unlock()
|
|
}()
|
|
}
|
|
|
|
// Concurrent reads
|
|
for i := 0; i < 50; i++ {
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
_ = metrics.GetSnapshot()
|
|
}()
|
|
}
|
|
|
|
wg.Wait()
|
|
}
|
|
|
|
// --- processMessage comprehensive coverage ---
|
|
|
|
func TestProcessMessage_InvalidType(t *testing.T) {
|
|
server := newTestWebSocketServer()
|
|
client := &Client{
|
|
ID: "test-client",
|
|
UserID: "user123",
|
|
send: make(chan []byte, 256),
|
|
subscriptions: make(map[string]bool),
|
|
authenticated: true,
|
|
lastActivity: time.Now(),
|
|
rateLimiter: NewRateLimiter(100, time.Minute),
|
|
}
|
|
|
|
// Message without type string (number instead)
|
|
msg := map[string]interface{}{
|
|
"type": 123, // Not a string
|
|
}
|
|
|
|
server.processMessage(client, msg)
|
|
|
|
select {
|
|
case <-client.send:
|
|
// Error received
|
|
case <-time.After(time.Second):
|
|
t.Fatal("Expected error response")
|
|
}
|
|
}
|
|
|
|
func TestProcessMessage_UnknownType(t *testing.T) {
|
|
server := newTestWebSocketServer()
|
|
client := &Client{
|
|
ID: "test-client",
|
|
UserID: "user123",
|
|
send: make(chan []byte, 256),
|
|
subscriptions: make(map[string]bool),
|
|
authenticated: true,
|
|
lastActivity: time.Now(),
|
|
rateLimiter: NewRateLimiter(100, time.Minute),
|
|
}
|
|
|
|
msg := map[string]interface{}{
|
|
"type": "unknown_message_type",
|
|
}
|
|
|
|
server.processMessage(client, msg)
|
|
|
|
select {
|
|
case <-client.send:
|
|
// Error received
|
|
case <-time.After(time.Second):
|
|
t.Fatal("Expected error response")
|
|
}
|
|
}
|
|
|
|
func TestProcessMessage_GetPositions(t *testing.T) {
|
|
server := newTestWebSocketServer()
|
|
client := &Client{
|
|
ID: "test-client",
|
|
UserID: "user123",
|
|
send: make(chan []byte, 256),
|
|
subscriptions: make(map[string]bool),
|
|
authenticated: true,
|
|
lastActivity: time.Now(),
|
|
rateLimiter: NewRateLimiter(100, time.Minute),
|
|
}
|
|
|
|
msg := map[string]interface{}{
|
|
"type": "get_positions",
|
|
"request_id": "req-get-pos",
|
|
}
|
|
|
|
server.processMessage(client, msg)
|
|
|
|
select {
|
|
case <-client.send:
|
|
// Response received
|
|
case <-time.After(time.Second):
|
|
t.Fatal("Expected response")
|
|
}
|
|
}
|
|
|
|
func TestProcessMessage_GetOrders(t *testing.T) {
|
|
server := newTestWebSocketServer()
|
|
client := &Client{
|
|
ID: "test-client",
|
|
UserID: "user123",
|
|
send: make(chan []byte, 256),
|
|
subscriptions: make(map[string]bool),
|
|
authenticated: true,
|
|
lastActivity: time.Now(),
|
|
rateLimiter: NewRateLimiter(100, time.Minute),
|
|
}
|
|
|
|
msg := map[string]interface{}{
|
|
"type": "get_orders",
|
|
"request_id": "req-get-orders",
|
|
}
|
|
|
|
server.processMessage(client, msg)
|
|
|
|
select {
|
|
case <-client.send:
|
|
// Response received
|
|
case <-time.After(time.Second):
|
|
t.Fatal("Expected response")
|
|
}
|
|
}
|
|
|
|
// --- handleClosePosition valid path ---
|
|
|
|
func TestHandleClosePosition_ValidPath(t *testing.T) {
|
|
server := newTestWebSocketServer()
|
|
client := &Client{
|
|
ID: "test-client",
|
|
UserID: "user123",
|
|
send: make(chan []byte, 256),
|
|
subscriptions: make(map[string]bool),
|
|
authenticated: true,
|
|
lastActivity: time.Now(),
|
|
rateLimiter: NewRateLimiter(100, time.Minute),
|
|
}
|
|
|
|
msg := map[string]interface{}{
|
|
"type": "close_position",
|
|
"positionID": "position-123",
|
|
"size": 0.5,
|
|
}
|
|
|
|
server.handleClosePosition(client, msg, "req123")
|
|
|
|
select {
|
|
case <-client.send:
|
|
// Response received (could be error if position not found, but still tests the path)
|
|
case <-time.After(time.Second):
|
|
t.Fatal("Expected response")
|
|
}
|
|
}
|
|
|
|
// --- handleVaultDeposit valid path ---
|
|
|
|
func TestHandleVaultDeposit_ValidPath(t *testing.T) {
|
|
server := newTestWebSocketServer()
|
|
client := &Client{
|
|
ID: "test-client",
|
|
UserID: "user123",
|
|
send: make(chan []byte, 256),
|
|
subscriptions: make(map[string]bool),
|
|
authenticated: true,
|
|
lastActivity: time.Now(),
|
|
rateLimiter: NewRateLimiter(100, time.Minute),
|
|
}
|
|
|
|
msg := map[string]interface{}{
|
|
"type": "vault_deposit",
|
|
"vaultID": "vault123",
|
|
"amount": "1000000000",
|
|
}
|
|
|
|
server.handleVaultDeposit(client, msg, "req123")
|
|
|
|
select {
|
|
case <-client.send:
|
|
// Response received
|
|
case <-time.After(time.Second):
|
|
t.Fatal("Expected response")
|
|
}
|
|
}
|
|
|
|
// --- handleGetBalances valid path ---
|
|
|
|
func TestHandleGetBalances_ValidPath(t *testing.T) {
|
|
server := newTestWebSocketServer()
|
|
client := &Client{
|
|
ID: "test-client",
|
|
UserID: "user123",
|
|
send: make(chan []byte, 256),
|
|
subscriptions: make(map[string]bool),
|
|
authenticated: true,
|
|
lastActivity: time.Now(),
|
|
rateLimiter: NewRateLimiter(100, time.Minute),
|
|
}
|
|
|
|
server.handleGetBalances(client, "req123")
|
|
|
|
select {
|
|
case <-client.send:
|
|
// Response received
|
|
case <-time.After(time.Second):
|
|
t.Fatal("Expected response")
|
|
}
|
|
}
|
|
|
|
// --- handleModifyLeverage valid path ---
|
|
|
|
func TestHandleModifyLeverage_ValidPath(t *testing.T) {
|
|
server := newTestWebSocketServer()
|
|
client := &Client{
|
|
ID: "test-client",
|
|
UserID: "user123",
|
|
send: make(chan []byte, 256),
|
|
subscriptions: make(map[string]bool),
|
|
authenticated: true,
|
|
lastActivity: time.Now(),
|
|
rateLimiter: NewRateLimiter(100, time.Minute),
|
|
}
|
|
|
|
msg := map[string]interface{}{
|
|
"type": "modify_leverage",
|
|
"position_id": "pos123",
|
|
"leverage": 15.0,
|
|
}
|
|
|
|
server.handleModifyLeverage(client, msg, "req123")
|
|
|
|
select {
|
|
case <-client.send:
|
|
// Response received (could be error if position not found, but still tests the path)
|
|
case <-time.After(time.Second):
|
|
t.Fatal("Expected response")
|
|
}
|
|
}
|
|
|
|
// --- handleAuth edge cases ---
|
|
|
|
func TestHandleAuth_MissingCredentials(t *testing.T) {
|
|
server := newTestWebSocketServer()
|
|
client := &Client{
|
|
ID: "test-client",
|
|
send: make(chan []byte, 256),
|
|
subscriptions: make(map[string]bool),
|
|
authenticated: false,
|
|
lastActivity: time.Now(),
|
|
rateLimiter: NewRateLimiter(100, time.Minute),
|
|
}
|
|
|
|
testCases := []struct {
|
|
name string
|
|
msg map[string]interface{}
|
|
}{
|
|
{
|
|
name: "missing_api_key",
|
|
msg: map[string]interface{}{
|
|
"apiSecret": "secret",
|
|
},
|
|
},
|
|
{
|
|
name: "missing_api_secret",
|
|
msg: map[string]interface{}{
|
|
"apiKey": "key",
|
|
},
|
|
},
|
|
{
|
|
name: "empty_credentials",
|
|
msg: map[string]interface{}{
|
|
"apiKey": "",
|
|
"apiSecret": "",
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
server.handleAuth(client, tc.msg, "auth-req")
|
|
|
|
select {
|
|
case <-client.send:
|
|
// Error received
|
|
case <-time.After(time.Second):
|
|
t.Fatal("Expected error response")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// --- handleSubscribe/handleUnsubscribe tests ---
|
|
|
|
func TestHandleSubscribe_ValidChannel_Coverage(t *testing.T) {
|
|
server := newTestWebSocketServer()
|
|
client := &Client{
|
|
ID: "test-client",
|
|
UserID: "user123",
|
|
send: make(chan []byte, 256),
|
|
subscriptions: make(map[string]bool),
|
|
authenticated: true,
|
|
lastActivity: time.Now(),
|
|
rateLimiter: NewRateLimiter(100, time.Minute),
|
|
}
|
|
|
|
msg := map[string]interface{}{
|
|
"type": "subscribe",
|
|
"channel": "orderbook:BTC/USD",
|
|
}
|
|
|
|
server.handleSubscribe(client, msg, "req123")
|
|
|
|
select {
|
|
case <-client.send:
|
|
// Response received
|
|
case <-time.After(time.Second):
|
|
t.Fatal("Expected response")
|
|
}
|
|
}
|
|
|
|
func TestHandleUnsubscribe_ValidChannel_Coverage(t *testing.T) {
|
|
server := newTestWebSocketServer()
|
|
client := &Client{
|
|
ID: "test-client",
|
|
UserID: "user123",
|
|
send: make(chan []byte, 256),
|
|
subscriptions: map[string]bool{"orderbook:BTC/USD": true},
|
|
authenticated: true,
|
|
lastActivity: time.Now(),
|
|
rateLimiter: NewRateLimiter(100, time.Minute),
|
|
}
|
|
|
|
msg := map[string]interface{}{
|
|
"type": "unsubscribe",
|
|
"channel": "orderbook:BTC/USD",
|
|
}
|
|
|
|
server.handleUnsubscribe(client, msg, "req123")
|
|
|
|
select {
|
|
case <-client.send:
|
|
// Response received
|
|
case <-time.After(time.Second):
|
|
t.Fatal("Expected response")
|
|
}
|
|
}
|
|
|
|
func TestHandleSubscribe_MissingChannel(t *testing.T) {
|
|
server := newTestWebSocketServer()
|
|
client := &Client{
|
|
ID: "test-client",
|
|
UserID: "user123",
|
|
send: make(chan []byte, 256),
|
|
subscriptions: make(map[string]bool),
|
|
authenticated: true,
|
|
lastActivity: time.Now(),
|
|
rateLimiter: NewRateLimiter(100, time.Minute),
|
|
}
|
|
|
|
msg := map[string]interface{}{
|
|
"type": "subscribe",
|
|
}
|
|
|
|
server.handleSubscribe(client, msg, "req123")
|
|
|
|
select {
|
|
case <-client.send:
|
|
// Error received
|
|
case <-time.After(time.Second):
|
|
t.Fatal("Expected error response")
|
|
}
|
|
}
|
|
|
|
func TestHandleUnsubscribe_MissingChannel(t *testing.T) {
|
|
server := newTestWebSocketServer()
|
|
client := &Client{
|
|
ID: "test-client",
|
|
UserID: "user123",
|
|
send: make(chan []byte, 256),
|
|
subscriptions: make(map[string]bool),
|
|
authenticated: true,
|
|
lastActivity: time.Now(),
|
|
rateLimiter: NewRateLimiter(100, time.Minute),
|
|
}
|
|
|
|
msg := map[string]interface{}{
|
|
"type": "unsubscribe",
|
|
}
|
|
|
|
server.handleUnsubscribe(client, msg, "req123")
|
|
|
|
select {
|
|
case <-client.send:
|
|
// Error received
|
|
case <-time.After(time.Second):
|
|
t.Fatal("Expected error response")
|
|
}
|
|
}
|
|
|
|
// --- handlePing test ---
|
|
|
|
func TestHandlePing_Response(t *testing.T) {
|
|
server := newTestWebSocketServer()
|
|
client := &Client{
|
|
ID: "test-client",
|
|
send: make(chan []byte, 256),
|
|
subscriptions: make(map[string]bool),
|
|
authenticated: false,
|
|
lastActivity: time.Now(),
|
|
rateLimiter: NewRateLimiter(100, time.Minute),
|
|
}
|
|
|
|
server.handlePing(client, "ping-req")
|
|
|
|
select {
|
|
case <-client.send:
|
|
// Pong received
|
|
case <-time.After(time.Second):
|
|
t.Fatal("Expected pong response")
|
|
}
|
|
}
|
|
|
|
// --- handleOpenPosition valid path ---
|
|
|
|
func TestHandleOpenPosition_ValidPath(t *testing.T) {
|
|
server := newTestWebSocketServer()
|
|
client := &Client{
|
|
ID: "test-client",
|
|
UserID: "user123",
|
|
send: make(chan []byte, 256),
|
|
subscriptions: make(map[string]bool),
|
|
authenticated: true,
|
|
lastActivity: time.Now(),
|
|
rateLimiter: NewRateLimiter(100, time.Minute),
|
|
}
|
|
|
|
msg := map[string]interface{}{
|
|
"type": "open_position",
|
|
"symbol": "BTC/USD",
|
|
"side": "buy",
|
|
"size": 1.0,
|
|
"leverage": 10.0,
|
|
}
|
|
|
|
server.handleOpenPosition(client, msg, "req123")
|
|
|
|
select {
|
|
case <-client.send:
|
|
// Response received
|
|
case <-time.After(time.Second):
|
|
t.Fatal("Expected response")
|
|
}
|
|
}
|
|
|
|
// --- handlePlaceOrder valid path ---
|
|
|
|
func TestHandlePlaceOrder_ValidPath(t *testing.T) {
|
|
server := newTestWebSocketServer()
|
|
client := &Client{
|
|
ID: "test-client",
|
|
UserID: "user123",
|
|
send: make(chan []byte, 256),
|
|
subscriptions: make(map[string]bool),
|
|
authenticated: true,
|
|
lastActivity: time.Now(),
|
|
rateLimiter: NewRateLimiter(100, time.Minute),
|
|
}
|
|
|
|
msg := map[string]interface{}{
|
|
"type": "place_order",
|
|
"order": map[string]interface{}{
|
|
"symbol": "BTC/USD",
|
|
"side": "buy",
|
|
"type": "limit",
|
|
"price": 50000.0,
|
|
"size": 1.0,
|
|
},
|
|
}
|
|
|
|
server.handlePlaceOrder(client, msg, "req123")
|
|
|
|
select {
|
|
case <-client.send:
|
|
// Response received
|
|
case <-time.After(time.Second):
|
|
t.Fatal("Expected response")
|
|
}
|
|
}
|
|
|
|
// --- handleCancelOrder valid path ---
|
|
|
|
func TestHandleCancelOrder_ValidPath(t *testing.T) {
|
|
server := newTestWebSocketServer()
|
|
client := &Client{
|
|
ID: "test-client",
|
|
UserID: "user123",
|
|
send: make(chan []byte, 256),
|
|
subscriptions: make(map[string]bool),
|
|
authenticated: true,
|
|
lastActivity: time.Now(),
|
|
rateLimiter: NewRateLimiter(100, time.Minute),
|
|
}
|
|
|
|
msg := map[string]interface{}{
|
|
"type": "cancel_order",
|
|
"orderID": float64(12345),
|
|
}
|
|
|
|
server.handleCancelOrder(client, msg, "req123")
|
|
|
|
select {
|
|
case <-client.send:
|
|
// Response received (could be error if order not found, but still tests the path)
|
|
case <-time.After(time.Second):
|
|
t.Fatal("Expected response")
|
|
}
|
|
}
|
|
|
|
// --- handleLendingSupply valid path ---
|
|
|
|
func TestHandleLendingSupply_ValidPath(t *testing.T) {
|
|
server := newTestWebSocketServer()
|
|
client := &Client{
|
|
ID: "test-client",
|
|
UserID: "user123",
|
|
send: make(chan []byte, 256),
|
|
subscriptions: make(map[string]bool),
|
|
authenticated: true,
|
|
lastActivity: time.Now(),
|
|
rateLimiter: NewRateLimiter(100, time.Minute),
|
|
}
|
|
|
|
msg := map[string]interface{}{
|
|
"type": "lending_supply",
|
|
"asset": "USDC",
|
|
"amount": "1000000000",
|
|
}
|
|
|
|
server.handleLendingSupply(client, msg, "req123")
|
|
|
|
select {
|
|
case <-client.send:
|
|
// Response received
|
|
case <-time.After(time.Second):
|
|
t.Fatal("Expected response")
|
|
}
|
|
}
|
|
|
|
// --- handleLendingBorrow valid path ---
|
|
|
|
func TestHandleLendingBorrow_ValidPath(t *testing.T) {
|
|
server := newTestWebSocketServer()
|
|
client := &Client{
|
|
ID: "test-client",
|
|
UserID: "user123",
|
|
send: make(chan []byte, 256),
|
|
subscriptions: make(map[string]bool),
|
|
authenticated: true,
|
|
lastActivity: time.Now(),
|
|
rateLimiter: NewRateLimiter(100, time.Minute),
|
|
}
|
|
|
|
msg := map[string]interface{}{
|
|
"type": "lending_borrow",
|
|
"asset": "USDC",
|
|
"amount": "500000000",
|
|
}
|
|
|
|
server.handleLendingBorrow(client, msg, "req123")
|
|
|
|
select {
|
|
case <-client.send:
|
|
// Response received
|
|
case <-time.After(time.Second):
|
|
t.Fatal("Expected response")
|
|
}
|
|
}
|
|
|
|
// --- sendInitialData test ---
|
|
|
|
func TestSendInitialData(t *testing.T) {
|
|
server := newTestWebSocketServer()
|
|
client := &Client{
|
|
ID: "test-client",
|
|
UserID: "user123",
|
|
send: make(chan []byte, 256),
|
|
subscriptions: make(map[string]bool),
|
|
authenticated: true,
|
|
lastActivity: time.Now(),
|
|
rateLimiter: NewRateLimiter(100, time.Minute),
|
|
}
|
|
|
|
server.sendInitialData(client)
|
|
|
|
select {
|
|
case <-client.send:
|
|
// Initial data received
|
|
case <-time.After(time.Second):
|
|
t.Fatal("Expected initial data")
|
|
}
|
|
}
|
|
|
|
// --- removeClient test ---
|
|
|
|
func TestRemoveClient_Coverage(t *testing.T) {
|
|
server := newTestWebSocketServer()
|
|
client := &Client{
|
|
ID: "test-client",
|
|
UserID: "user123",
|
|
send: make(chan []byte, 256),
|
|
subscriptions: make(map[string]bool),
|
|
authenticated: true,
|
|
lastActivity: time.Now(),
|
|
rateLimiter: NewRateLimiter(100, time.Minute),
|
|
}
|
|
|
|
// Add client first
|
|
server.mu.Lock()
|
|
server.clients[client.ID] = client
|
|
server.metrics.ConnectionsActive = 1
|
|
server.mu.Unlock()
|
|
|
|
// Remove client
|
|
server.removeClient(client)
|
|
|
|
// Verify removed
|
|
server.mu.RLock()
|
|
_, exists := server.clients[client.ID]
|
|
server.mu.RUnlock()
|
|
|
|
assert.False(t, exists)
|
|
}
|
|
|
|
// --- RateLimiter tests ---
|
|
|
|
func TestRateLimiter_Allow(t *testing.T) {
|
|
rl := NewRateLimiter(5, time.Second)
|
|
|
|
// First 5 should pass
|
|
for i := 0; i < 5; i++ {
|
|
assert.True(t, rl.Allow())
|
|
}
|
|
|
|
// 6th should fail
|
|
assert.False(t, rl.Allow())
|
|
}
|
|
|
|
func TestRateLimiter_WindowReset(t *testing.T) {
|
|
rl := NewRateLimiter(2, 50*time.Millisecond)
|
|
|
|
assert.True(t, rl.Allow())
|
|
assert.True(t, rl.Allow())
|
|
assert.False(t, rl.Allow())
|
|
|
|
// Wait for window reset
|
|
time.Sleep(60 * time.Millisecond)
|
|
|
|
assert.True(t, rl.Allow())
|
|
}
|
|
|
|
// --- generateClientID test ---
|
|
|
|
func TestGenerateClientID_GeneratesID(t *testing.T) {
|
|
id := generateClientID()
|
|
assert.NotEmpty(t, id)
|
|
assert.Contains(t, id, "client_")
|
|
}
|
|
|
|
// --- checkLiquidations test ---
|
|
|
|
func TestCheckLiquidations_NoPositions(t *testing.T) {
|
|
server := newTestWebSocketServer()
|
|
|
|
// Should not panic with no positions
|
|
server.checkLiquidations()
|
|
}
|
|
|
|
// --- handleGetPositions tests ---
|
|
|
|
func TestHandleGetPositions_NotAuthenticated(t *testing.T) {
|
|
server := newTestWebSocketServer()
|
|
client := &Client{
|
|
ID: "test-client",
|
|
send: make(chan []byte, 256),
|
|
subscriptions: make(map[string]bool),
|
|
authenticated: false,
|
|
lastActivity: time.Now(),
|
|
rateLimiter: NewRateLimiter(100, time.Minute),
|
|
}
|
|
|
|
server.handleGetPositions(client, "req123")
|
|
|
|
select {
|
|
case <-client.send:
|
|
// Error received
|
|
case <-time.After(time.Second):
|
|
t.Fatal("Expected error response")
|
|
}
|
|
}
|
|
|
|
// --- handleCancelOrder additional tests ---
|
|
|
|
func TestHandleCancelOrder_WithOrderIdField(t *testing.T) {
|
|
server := newTestWebSocketServer()
|
|
client := &Client{
|
|
ID: "test-client",
|
|
UserID: "user123",
|
|
send: make(chan []byte, 256),
|
|
subscriptions: make(map[string]bool),
|
|
authenticated: true,
|
|
lastActivity: time.Now(),
|
|
rateLimiter: NewRateLimiter(100, time.Minute),
|
|
}
|
|
|
|
// Test with order_id field (alternative field name)
|
|
msg := map[string]interface{}{
|
|
"type": "cancel_order",
|
|
"order_id": float64(99999),
|
|
}
|
|
|
|
server.handleCancelOrder(client, msg, "req123")
|
|
|
|
select {
|
|
case <-client.send:
|
|
// Response received
|
|
case <-time.After(time.Second):
|
|
t.Fatal("Expected response")
|
|
}
|
|
}
|
|
|
|
// --- handleVaultDeposit additional tests ---
|
|
|
|
func TestHandleVaultDeposit_WithVaultIdField(t *testing.T) {
|
|
server := newTestWebSocketServer()
|
|
client := &Client{
|
|
ID: "test-client",
|
|
UserID: "user123",
|
|
send: make(chan []byte, 256),
|
|
subscriptions: make(map[string]bool),
|
|
authenticated: true,
|
|
lastActivity: time.Now(),
|
|
rateLimiter: NewRateLimiter(100, time.Minute),
|
|
}
|
|
|
|
// Test with vault_id field (alternative field name)
|
|
msg := map[string]interface{}{
|
|
"type": "vault_deposit",
|
|
"vault_id": "vault123",
|
|
"amount": "1000000000",
|
|
}
|
|
|
|
server.handleVaultDeposit(client, msg, "req123")
|
|
|
|
select {
|
|
case <-client.send:
|
|
// Response received
|
|
case <-time.After(time.Second):
|
|
t.Fatal("Expected response")
|
|
}
|
|
}
|
|
|
|
// --- handleOpenPosition additional tests ---
|
|
|
|
func TestHandleOpenPosition_InvalidSide(t *testing.T) {
|
|
server := newTestWebSocketServer()
|
|
client := &Client{
|
|
ID: "test-client",
|
|
UserID: "user123",
|
|
send: make(chan []byte, 256),
|
|
subscriptions: make(map[string]bool),
|
|
authenticated: true,
|
|
lastActivity: time.Now(),
|
|
rateLimiter: NewRateLimiter(100, time.Minute),
|
|
}
|
|
|
|
msg := map[string]interface{}{
|
|
"type": "open_position",
|
|
"symbol": "BTC/USD",
|
|
"side": "invalid",
|
|
"size": 1.0,
|
|
"leverage": 10.0,
|
|
}
|
|
|
|
server.handleOpenPosition(client, msg, "req123")
|
|
|
|
select {
|
|
case <-client.send:
|
|
// Response received
|
|
case <-time.After(time.Second):
|
|
t.Fatal("Expected response")
|
|
}
|
|
}
|
|
|
|
// --- handlePlaceOrder additional tests ---
|
|
|
|
func TestHandlePlaceOrder_NotAuthenticated(t *testing.T) {
|
|
server := newTestWebSocketServer()
|
|
client := &Client{
|
|
ID: "test-client",
|
|
send: make(chan []byte, 256),
|
|
subscriptions: make(map[string]bool),
|
|
authenticated: false,
|
|
lastActivity: time.Now(),
|
|
rateLimiter: NewRateLimiter(100, time.Minute),
|
|
}
|
|
|
|
msg := map[string]interface{}{
|
|
"type": "place_order",
|
|
"order": map[string]interface{}{
|
|
"symbol": "BTC/USD",
|
|
"side": "buy",
|
|
"type": "limit",
|
|
"price": 50000.0,
|
|
"size": 1.0,
|
|
},
|
|
}
|
|
|
|
server.handlePlaceOrder(client, msg, "req123")
|
|
|
|
select {
|
|
case <-client.send:
|
|
// Error received
|
|
case <-time.After(time.Second):
|
|
t.Fatal("Expected error response")
|
|
}
|
|
}
|
|
|
|
// --- handleClosePosition additional tests ---
|
|
|
|
func TestHandleClosePosition_WithPositionIdField(t *testing.T) {
|
|
server := newTestWebSocketServer()
|
|
client := &Client{
|
|
ID: "test-client",
|
|
UserID: "user123",
|
|
send: make(chan []byte, 256),
|
|
subscriptions: make(map[string]bool),
|
|
authenticated: true,
|
|
lastActivity: time.Now(),
|
|
rateLimiter: NewRateLimiter(100, time.Minute),
|
|
}
|
|
|
|
// Test with position_id field (alternative field name)
|
|
msg := map[string]interface{}{
|
|
"type": "close_position",
|
|
"position_id": "pos123",
|
|
"size": 0.5,
|
|
}
|
|
|
|
server.handleClosePosition(client, msg, "req123")
|
|
|
|
select {
|
|
case <-client.send:
|
|
// Response received
|
|
case <-time.After(time.Second):
|
|
t.Fatal("Expected response")
|
|
}
|
|
}
|
|
|
|
// --- handleModifyLeverage additional tests ---
|
|
|
|
func TestHandleModifyLeverage_WithPositionID(t *testing.T) {
|
|
server := newTestWebSocketServer()
|
|
client := &Client{
|
|
ID: "test-client",
|
|
UserID: "user123",
|
|
send: make(chan []byte, 256),
|
|
subscriptions: make(map[string]bool),
|
|
authenticated: true,
|
|
lastActivity: time.Now(),
|
|
rateLimiter: NewRateLimiter(100, time.Minute),
|
|
}
|
|
|
|
// Test with positionID field
|
|
msg := map[string]interface{}{
|
|
"type": "modify_leverage",
|
|
"positionID": "pos456",
|
|
"leverage": 20.0,
|
|
}
|
|
|
|
server.handleModifyLeverage(client, msg, "req123")
|
|
|
|
select {
|
|
case <-client.send:
|
|
// Response received
|
|
case <-time.After(time.Second):
|
|
t.Fatal("Expected response")
|
|
}
|
|
}
|