- Change file mode from 644 to 755 for all project files - Add chain_id parameter to get_balance RPC endpoint with default "ait-devnet" - Rename Miner.extra_meta_data to extra_metadata for consistency
425 lines
15 KiB
Python
Executable File
425 lines
15 KiB
Python
Executable File
"""Dual-Mode Wallet Tests
|
|
|
|
Tests for the dual-mode wallet adapter that supports both file-based
|
|
and daemon-based wallet operations.
|
|
"""
|
|
|
|
import pytest
|
|
import tempfile
|
|
import shutil
|
|
import json
|
|
from pathlib import Path
|
|
from unittest.mock import patch, MagicMock, Mock
|
|
from click.testing import CliRunner
|
|
|
|
from aitbc_cli.config import Config
|
|
from aitbc_cli.dual_mode_wallet_adapter import DualModeWalletAdapter
|
|
from aitbc_cli.wallet_daemon_client import WalletDaemonClient, WalletInfo, WalletBalance
|
|
from aitbc_cli.commands.wallet import wallet
|
|
from aitbc_cli.wallet_migration_service import WalletMigrationService
|
|
|
|
|
|
class TestWalletDaemonClient:
|
|
"""Test the wallet daemon client"""
|
|
|
|
def setup_method(self):
|
|
"""Set up test configuration"""
|
|
self.config = Config()
|
|
self.config.wallet_url = "http://localhost:8002"
|
|
self.client = WalletDaemonClient(self.config)
|
|
|
|
def test_client_initialization(self):
|
|
"""Test client initialization"""
|
|
assert self.client.base_url == "http://localhost:8002"
|
|
assert self.client.timeout == 30
|
|
|
|
@patch('aitbc_cli.wallet_daemon_client.httpx.Client')
|
|
def test_is_available_success(self, mock_client):
|
|
"""Test daemon availability check - success"""
|
|
mock_response = Mock()
|
|
mock_response.status_code = 200
|
|
mock_client.return_value.__enter__.return_value.get.return_value = mock_response
|
|
|
|
assert self.client.is_available() is True
|
|
|
|
@patch('aitbc_cli.wallet_daemon_client.httpx.Client')
|
|
def test_is_available_failure(self, mock_client):
|
|
"""Test daemon availability check - failure"""
|
|
mock_client.return_value.__enter__.side_effect = Exception("Connection failed")
|
|
|
|
assert self.client.is_available() is False
|
|
|
|
@patch('aitbc_cli.wallet_daemon_client.httpx.Client')
|
|
def test_create_wallet_success(self, mock_client):
|
|
"""Test wallet creation - success"""
|
|
mock_response = Mock()
|
|
mock_response.status_code = 201
|
|
mock_response.json.return_value = {
|
|
"wallet_id": "test-wallet",
|
|
"public_key": "0x123456",
|
|
"address": "aitbc1test",
|
|
"created_at": "2023-01-01T00:00:00Z",
|
|
"metadata": {}
|
|
}
|
|
mock_client.return_value.__enter__.return_value.post.return_value = mock_response
|
|
|
|
result = self.client.create_wallet("test-wallet", "password123")
|
|
|
|
assert isinstance(result, WalletInfo)
|
|
assert result.wallet_id == "test-wallet"
|
|
assert result.public_key == "0x123456"
|
|
assert result.address == "aitbc1test"
|
|
|
|
@patch('aitbc_cli.wallet_daemon_client.httpx.Client')
|
|
def test_list_wallets_success(self, mock_client):
|
|
"""Test wallet listing - success"""
|
|
mock_response = Mock()
|
|
mock_response.status_code = 200
|
|
mock_response.json.return_value = {
|
|
"wallets": [
|
|
{
|
|
"wallet_id": "wallet1",
|
|
"public_key": "0x111",
|
|
"address": "aitbc1wallet1",
|
|
"created_at": "2023-01-01T00:00:00Z"
|
|
},
|
|
{
|
|
"wallet_id": "wallet2",
|
|
"public_key": "0x222",
|
|
"address": "aitbc1wallet2",
|
|
"created_at": "2023-01-02T00:00:00Z"
|
|
}
|
|
]
|
|
}
|
|
mock_client.return_value.__enter__.return_value.get.return_value = mock_response
|
|
|
|
result = self.client.list_wallets()
|
|
|
|
assert len(result) == 2
|
|
assert result[0].wallet_id == "wallet1"
|
|
assert result[1].wallet_id == "wallet2"
|
|
|
|
@patch('aitbc_cli.wallet_daemon_client.httpx.Client')
|
|
def test_get_wallet_balance_success(self, mock_client):
|
|
"""Test wallet balance retrieval - success"""
|
|
mock_response = Mock()
|
|
mock_response.status_code = 200
|
|
mock_response.json.return_value = {
|
|
"wallet_id": "test-wallet",
|
|
"balance": 100.5,
|
|
"address": "aitbc1test",
|
|
"last_updated": "2023-01-01T00:00:00Z"
|
|
}
|
|
mock_client.return_value.__enter__.return_value.get.return_value = mock_response
|
|
|
|
result = self.client.get_wallet_balance("test-wallet")
|
|
|
|
assert isinstance(result, WalletBalance)
|
|
assert result.wallet_id == "test-wallet"
|
|
assert result.balance == 100.5
|
|
|
|
|
|
class TestDualModeWalletAdapter:
|
|
"""Test the dual-mode wallet adapter"""
|
|
|
|
def setup_method(self):
|
|
"""Set up test environment"""
|
|
self.temp_dir = Path(tempfile.mkdtemp())
|
|
self.config = Config()
|
|
self.config.config_dir = self.temp_dir
|
|
|
|
# Mock wallet directory
|
|
self.wallet_dir = self.temp_dir / "wallets"
|
|
self.wallet_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
def teardown_method(self):
|
|
"""Clean up test environment"""
|
|
shutil.rmtree(self.temp_dir)
|
|
|
|
def test_file_mode_initialization(self):
|
|
"""Test adapter initialization in file mode"""
|
|
adapter = DualModeWalletAdapter(self.config, use_daemon=False)
|
|
|
|
assert adapter.use_daemon is False
|
|
assert adapter.daemon_client is None
|
|
assert adapter.wallet_dir == Path.home() / ".aitbc" / "wallets"
|
|
|
|
def test_daemon_mode_initialization(self):
|
|
"""Test adapter initialization in daemon mode"""
|
|
adapter = DualModeWalletAdapter(self.config, use_daemon=True)
|
|
|
|
assert adapter.use_daemon is True
|
|
assert adapter.daemon_client is not None
|
|
assert isinstance(adapter.daemon_client, WalletDaemonClient)
|
|
|
|
def test_create_wallet_file_mode(self):
|
|
"""Test wallet creation in file mode"""
|
|
adapter = DualModeWalletAdapter(self.config, use_daemon=False)
|
|
|
|
with patch('aitbc_cli.dual_mode_wallet_adapter.Path.home') as mock_home:
|
|
mock_home.return_value = self.temp_dir
|
|
adapter.wallet_dir = self.temp_dir / "wallets"
|
|
|
|
result = adapter.create_wallet("test-wallet", "password123", "hd")
|
|
|
|
assert result["mode"] == "file"
|
|
assert result["wallet_name"] == "test-wallet"
|
|
assert result["wallet_type"] == "hd"
|
|
|
|
# Check wallet file was created
|
|
wallet_file = self.wallet_dir / "test-wallet.json"
|
|
assert wallet_file.exists()
|
|
|
|
@patch('aitbc_cli.dual_mode_wallet_adapter.Path.home')
|
|
def test_create_wallet_daemon_mode_success(self, mock_home):
|
|
"""Test wallet creation in daemon mode - success"""
|
|
mock_home.return_value = self.temp_dir
|
|
|
|
adapter = DualModeWalletAdapter(self.config, use_daemon=True)
|
|
|
|
# Mock daemon client
|
|
mock_client = Mock()
|
|
mock_client.is_available.return_value = True
|
|
mock_client.create_wallet.return_value = WalletInfo(
|
|
wallet_id="test-wallet",
|
|
public_key="0x123456",
|
|
address="aitbc1test",
|
|
created_at="2023-01-01T00:00:00Z"
|
|
)
|
|
adapter.daemon_client = mock_client
|
|
|
|
result = adapter.create_wallet("test-wallet", "password123", metadata={})
|
|
|
|
assert result["mode"] == "daemon"
|
|
assert result["wallet_name"] == "test-wallet"
|
|
assert result["wallet_id"] == "test-wallet"
|
|
mock_client.create_wallet.assert_called_once()
|
|
|
|
@patch('aitbc_cli.dual_mode_wallet_adapter.Path.home')
|
|
def test_create_wallet_daemon_mode_fallback(self, mock_home):
|
|
"""Test wallet creation in daemon mode - fallback to file"""
|
|
mock_home.return_value = self.temp_dir
|
|
|
|
adapter = DualModeWalletAdapter(self.config, use_daemon=True)
|
|
|
|
# Mock unavailable daemon
|
|
mock_client = Mock()
|
|
mock_client.is_available.return_value = False
|
|
adapter.daemon_client = mock_client
|
|
|
|
result = adapter.create_wallet("test-wallet", "password123", "hd")
|
|
|
|
assert result["mode"] == "file"
|
|
assert result["wallet_name"] == "test-wallet"
|
|
|
|
@patch('aitbc_cli.dual_mode_wallet_adapter.Path.home')
|
|
def test_list_wallets_file_mode(self, mock_home):
|
|
"""Test wallet listing in file mode"""
|
|
mock_home.return_value = self.temp_dir
|
|
|
|
# Create test wallets
|
|
wallet1_data = {
|
|
"name": "wallet1",
|
|
"address": "aitbc1wallet1",
|
|
"balance": 10.0,
|
|
"wallet_type": "hd",
|
|
"created_at": "2023-01-01T00:00:00Z"
|
|
}
|
|
|
|
wallet2_data = {
|
|
"name": "wallet2",
|
|
"address": "aitbc1wallet2",
|
|
"balance": 20.0,
|
|
"wallet_type": "simple",
|
|
"created_at": "2023-01-02T00:00:00Z"
|
|
}
|
|
|
|
with open(self.wallet_dir / "wallet1.json", "w") as f:
|
|
json.dump(wallet1_data, f)
|
|
with open(self.wallet_dir / "wallet2.json", "w") as f:
|
|
json.dump(wallet2_data, f)
|
|
|
|
adapter = DualModeWalletAdapter(self.config, use_daemon=False)
|
|
adapter.wallet_dir = self.wallet_dir
|
|
|
|
result = adapter.list_wallets()
|
|
|
|
assert len(result) == 2
|
|
assert result[0]["wallet_name"] == "wallet1"
|
|
assert result[0]["mode"] == "file"
|
|
assert result[1]["wallet_name"] == "wallet2"
|
|
assert result[1]["mode"] == "file"
|
|
|
|
|
|
class TestWalletCommands:
|
|
"""Test wallet commands with dual-mode support"""
|
|
|
|
def setup_method(self):
|
|
"""Set up test environment"""
|
|
self.runner = CliRunner()
|
|
self.temp_dir = Path(tempfile.mkdtemp())
|
|
|
|
def teardown_method(self):
|
|
"""Clean up test environment"""
|
|
shutil.rmtree(self.temp_dir)
|
|
|
|
@patch('aitbc_cli.commands.wallet.Path.home')
|
|
def test_wallet_create_file_mode(self, mock_home):
|
|
"""Test wallet creation command in file mode"""
|
|
mock_home.return_value = self.temp_dir
|
|
|
|
result = self.runner.invoke(wallet, [
|
|
'create', 'test-wallet', '--type', 'simple', '--no-encrypt'
|
|
])
|
|
|
|
assert result.exit_code == 0
|
|
assert 'Created file wallet' in result.output
|
|
|
|
@patch('aitbc_cli.commands.wallet.Path.home')
|
|
def test_wallet_create_daemon_mode_unavailable(self, mock_home):
|
|
"""Test wallet creation command in daemon mode when daemon unavailable"""
|
|
mock_home.return_value = self.temp_dir
|
|
|
|
result = self.runner.invoke(wallet, [
|
|
'--use-daemon', 'create', 'test-wallet', '--type', 'simple', '--no-encrypt'
|
|
])
|
|
|
|
assert result.exit_code == 0
|
|
assert 'Falling back to file-based wallet' in result.output
|
|
|
|
@patch('aitbc_cli.commands.wallet.Path.home')
|
|
def test_wallet_list_file_mode(self, mock_home):
|
|
"""Test wallet listing command in file mode"""
|
|
mock_home.return_value = self.temp_dir
|
|
|
|
# Create a test wallet first
|
|
wallet_dir = self.temp_dir / ".aitbc" / "wallets"
|
|
wallet_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
wallet_data = {
|
|
"name": "test-wallet",
|
|
"address": "aitbc1test",
|
|
"balance": 10.0,
|
|
"wallet_type": "hd",
|
|
"created_at": "2023-01-01T00:00:00Z"
|
|
}
|
|
|
|
with open(wallet_dir / "test-wallet.json", "w") as f:
|
|
json.dump(wallet_data, f)
|
|
|
|
result = self.runner.invoke(wallet, ['list'])
|
|
|
|
assert result.exit_code == 0
|
|
assert 'test-wallet' in result.output
|
|
|
|
@patch('aitbc_cli.commands.wallet.Path.home')
|
|
def test_wallet_balance_file_mode(self, mock_home):
|
|
"""Test wallet balance command in file mode"""
|
|
mock_home.return_value = self.temp_dir
|
|
|
|
# Create a test wallet first
|
|
wallet_dir = self.temp_dir / ".aitbc" / "wallets"
|
|
wallet_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
wallet_data = {
|
|
"name": "test-wallet",
|
|
"address": "aitbc1test",
|
|
"balance": 25.5,
|
|
"wallet_type": "hd",
|
|
"created_at": "2023-01-01T00:00:00Z"
|
|
}
|
|
|
|
with open(wallet_dir / "test-wallet.json", "w") as f:
|
|
json.dump(wallet_data, f)
|
|
|
|
result = self.runner.invoke(wallet, ['balance'])
|
|
|
|
assert result.exit_code == 0
|
|
assert '25.5' in result.output
|
|
|
|
|
|
class TestWalletMigrationService:
|
|
"""Test wallet migration service"""
|
|
|
|
def setup_method(self):
|
|
"""Set up test environment"""
|
|
self.temp_dir = Path(tempfile.mkdtemp())
|
|
self.config = Config()
|
|
self.config.config_dir = self.temp_dir
|
|
|
|
# Mock wallet directory
|
|
self.wallet_dir = self.temp_dir / "wallets"
|
|
self.wallet_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
def teardown_method(self):
|
|
"""Clean up test environment"""
|
|
shutil.rmtree(self.temp_dir)
|
|
|
|
@patch('aitbc_cli.wallet_migration_service.Path.home')
|
|
def test_migration_status_daemon_unavailable(self, mock_home):
|
|
"""Test migration status when daemon is unavailable"""
|
|
mock_home.return_value = self.temp_dir
|
|
|
|
migration_service = WalletMigrationService(self.config)
|
|
|
|
# Create test file wallet
|
|
wallet_data = {
|
|
"name": "test-wallet",
|
|
"address": "aitbc1test",
|
|
"balance": 10.0,
|
|
"wallet_type": "hd",
|
|
"created_at": "2023-01-01T00:00:00Z"
|
|
}
|
|
|
|
with open(self.wallet_dir / "test-wallet.json", "w") as f:
|
|
json.dump(wallet_data, f)
|
|
|
|
status = migration_service.get_migration_status()
|
|
|
|
assert status["daemon_available"] is False
|
|
assert status["total_file_wallets"] == 1
|
|
assert status["total_daemon_wallets"] == 0
|
|
assert "test-wallet" in status["file_only_wallets"]
|
|
|
|
@patch('aitbc_cli.wallet_migration_service.Path.home')
|
|
def test_migrate_to_daemon_success(self, mock_home):
|
|
"""Test migration to daemon - success"""
|
|
mock_home.return_value = self.temp_dir
|
|
|
|
migration_service = WalletMigrationService(self.config)
|
|
|
|
# Create test file wallet
|
|
wallet_data = {
|
|
"name": "test-wallet",
|
|
"address": "aitbc1test",
|
|
"balance": 10.0,
|
|
"wallet_type": "hd",
|
|
"created_at": "2023-01-01T00:00:00Z",
|
|
"transactions": []
|
|
}
|
|
|
|
with open(self.wallet_dir / "test-wallet.json", "w") as f:
|
|
json.dump(wallet_data, f)
|
|
|
|
# Mock successful daemon migration
|
|
mock_adapter = Mock()
|
|
mock_adapter.is_daemon_available.return_value = True
|
|
mock_adapter.get_wallet_info.return_value = None # Wallet doesn't exist in daemon
|
|
mock_adapter.create_wallet.return_value = {
|
|
"wallet_id": "test-wallet",
|
|
"public_key": "0x123456",
|
|
"address": "aitbc1test"
|
|
}
|
|
migration_service.daemon_adapter = mock_adapter
|
|
|
|
result = migration_service.migrate_to_daemon("test-wallet", "password123")
|
|
|
|
assert result["wallet_name"] == "test-wallet"
|
|
assert result["source_mode"] == "file"
|
|
assert result["target_mode"] == "daemon"
|
|
assert result["original_balance"] == 10.0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
pytest.main([__file__])
|