407 lines
13 KiB
Python
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
|