"""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