Files
nofx/trader/okx_trader_test.go
0xYYBB | ZYY | Bobo 62bce32d1f feat: 添加 OKX 交易所支持 (#1150)
* feat: 添加 OKX 交易所支持(USDT Perpetual Swap)
## 新增功能
- 實現完整的 OKX API v5 REST 客戶端(純 Go 標準庫,無外部依賴)
- 支持 USDT 永續合約交易(BTC-USDT-SWAP 等)
- 實現 Trader 接口的 13 個核心方法
## 技術細節
### trader/okx_trader.go (NEW)
- HMAC-SHA256 簽名機制(完全符合 OKX API v5 規範)
- 餘額和持倉緩存(15秒,參考 Binance 實現)
- 支持 Demo Trading(testnet 模式)
- Symbol 格式轉換(BTCUSDT ↔ BTC-USDT-SWAP)
- 全倉模式(Cross Margin)支持
- 自動槓桿設置
### 實現的接口方法:
-  GetBalance() - 獲取賬戶餘額
-  GetPositions() - 獲取所有持倉
-  OpenLong() / OpenShort() - 開倉
-  CloseLong() / CloseShort() - 平倉
-  SetLeverage() - 設置槓桿
-  SetMarginMode() - 設置保證金模式
-  GetMarketPrice() - 獲取市場價格
-  FormatQuantity() - 格式化數量
- ⚠️  止盈止損功能標記為 TODO(非核心交易功能)
### config/database.go (MODIFIED)
- 添加 "okx" 到預設交易所列表
- 新增 okx_passphrase 字段(OKX 需要 3 個認證參數)
- 更新 ExchangeConfig 結構
- 添加數據庫遷移語句(ALTER TABLE)
### api/server.go (MODIFIED)
- 在 handleCreateTrader() 添加 OKX 初始化邏輯
- switch case "okx" 分支
## 代碼品質
- 代碼行數:~450 行
- 外部依賴:0 個
- 編譯狀態: 通過
- 測試覆蓋:待實現(下一步)
## 待完成事項
- [ ] 撰寫單元測試(目標 >80% 覆蓋率)
- [ ] 完善數據庫查詢邏輯(GetExchanges 添加 OKX passphrase 掃描)
- [ ] 實現止盈止損功能(可選)
* refactor: 完善 OKX passphrase 數據庫和 API 支持
- config/database.go:
  • GetExchanges() 添加 okx_passphrase 查詢和解密
  • UpdateExchange() 函數簽名添加 okxPassphrase 參數
  • UpdateExchange() UPDATE 邏輯添加 okx_passphrase SET 子句
  • UpdateExchange() INSERT 添加 okx_passphrase 加密和列
- api/server.go:
  • UpdateExchangeConfigRequest 添加 OKXPassphrase 字段
  • UpdateExchange 調用添加 OKXPassphrase 參數
- api/utils.go:
  • SanitizeExchangeConfigForLog 添加 OKXPassphrase 脫敏
 編譯測試通過,OKX 完整功能支持完成
* test: 添加 OKX Trader 完整單元測試套件
📊 測試覆蓋率:92.6% (遠超 80% 目標)
 完成的測試:
- 接口兼容性測試
- NewOKXTrader 構造函數測試(5個場景)
- 符號格式轉換測試(5個場景)
- HMAC-SHA256 簽名一致性測試
- GetBalance 測試(含字段驗證)
- GetPositions 測試(含標準化數據驗證)
- GetMarketPrice 測試(3個場景)
- FormatQuantity 測試(5個場景)
- SetLeverage/SetMarginMode 測試
- OpenLong/OpenShort 測試
- CloseLong/CloseShort 測試
- 緩存機制測試
- 錯誤處理測試(API錯誤、網絡錯誤、JSON錯誤)
🔧 測試套件架構:
- OKXTraderTestSuite 繼承 TraderTestSuite
- Mock HTTP 服務器模擬 OKX API v5 響應
- 完整覆蓋所有公開方法
- 包含邊界條件和錯誤場景測試
📈 方法覆蓋率明細:
- request: 90.0%
- GetBalance: 97.0%
- GetPositions: 83.3%
- formatSymbol, OpenLong, OpenShort, CloseLong, CloseShort: 100%
- placeOrder, SetMarginMode, FormatQuantity, clearCache: 100%
- Cancel* 方法系列: 100%
- SetLeverage: 81.8%
- GetMarketPrice: 85.7%
---------
Co-authored-by: the-dev-z <the-dev-z@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-02 10:18:13 +08:00

777 lines
19 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package trader
import (
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/stretchr/testify/assert"
)
// ============================================================
// 一、OKXTraderTestSuite - 继承 base test suite
// ============================================================
// OKXTraderTestSuite OKX交易器测试套件
// 继承 TraderTestSuite 并添加 OKX 特定的 mock 逻辑
type OKXTraderTestSuite struct {
*TraderTestSuite // 嵌入基础测试套件
mockServer *httptest.Server
}
// NewOKXTraderTestSuite 创建 OKX 测试套件
func NewOKXTraderTestSuite(t *testing.T) *OKXTraderTestSuite {
// 创建 mock HTTP 服务器
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
path := r.URL.Path
var respBody interface{}
switch {
// Mock GetBalance - /api/v5/account/balance
case path == "/api/v5/account/balance":
respBody = map[string]interface{}{
"code": "0",
"msg": "",
"data": []map[string]interface{}{
{
"totalEq": "10100.50",
"details": []map[string]interface{}{
{
"ccy": "USDT",
"eq": "10000.00",
"availEq": "8000.00",
"frozenBal": "2000.00",
"upl": "100.50",
"cashBal": "10000.00",
"ordFrozen": "0",
"liab": "0",
"uTime": "1609459200000",
"crossLiab": "0",
"isoLiab": "0",
"mgnRatio": "",
"interest": "0",
"twap": "0",
"maxLoan": "",
"eqUsd": "10000.00",
"notionalLever": "",
"stgyEq": "0",
"isoEq": "0",
},
},
},
},
}
// Mock GetPositions - /api/v5/account/positions
case path == "/api/v5/account/positions":
respBody = map[string]interface{}{
"code": "0",
"msg": "",
"data": []map[string]interface{}{
{
"instId": "BTC-USDT-SWAP",
"pos": "0.5",
"posSide": "long",
"avgPx": "50000.00",
"markPx": "50500.00",
"upl": "250.00",
"uplRatio": "0.01",
"lever": "10",
"liqPx": "45000.00",
"notionalUsd": "25250.00",
"instType": "SWAP",
"mgnMode": "cross",
"cTime": "1609459200000",
"uTime": "1609459200000",
},
},
}
// Mock GetMarketPrice - /api/v5/market/ticker
case path == "/api/v5/market/ticker":
instId := r.URL.Query().Get("instId")
if instId == "" {
instId = "BTC-USDT-SWAP"
}
price := "50000.00"
if instId == "ETH-USDT-SWAP" {
price = "3000.00"
} else if instId == "INVALID-USDT-SWAP" {
// 返回错误
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{
"code": "51001",
"msg": "Instrument ID does not exist",
"data": []interface{}{},
})
return
}
respBody = map[string]interface{}{
"code": "0",
"msg": "",
"data": []map[string]interface{}{
{
"instId": instId,
"last": price,
"lastSz": "1",
"askPx": price,
"askSz": "10",
"bidPx": price,
"bidSz": "10",
"open24h": price,
"high24h": price,
"low24h": price,
"volCcy24h": "1000000",
"vol24h": "20",
"ts": "1609459200000",
"sodUtc0": price,
"sodUtc8": price,
},
},
}
// Mock CreateOrder - /api/v5/trade/order (POST)
case path == "/api/v5/trade/order" && r.Method == "POST":
respBody = map[string]interface{}{
"code": "0",
"msg": "",
"data": []map[string]interface{}{
{
"ordId": "123456789",
"clOrdId": "test_order_123",
"tag": "",
"sCode": "0",
"sMsg": "",
},
},
}
// Mock SetLeverage - /api/v5/account/set-leverage (POST)
case path == "/api/v5/account/set-leverage" && r.Method == "POST":
respBody = map[string]interface{}{
"code": "0",
"msg": "",
"data": []map[string]interface{}{
{
"instId": "BTC-USDT-SWAP",
"lever": "10",
"mgnMode": "cross",
"posSide": "long",
},
},
}
// Mock SetMarginMode - /api/v5/account/set-position-mode (POST)
case path == "/api/v5/account/set-position-mode" && r.Method == "POST":
respBody = map[string]interface{}{
"code": "0",
"msg": "",
"data": []map[string]interface{}{
{
"posMode": "net_mode",
},
},
}
// Default: empty success response
default:
respBody = map[string]interface{}{
"code": "0",
"msg": "",
"data": []interface{}{},
}
}
// 序列化响应
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(respBody)
}))
// 创建 OKXTrader 并设置为使用 mock 服务器
trader := &OKXTrader{
apiKey: "test_api_key",
secretKey: "test_secret_key",
passphrase: "test_passphrase",
baseURL: mockServer.URL,
httpClient: mockServer.Client(),
testnet: false,
cacheDuration: 0, // 禁用缓存以便测试
}
// 创建基础套件
baseSuite := NewTraderTestSuite(t, trader)
return &OKXTraderTestSuite{
TraderTestSuite: baseSuite,
mockServer: mockServer,
}
}
// Cleanup 清理资源
func (s *OKXTraderTestSuite) Cleanup() {
if s.mockServer != nil {
s.mockServer.Close()
}
s.TraderTestSuite.Cleanup()
}
// ============================================================
// 二、使用 OKXTraderTestSuite 运行通用测试
// ============================================================
// TestOKXTrader_InterfaceCompliance 测试接口兼容性
func TestOKXTrader_InterfaceCompliance(t *testing.T) {
var _ Trader = (*OKXTrader)(nil)
}
// TestOKXTrader_CommonInterface 使用测试套件运行所有通用接口测试
func TestOKXTrader_CommonInterface(t *testing.T) {
// 创建测试套件
suite := NewOKXTraderTestSuite(t)
defer suite.Cleanup()
// 运行所有通用接口测试
suite.RunAllTests()
}
// ============================================================
// 三、OKX 特定功能的单元测试
// ============================================================
// TestNewOKXTrader 测试创建 OKX 交易器
func TestNewOKXTrader(t *testing.T) {
tests := []struct {
name string
apiKey string
secretKey string
passphrase string
testnet bool
wantNil bool
}{
{
name: "成功创建(正式环境)",
apiKey: "test_api_key",
secretKey: "test_secret_key",
passphrase: "test_passphrase",
testnet: false,
wantNil: false,
},
{
name: "成功创建(测试环境)",
apiKey: "test_api_key",
secretKey: "test_secret_key",
passphrase: "test_passphrase",
testnet: true,
wantNil: false,
},
{
name: "空API Key仍可创建",
apiKey: "",
secretKey: "test_secret_key",
passphrase: "test_passphrase",
testnet: false,
wantNil: false,
},
{
name: "空Secret Key仍可创建",
apiKey: "test_api_key",
secretKey: "",
passphrase: "test_passphrase",
testnet: false,
wantNil: false,
},
{
name: "空Passphrase仍可创建",
apiKey: "test_api_key",
secretKey: "test_secret_key",
passphrase: "",
testnet: false,
wantNil: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
trader := NewOKXTrader(tt.apiKey, tt.secretKey, tt.passphrase, tt.testnet)
if tt.wantNil {
assert.Nil(t, trader)
} else {
assert.NotNil(t, trader)
assert.NotNil(t, trader.httpClient)
assert.Equal(t, tt.apiKey, trader.apiKey)
assert.Equal(t, tt.secretKey, trader.secretKey)
assert.Equal(t, tt.passphrase, trader.passphrase)
assert.Equal(t, tt.testnet, trader.testnet)
// 检查 baseURL
if tt.testnet {
assert.Equal(t, "https://www.okx.com", trader.baseURL)
} else {
assert.Equal(t, "https://www.okx.com", trader.baseURL)
}
// 检查缓存时间
assert.Equal(t, 15*time.Second, trader.cacheDuration)
}
})
}
}
// TestOKXTrader_SymbolFormat 测试符号格式转换
func TestOKXTrader_SymbolFormat(t *testing.T) {
trader := &OKXTrader{}
tests := []struct {
name string
input string
expected string
}{
{
name: "BTC USDT Swap",
input: "BTCUSDT",
expected: "BTC-USDT-SWAP",
},
{
name: "ETH USDT Swap",
input: "ETHUSDT",
expected: "ETH-USDT-SWAP",
},
{
name: "SOL USDT Swap",
input: "SOLUSDT",
expected: "SOL-USDT-SWAP",
},
{
name: "小写输入",
input: "btcusdt",
expected: "BTC-USDT-SWAP",
},
{
name: "混合大小写",
input: "BtcUsdT",
expected: "BTC-USDT-SWAP",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := trader.formatSymbol(tt.input)
assert.Equal(t, tt.expected, result)
})
}
}
// TestOKXTrader_Sign 测试签名算法
func TestOKXTrader_Sign(t *testing.T) {
trader := &OKXTrader{
secretKey: "test_secret_key",
}
// 测试签名一致性
timestamp := "2024-01-01T00:00:00.000Z"
method := "GET"
requestPath := "/api/v5/account/balance"
body := ""
// 多次签名应该产生相同结果
sign1 := trader.sign(timestamp, method, requestPath, body)
sign2 := trader.sign(timestamp, method, requestPath, body)
assert.Equal(t, sign1, sign2, "相同输入应产生相同签名")
// 不同输入应该产生不同签名
sign3 := trader.sign("2024-01-01T00:00:01.000Z", method, requestPath, body)
assert.NotEqual(t, sign1, sign3, "不同timestamp应产生不同签名")
sign4 := trader.sign(timestamp, "POST", requestPath, body)
assert.NotEqual(t, sign1, sign4, "不同method应产生不同签名")
sign5 := trader.sign(timestamp, method, "/api/v5/account/positions", body)
assert.NotEqual(t, sign1, sign5, "不同path应产生不同签名")
sign6 := trader.sign(timestamp, method, requestPath, `{"instId":"BTC-USDT-SWAP"}`)
assert.NotEqual(t, sign1, sign6, "不同body应产生不同签名")
}
// TestOKXTrader_GetBalance 测试获取余额
func TestOKXTrader_GetBalance(t *testing.T) {
// 创建测试套件
suite := NewOKXTraderTestSuite(t)
defer suite.Cleanup()
trader := suite.Trader.(*OKXTrader)
// 测试获取余额
balance, err := trader.GetBalance()
assert.NoError(t, err)
assert.NotNil(t, balance)
// 验证返回的标准化余额字段
assert.Contains(t, balance, "totalWalletBalance")
assert.Contains(t, balance, "availableBalance")
assert.Contains(t, balance, "totalUnrealizedProfit")
assert.Contains(t, balance, "balance")
// 验证余额值
totalBalance, ok := balance["totalWalletBalance"].(float64)
assert.True(t, ok)
assert.Equal(t, 10000.00, totalBalance)
availBalance, ok := balance["availableBalance"].(float64)
assert.True(t, ok)
assert.Equal(t, 8000.00, availBalance)
upl, ok := balance["totalUnrealizedProfit"].(float64)
assert.True(t, ok)
assert.Equal(t, 100.50, upl)
}
// TestOKXTrader_GetPositions 测试获取持仓
func TestOKXTrader_GetPositions(t *testing.T) {
// 创建测试套件
suite := NewOKXTraderTestSuite(t)
defer suite.Cleanup()
trader := suite.Trader.(*OKXTrader)
// 测试获取持仓
positions, err := trader.GetPositions()
assert.NoError(t, err)
assert.NotNil(t, positions)
assert.GreaterOrEqual(t, len(positions), 1)
// 验证标准化的持仓字段
position := positions[0]
assert.Contains(t, position, "symbol")
assert.Contains(t, position, "side")
assert.Contains(t, position, "entry_price")
assert.Contains(t, position, "mark_price")
assert.Contains(t, position, "quantity")
assert.Contains(t, position, "leverage")
assert.Contains(t, position, "unrealized_pnl")
assert.Contains(t, position, "unrealized_pnl_pct")
assert.Contains(t, position, "liquidation_price")
assert.Contains(t, position, "margin_used")
// 验证具体值OKX 的数据被标准化)
assert.Equal(t, "BTC", position["symbol"]) // BTC-USDT-SWAP → BTC
assert.Equal(t, "long", position["side"])
assert.Equal(t, 50000.0, position["entry_price"])
assert.Equal(t, 50500.0, position["mark_price"])
assert.Equal(t, 0.5, position["quantity"])
assert.Equal(t, 10, position["leverage"])
assert.Equal(t, 250.0, position["unrealized_pnl"])
assert.Equal(t, 45000.0, position["liquidation_price"])
}
// TestOKXTrader_GetMarketPrice 测试获取市场价格
func TestOKXTrader_GetMarketPrice(t *testing.T) {
// 创建测试套件
suite := NewOKXTraderTestSuite(t)
defer suite.Cleanup()
trader := suite.Trader.(*OKXTrader)
tests := []struct {
name string
symbol string
wantError bool
wantPrice float64
}{
{
name: "获取BTC价格",
symbol: "BTCUSDT",
wantError: false,
wantPrice: 50000.00,
},
{
name: "获取ETH价格",
symbol: "ETHUSDT",
wantError: false,
wantPrice: 3000.00,
},
{
name: "无效符号",
symbol: "INVALID",
wantError: true,
wantPrice: 0,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
price, err := trader.GetMarketPrice(tt.symbol)
if tt.wantError {
assert.Error(t, err)
} else {
assert.NoError(t, err)
assert.Equal(t, tt.wantPrice, price)
}
})
}
}
// TestOKXTrader_FormatQuantity 测试数量格式化
func TestOKXTrader_FormatQuantity(t *testing.T) {
trader := &OKXTrader{}
tests := []struct {
name string
symbol string
quantity float64
expected string
}{
{
name: "整数数量",
symbol: "BTCUSDT",
quantity: 1.0,
expected: "1.0000",
},
{
name: "小数数量",
symbol: "BTCUSDT",
quantity: 0.5,
expected: "0.5000",
},
{
name: "多位小数四舍五入到4位",
symbol: "BTCUSDT",
quantity: 0.123456,
expected: "0.1235",
},
{
name: "零数量",
symbol: "BTCUSDT",
quantity: 0,
expected: "0.0000",
},
{
name: "大数量",
symbol: "BTCUSDT",
quantity: 100.123,
expected: "100.1230",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result, err := trader.FormatQuantity(tt.symbol, tt.quantity)
assert.NoError(t, err)
assert.Equal(t, tt.expected, result)
})
}
}
// TestOKXTrader_SetLeverage 测试设置杠杆
func TestOKXTrader_SetLeverage(t *testing.T) {
// 创建测试套件
suite := NewOKXTraderTestSuite(t)
defer suite.Cleanup()
trader := suite.Trader.(*OKXTrader)
// 测试设置杠杆
err := trader.SetLeverage("BTCUSDT", 10)
assert.NoError(t, err)
}
// TestOKXTrader_SetMarginMode 测试设置保证金模式
func TestOKXTrader_SetMarginMode(t *testing.T) {
// 创建测试套件
suite := NewOKXTraderTestSuite(t)
defer suite.Cleanup()
trader := suite.Trader.(*OKXTrader)
// 测试设置保证金模式cross margin = true
err := trader.SetMarginMode("BTCUSDT", true)
assert.NoError(t, err)
// 测试设置保证金模式isolated margin = false
err = trader.SetMarginMode("BTCUSDT", false)
assert.NoError(t, err)
}
// TestOKXTrader_OpenLong 测试开多仓
func TestOKXTrader_OpenLong(t *testing.T) {
// 创建测试套件
suite := NewOKXTraderTestSuite(t)
defer suite.Cleanup()
trader := suite.Trader.(*OKXTrader)
// 测试开多仓OKX 的 OpenLong 接受 leverage 参数)
result, err := trader.OpenLong("BTCUSDT", 0.01, 10)
assert.NoError(t, err)
assert.NotNil(t, result)
}
// TestOKXTrader_OpenShort 测试开空仓
func TestOKXTrader_OpenShort(t *testing.T) {
// 创建测试套件
suite := NewOKXTraderTestSuite(t)
defer suite.Cleanup()
trader := suite.Trader.(*OKXTrader)
// 测试开空仓OKX 的 OpenShort 接受 leverage 参数)
result, err := trader.OpenShort("BTCUSDT", 0.01, 10)
assert.NoError(t, err)
assert.NotNil(t, result)
}
// TestOKXTrader_CloseLong 测试平多仓
func TestOKXTrader_CloseLong(t *testing.T) {
// 创建测试套件
suite := NewOKXTraderTestSuite(t)
defer suite.Cleanup()
trader := suite.Trader.(*OKXTrader)
// 测试平多仓OKX 的 CloseLong 只接受 symbol 和 quantity
result, err := trader.CloseLong("BTCUSDT", 0.01)
assert.NoError(t, err)
assert.NotNil(t, result)
}
// TestOKXTrader_CloseShort 测试平空仓
func TestOKXTrader_CloseShort(t *testing.T) {
// 创建测试套件
suite := NewOKXTraderTestSuite(t)
defer suite.Cleanup()
trader := suite.Trader.(*OKXTrader)
// 测试平空仓OKX 的 CloseShort 只接受 symbol 和 quantity
result, err := trader.CloseShort("BTCUSDT", 0.01)
assert.NoError(t, err)
assert.NotNil(t, result)
}
// TestOKXTrader_Cache 测试缓存机制
func TestOKXTrader_Cache(t *testing.T) {
// 创建测试套件
suite := NewOKXTraderTestSuite(t)
defer suite.Cleanup()
trader := suite.Trader.(*OKXTrader)
// 启用缓存
trader.cacheDuration = 5 * time.Second
// 第一次调用 - 应该访问 API
balance1, err := trader.GetBalance()
assert.NoError(t, err)
assert.NotNil(t, balance1)
// 第二次调用 - 应该使用缓存
balance2, err := trader.GetBalance()
assert.NoError(t, err)
assert.NotNil(t, balance2)
assert.Equal(t, balance1, balance2)
// 清空缓存
trader.balanceCacheMutex.Lock()
trader.cachedBalance = nil
trader.balanceCacheTime = time.Time{}
trader.balanceCacheMutex.Unlock()
// 第三次调用 - 应该重新访问 API
balance3, err := trader.GetBalance()
assert.NoError(t, err)
assert.NotNil(t, balance3)
}
// TestOKXTrader_ErrorHandling 测试错误处理
func TestOKXTrader_ErrorHandling(t *testing.T) {
// 创建错误响应的 mock 服务器
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{
"code": "50000",
"msg": "Internal server error",
"data": []interface{}{},
})
}))
defer mockServer.Close()
trader := &OKXTrader{
apiKey: "test_api_key",
secretKey: "test_secret_key",
passphrase: "test_passphrase",
baseURL: mockServer.URL,
httpClient: mockServer.Client(),
testnet: false,
}
// 测试各种操作应该返回错误
_, err := trader.GetBalance()
assert.Error(t, err)
assert.Contains(t, err.Error(), "50000")
_, err = trader.GetPositions()
assert.Error(t, err)
_, err = trader.GetMarketPrice("BTCUSDT")
assert.Error(t, err)
_, err = trader.OpenLong("BTCUSDT", 0.01, 10)
assert.Error(t, err)
err = trader.SetLeverage("BTCUSDT", 10)
assert.Error(t, err)
}
// TestOKXTrader_HTTPRequestError 测试 HTTP 请求错误
func TestOKXTrader_HTTPRequestError(t *testing.T) {
// 使用无效的 baseURL
trader := &OKXTrader{
apiKey: "test_api_key",
secretKey: "test_secret_key",
passphrase: "test_passphrase",
baseURL: "http://invalid-url-that-does-not-exist-12345.com",
httpClient: &http.Client{Timeout: 1 * time.Second},
testnet: false,
}
// 测试各种操作应该返回网络错误
_, err := trader.GetBalance()
assert.Error(t, err)
_, err = trader.GetPositions()
assert.Error(t, err)
_, err = trader.GetMarketPrice("BTCUSDT")
assert.Error(t, err)
}
// TestOKXTrader_InvalidJSON 测试无效 JSON 响应
func TestOKXTrader_InvalidJSON(t *testing.T) {
// 创建返回无效 JSON 的 mock 服务器
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
fmt.Fprint(w, "invalid json{{{")
}))
defer mockServer.Close()
trader := &OKXTrader{
apiKey: "test_api_key",
secretKey: "test_secret_key",
passphrase: "test_passphrase",
baseURL: mockServer.URL,
httpClient: mockServer.Client(),
testnet: false,
}
// 测试应该返回 JSON 解析错误
_, err := trader.GetBalance()
assert.Error(t, err)
assert.Contains(t, err.Error(), "解析")
}