stock/tests/test_exchange_config.py
ZhangPeng 9aecdd036c Initial commit: OpenClaw Trading - AI多智能体量化交易系统
- 添加项目核心代码和配置
- 添加前端界面 (Next.js)
- 添加单元测试
- 更新 .gitignore 排除缓存和依赖
2026-02-27 03:47:40 +08:00

407 lines
13 KiB
Python

"""Tests for exchange configuration management."""
import asyncio
import tempfile
from datetime import datetime
from pathlib import Path
import pytest
import yaml
from openclaw.config.exchange_config import (
EncryptionManager,
ExchangeConfig,
ExchangeConfigManager,
ExchangeType,
FeeConfig,
ProxyConfig,
TimeoutConfig,
get_exchange_manager,
reset_exchange_manager,
)
class TestEncryptionManager:
"""Test encryption functionality."""
def test_singleton_pattern(self):
"""Test that EncryptionManager is a singleton."""
manager1 = EncryptionManager()
manager2 = EncryptionManager()
assert manager1 is manager2
def test_encrypt_decrypt(self):
"""Test encryption and decryption."""
manager = EncryptionManager()
manager.initialize("test_key_12345")
original = "sensitive_data_123"
encrypted = manager.encrypt(original)
decrypted = manager.decrypt(encrypted)
assert encrypted != original
assert decrypted == original
def test_different_data_produces_different_ciphertexts(self):
"""Test that same data encrypts differently each time."""
manager = EncryptionManager()
manager.initialize("test_key_12345")
encrypted1 = manager.encrypt("test_data")
encrypted2 = manager.encrypt("test_data")
# Fernet produces different ciphertexts for same plaintext
assert encrypted1 != encrypted2
# But both decrypt to same plaintext
assert manager.decrypt(encrypted1) == manager.decrypt(encrypted2)
class TestExchangeConfig:
"""Test ExchangeConfig model."""
def test_create_basic_config(self):
"""Test creating a basic exchange configuration."""
config = ExchangeConfig(
name="Test Binance",
exchange_id=ExchangeType.BINANCE,
api_key="test_api_key_12345",
api_secret="test_secret_12345",
)
assert config.name == "Test Binance"
assert config.exchange_id == ExchangeType.BINANCE
assert config.api_key == "test_api_key_12345"
assert config.api_secret == "test_secret_12345"
assert config.sandbox is False
assert config.enabled is True
def test_config_validation_empty_api_key(self):
"""Test that empty API key raises validation error."""
with pytest.raises(ValueError, match="API密钥不能为空"):
ExchangeConfig(
name="Test",
exchange_id=ExchangeType.BINANCE,
api_key="",
api_secret="secret",
)
def test_config_validation_empty_api_secret(self):
"""Test that empty API secret raises validation error."""
with pytest.raises(ValueError, match="API密钥密码不能为空"):
ExchangeConfig(
name="Test",
exchange_id=ExchangeType.BINANCE,
api_key="key",
api_secret=" ",
)
def test_config_with_optional_fields(self):
"""Test creating config with all optional fields."""
config = ExchangeConfig(
name="Full Config",
exchange_id=ExchangeType.OKX,
api_key="api_key_12345",
api_secret="secret_12345",
passphrase="passphrase_123",
sandbox=True,
enabled=False,
proxy=ProxyConfig(
enabled=True,
http_proxy="http://proxy.example.com:8080",
),
timeout=TimeoutConfig(connect=5, read=60, request=60),
fee_rate=FeeConfig(maker_fee=0.0005, taker_fee=0.001),
description="Test configuration",
)
assert config.passphrase == "passphrase_123"
assert config.sandbox is True
assert config.enabled is False
assert config.proxy.enabled is True
assert config.proxy.http_proxy == "http://proxy.example.com:8080"
assert config.timeout.connect == 5
assert config.fee_rate.maker_fee == 0.0005
assert config.description == "Test configuration"
def test_exchange_type_enum(self):
"""Test ExchangeType enum values."""
assert ExchangeType.BINANCE.value == "binance"
assert ExchangeType.BINANCE_FUTURES.value == "binanceusdm"
assert ExchangeType.OKX.value == "okx"
assert ExchangeType.BYBIT.value == "bybit"
class TestExchangeConfigManager:
"""Test ExchangeConfigManager functionality."""
@pytest.fixture
def temp_config_file(self):
"""Create a temporary config file."""
with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as f:
yaml.dump({"exchanges": []}, f)
path = Path(f.name)
yield path
path.unlink(missing_ok=True)
reset_exchange_manager()
@pytest.fixture
def manager(self, temp_config_file):
"""Create a config manager with temp file."""
reset_exchange_manager()
return ExchangeConfigManager(temp_config_file)
def test_add_config(self, manager):
"""Test adding a configuration."""
config_id = manager.add_config(
exchange="binance",
api_key="test_key_12345",
api_secret="test_secret_12345",
name="Test Binance",
)
assert config_id is not None
assert len(manager.get_all_configs()) == 1
config = manager.get_config(config_id)
assert config is not None
assert config.name == "Test Binance"
assert config.exchange_id == ExchangeType.BINANCE
def test_add_multiple_configs(self, manager):
"""Test adding multiple configurations."""
id1 = manager.add_config(
exchange="binance",
api_key="key1",
api_secret="secret1",
name="Binance 1",
)
id2 = manager.add_config(
exchange="okx",
api_key="key2",
api_secret="secret2",
name="OKX 1",
)
configs = manager.get_all_configs()
assert len(configs) == 2
assert id1 in configs
assert id2 in configs
def test_get_nonexistent_config(self, manager):
"""Test getting a non-existent configuration."""
config = manager.get_config("nonexistent_id")
assert config is None
def test_update_config(self, manager):
"""Test updating a configuration."""
config_id = manager.add_config(
exchange="binance",
api_key="old_key",
api_secret="old_secret",
name="Old Name",
)
updated = manager.update_config(
config_id,
name="New Name",
api_key="new_key",
testnet=True,
)
assert updated is not None
assert updated.name == "New Name"
assert updated.api_key == "new_key"
assert updated.sandbox is True
# Fields not updated should remain
assert updated.api_secret == "old_secret"
def test_update_nonexistent_config(self, manager):
"""Test updating a non-existent configuration."""
result = manager.update_config(
"nonexistent_id",
name="New Name",
)
assert result is None
def test_delete_config(self, manager):
"""Test deleting a configuration."""
config_id = manager.add_config(
exchange="binance",
api_key="key",
api_secret="secret",
)
assert len(manager.get_all_configs()) == 1
deleted = manager.delete_config(config_id)
assert deleted is True
assert len(manager.get_all_configs()) == 0
def test_delete_nonexistent_config(self, manager):
"""Test deleting a non-existent configuration."""
result = manager.delete_config("nonexistent_id")
assert result is False
def test_persistence(self, temp_config_file):
"""Test that configurations persist to file."""
# Create manager and add config
manager1 = ExchangeConfigManager(temp_config_file)
config_id = manager1.add_config(
exchange="binance",
api_key="persist_key",
api_secret="persist_secret",
name="Persistent Config",
)
# Create new manager instance with same file
reset_exchange_manager()
manager2 = ExchangeConfigManager(temp_config_file)
config = manager2.get_config(config_id)
assert config is not None
assert config.name == "Persistent Config"
assert config.api_key == "persist_key"
def test_validate_config_valid(self, manager):
"""Test validating a valid configuration."""
config_id = manager.add_config(
exchange="binance",
api_key="valid_key_12345",
api_secret="valid_secret_12345",
)
result = manager.validate_config(config_id)
assert result["valid"] is True
assert len(result["errors"]) == 0
def test_validate_config_invalid_short_key(self, manager):
"""Test validating config with short API key."""
config_id = manager.add_config(
exchange="binance",
api_key="short",
api_secret="valid_secret_12345",
)
result = manager.validate_config(config_id)
assert result["valid"] is False
assert any("API密钥格式不正确" in e for e in result["errors"])
def test_validate_nonexistent_config(self, manager):
"""Test validating a non-existent configuration."""
result = manager.validate_config("nonexistent_id")
assert result["valid"] is False
assert any("配置不存在" in e for e in result["errors"])
class TestExchangeConfigAsync:
"""Test async methods of ExchangeConfigManager."""
@pytest.fixture
def temp_config_file(self):
"""Create a temporary config file."""
with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as f:
yaml.dump({"exchanges": []}, f)
path = Path(f.name)
yield path
path.unlink(missing_ok=True)
reset_exchange_manager()
@pytest.fixture
def manager(self, temp_config_file):
"""Create a config manager with temp file."""
reset_exchange_manager()
return ExchangeConfigManager(temp_config_file)
@pytest.mark.asyncio
async def test_test_connection_nonexistent_config(self, manager):
"""Test connection test for non-existent config."""
result = await manager.test_connection("nonexistent_id")
assert result["success"] is False
assert "配置不存在" in result["message"]
class TestProxyConfig:
"""Test ProxyConfig model."""
def test_default_proxy_config(self):
"""Test default proxy configuration."""
config = ProxyConfig()
assert config.enabled is False
assert config.http_proxy is None
assert config.https_proxy is None
assert config.socks_proxy is None
def test_custom_proxy_config(self):
"""Test custom proxy configuration."""
config = ProxyConfig(
enabled=True,
http_proxy="http://proxy.example.com:8080",
https_proxy="https://proxy.example.com:8443",
socks_proxy="socks5://proxy.example.com:1080",
)
assert config.enabled is True
assert config.http_proxy == "http://proxy.example.com:8080"
class TestTimeoutConfig:
"""Test TimeoutConfig model."""
def test_default_timeout_config(self):
"""Test default timeout configuration."""
config = TimeoutConfig()
assert config.connect == 10
assert config.read == 30
assert config.request == 30
def test_timeout_validation(self):
"""Test timeout value validation."""
# Valid values
config = TimeoutConfig(connect=1, read=1, request=1)
assert config.connect == 1
# Invalid values should raise validation error
with pytest.raises(ValueError):
TimeoutConfig(connect=0)
with pytest.raises(ValueError):
TimeoutConfig(connect=301)
class TestFeeConfig:
"""Test FeeConfig model."""
def test_default_fee_config(self):
"""Test default fee configuration."""
config = FeeConfig()
assert config.maker_fee == 0.001
assert config.taker_fee == 0.001
def test_fee_validation(self):
"""Test fee rate validation."""
# Valid values
config = FeeConfig(maker_fee=0.0001, taker_fee=0.0005)
assert config.maker_fee == 0.0001
# Invalid values should raise validation error
with pytest.raises(ValueError):
FeeConfig(maker_fee=-0.001)
with pytest.raises(ValueError):
FeeConfig(taker_fee=0.2)
class TestGlobalManager:
"""Test global manager instance functions."""
def test_get_exchange_manager_singleton(self):
"""Test that get_exchange_manager returns a singleton."""
reset_exchange_manager()
manager1 = get_exchange_manager()
manager2 = get_exchange_manager()
assert manager1 is manager2
def test_reset_exchange_manager(self):
"""Test resetting the global manager."""
manager1 = get_exchange_manager()
reset_exchange_manager()
manager2 = get_exchange_manager()
assert manager1 is not manager2