refactor: consolidate blockchain explorer into single app and update backup ignore patterns
- Remove standalone explorer-web app (README, HTML, package files) - Add /web endpoint to blockchain-explorer for web interface access - Update .gitignore to exclude application backup archives (*.tar.gz, *.zip) - Add backup documentation files to .gitignore (BACKUP_INDEX.md, README.md) - Consolidate explorer functionality into main blockchain-explorer application
This commit is contained in:
259
cli/tests/utils/command_tester.py
Normal file
259
cli/tests/utils/command_tester.py
Normal file
@@ -0,0 +1,259 @@
|
||||
"""
|
||||
Command tester utility for AITBC CLI testing
|
||||
"""
|
||||
|
||||
import time
|
||||
from typing import List, Dict, Any, Optional, Callable
|
||||
from click.testing import CliRunner
|
||||
from .test_helpers import CommandTestResult, run_command_test, TestEnvironment
|
||||
|
||||
|
||||
class CommandTester:
|
||||
"""Enhanced command tester for AITBC CLI"""
|
||||
|
||||
def __init__(self, cli_app):
|
||||
self.runner = CliRunner()
|
||||
self.cli = cli_app
|
||||
self.test_env = TestEnvironment()
|
||||
self.results: List[CommandTestResult] = []
|
||||
self.setup_mocks()
|
||||
|
||||
def setup_mocks(self):
|
||||
"""Setup common test mocks"""
|
||||
self.mocks = setup_test_mocks(self.test_env)
|
||||
|
||||
# Setup default API responses
|
||||
self.api_responses = mock_api_responses()
|
||||
|
||||
# Configure default mock responses
|
||||
if 'httpx' in self.mocks:
|
||||
self.mocks['httpx'].return_value = MockApiResponse.success_response(
|
||||
self.api_responses['blockchain_info']
|
||||
)
|
||||
|
||||
def cleanup(self):
|
||||
"""Cleanup test environment"""
|
||||
self.test_env.cleanup()
|
||||
|
||||
def run_command(self, command_args: List[str],
|
||||
expected_exit_code: int = 0,
|
||||
expected_text: str = None,
|
||||
timeout: int = 30) -> CommandTestResult:
|
||||
"""Run a command test"""
|
||||
result = run_command_test(
|
||||
self.runner, command_args, expected_exit_code, expected_text, timeout
|
||||
)
|
||||
self.results.append(result)
|
||||
return result
|
||||
|
||||
def test_command_help(self, command: str, subcommand: str = None) -> CommandTestResult:
|
||||
"""Test command help"""
|
||||
args = [command, '--help']
|
||||
if subcommand:
|
||||
args.insert(1, subcommand)
|
||||
|
||||
return self.run_command(args, expected_text='Usage:')
|
||||
|
||||
def test_command_group(self, group_name: str, subcommands: List[str] = None) -> Dict[str, CommandTestResult]:
|
||||
"""Test a command group and its subcommands"""
|
||||
results = {}
|
||||
|
||||
# Test main group help
|
||||
results[f"{group_name}_help"] = self.test_command_help(group_name)
|
||||
|
||||
# Test subcommands if provided
|
||||
if subcommands:
|
||||
for subcmd in subcommands:
|
||||
results[f"{group_name}_{subcmd}"] = self.test_command_help(group_name, subcmd)
|
||||
|
||||
return results
|
||||
|
||||
def test_config_commands(self) -> Dict[str, CommandTestResult]:
|
||||
"""Test configuration commands"""
|
||||
results = {}
|
||||
|
||||
# Test config show
|
||||
results['config_show'] = self.run_command(['config', 'show'])
|
||||
|
||||
# Test config set
|
||||
results['config_set'] = self.run_command(['config', 'set', 'test_key', 'test_value'])
|
||||
|
||||
# Test config get
|
||||
results['config_get'] = self.run_command(['config', 'get', 'test_key'])
|
||||
|
||||
# Test config environments
|
||||
results['config_environments'] = self.run_command(['config', 'environments'])
|
||||
|
||||
return results
|
||||
|
||||
def test_auth_commands(self) -> Dict[str, CommandTestResult]:
|
||||
"""Test authentication commands"""
|
||||
results = {}
|
||||
|
||||
# Test auth status
|
||||
results['auth_status'] = self.run_command(['auth', 'status'])
|
||||
|
||||
# Test auth login
|
||||
results['auth_login'] = self.run_command(['auth', 'login', 'test-api-key-12345'])
|
||||
|
||||
# Test auth logout
|
||||
results['auth_logout'] = self.run_command(['auth', 'logout'])
|
||||
|
||||
return results
|
||||
|
||||
def test_wallet_commands(self) -> Dict[str, CommandTestResult]:
|
||||
"""Test wallet commands"""
|
||||
results = {}
|
||||
|
||||
# Create mock wallet directory
|
||||
wallet_dir = self.test_env.create_mock_wallet_dir()
|
||||
self.mocks['home'].return_value = wallet_dir
|
||||
|
||||
# Test wallet list
|
||||
results['wallet_list'] = self.run_command(['--test-mode', 'wallet', 'list'])
|
||||
|
||||
# Test wallet create (mock password)
|
||||
with patch('getpass.getpass') as mock_getpass:
|
||||
mock_getpass.return_value = 'test-password'
|
||||
results['wallet_create'] = self.run_command(['--test-mode', 'wallet', 'create', 'test-wallet'])
|
||||
|
||||
return results
|
||||
|
||||
def test_blockchain_commands(self) -> Dict[str, CommandTestResult]:
|
||||
"""Test blockchain commands"""
|
||||
results = {}
|
||||
|
||||
# Setup blockchain API mocks
|
||||
self.mocks['httpx'].return_value = MockApiResponse.success_response(
|
||||
self.api_responses['blockchain_info']
|
||||
)
|
||||
|
||||
# Test blockchain info
|
||||
results['blockchain_info'] = self.run_command(['--test-mode', 'blockchain', 'info'])
|
||||
|
||||
# Test blockchain status
|
||||
self.mocks['httpx'].return_value = MockApiResponse.success_response(
|
||||
self.api_responses['blockchain_status']
|
||||
)
|
||||
results['blockchain_status'] = self.run_command(['--test-mode', 'blockchain', 'status'])
|
||||
|
||||
return results
|
||||
|
||||
def test_utility_commands(self) -> Dict[str, CommandTestResult]:
|
||||
"""Test utility commands"""
|
||||
results = {}
|
||||
|
||||
# Test version
|
||||
results['version'] = self.run_command(['version'])
|
||||
|
||||
# Test main help
|
||||
results['help'] = self.run_command(['--help'])
|
||||
|
||||
return results
|
||||
|
||||
def run_comprehensive_test(self) -> Dict[str, Dict[str, CommandTestResult]]:
|
||||
"""Run comprehensive test suite"""
|
||||
print("🚀 Running Comprehensive AITBC CLI Test Suite")
|
||||
|
||||
all_results = {}
|
||||
|
||||
# Test core command groups
|
||||
print("\n📂 Testing Core Command Groups...")
|
||||
all_results['config'] = self.test_config_commands()
|
||||
all_results['auth'] = self.test_auth_commands()
|
||||
all_results['wallet'] = self.test_wallet_commands()
|
||||
all_results['blockchain'] = self.test_blockchain_commands()
|
||||
all_results['utility'] = self.test_utility_commands()
|
||||
|
||||
return all_results
|
||||
|
||||
def print_results_summary(self, results: Dict[str, Dict[str, CommandTestResult]]):
|
||||
"""Print comprehensive results summary"""
|
||||
print("\n" + "="*80)
|
||||
print("📊 COMPREHENSIVE TEST RESULTS")
|
||||
print("="*80)
|
||||
|
||||
total_tests = 0
|
||||
total_passed = 0
|
||||
total_failed = 0
|
||||
|
||||
for category, tests in results.items():
|
||||
print(f"\n📂 {category.upper()} COMMANDS")
|
||||
print("-"*40)
|
||||
|
||||
category_passed = 0
|
||||
category_total = len(tests)
|
||||
|
||||
for test_name, result in tests.items():
|
||||
total_tests += 1
|
||||
if result.success:
|
||||
total_passed += 1
|
||||
category_passed += 1
|
||||
else:
|
||||
total_failed += 1
|
||||
|
||||
print(f" {result}")
|
||||
if not result.success and result.error:
|
||||
print(f" Error: {result.error}")
|
||||
|
||||
success_rate = (category_passed / category_total * 100) if category_total > 0 else 0
|
||||
print(f"\n Category Success: {category_passed}/{category_total} ({success_rate:.1f}%)")
|
||||
|
||||
# Overall summary
|
||||
print("\n" + "="*80)
|
||||
print("🎯 OVERALL SUMMARY")
|
||||
print("="*80)
|
||||
print(f"Total Tests: {total_tests}")
|
||||
print(f"✅ Passed: {total_passed}")
|
||||
print(f"❌ Failed: {total_failed}")
|
||||
|
||||
overall_success_rate = (total_passed / total_tests * 100) if total_tests > 0 else 0
|
||||
print(f"🎯 Success Rate: {overall_success_rate:.1f}%")
|
||||
|
||||
if overall_success_rate >= 90:
|
||||
print("🎉 EXCELLENT: CLI is in excellent condition!")
|
||||
elif overall_success_rate >= 75:
|
||||
print("👍 GOOD: CLI is in good condition")
|
||||
elif overall_success_rate >= 50:
|
||||
print("⚠️ FAIR: CLI needs some attention")
|
||||
else:
|
||||
print("🚨 POOR: CLI needs immediate attention")
|
||||
|
||||
return total_failed == 0
|
||||
|
||||
|
||||
# Import necessary functions and classes
|
||||
from .test_helpers import (
|
||||
MockConfig, MockApiResponse, TestEnvironment,
|
||||
mock_api_responses, setup_test_mocks
|
||||
)
|
||||
|
||||
# Mock API responses function that was missing
|
||||
def mock_api_responses():
|
||||
"""Common mock API responses for testing"""
|
||||
return {
|
||||
'blockchain_info': {
|
||||
'chain_id': 'ait-devnet',
|
||||
'height': 1000,
|
||||
'hash': '0x1234567890abcdef',
|
||||
'timestamp': '2026-01-01T00:00:00Z'
|
||||
},
|
||||
'blockchain_status': {
|
||||
'status': 'syncing',
|
||||
'height': 1000,
|
||||
'peers': 5,
|
||||
'sync_progress': 85.5
|
||||
},
|
||||
'wallet_balance': {
|
||||
'address': 'test-address',
|
||||
'balance': 1000.0,
|
||||
'unlocked': 800.0,
|
||||
'staked': 200.0
|
||||
},
|
||||
'node_info': {
|
||||
'id': 'test-node',
|
||||
'address': 'localhost:8006',
|
||||
'status': 'active',
|
||||
'chains': ['ait-devnet']
|
||||
}
|
||||
}
|
||||
267
cli/tests/utils/test_helpers.py
Normal file
267
cli/tests/utils/test_helpers.py
Normal file
@@ -0,0 +1,267 @@
|
||||
"""
|
||||
Test utilities and helpers for AITBC CLI testing
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
import json
|
||||
from pathlib import Path
|
||||
from unittest.mock import MagicMock, patch
|
||||
from typing import Dict, Any, Optional
|
||||
|
||||
|
||||
class MockConfig:
|
||||
"""Mock configuration for testing"""
|
||||
|
||||
def __init__(self, coordinator_url: str = "http://localhost:8000",
|
||||
api_key: str = "test-key"):
|
||||
self.coordinator_url = coordinator_url
|
||||
self.api_key = api_key
|
||||
self.timeout = 30
|
||||
self.blockchain_rpc_url = "http://localhost:8006"
|
||||
self.wallet_url = "http://localhost:8002"
|
||||
self.role = None
|
||||
self.config_dir = Path(tempfile.mkdtemp()) / ".aitbc"
|
||||
self.config_file = None
|
||||
|
||||
|
||||
class MockApiResponse:
|
||||
"""Mock API response for testing"""
|
||||
|
||||
@staticmethod
|
||||
def success_response(data: Dict[str, Any]) -> MagicMock:
|
||||
"""Create a successful API response mock"""
|
||||
response = MagicMock()
|
||||
response.status_code = 200
|
||||
response.json.return_value = data
|
||||
response.text = json.dumps(data)
|
||||
return response
|
||||
|
||||
@staticmethod
|
||||
def error_response(status_code: int, message: str) -> MagicMock:
|
||||
"""Create an error API response mock"""
|
||||
response = MagicMock()
|
||||
response.status_code = status_code
|
||||
response.json.return_value = {"error": message}
|
||||
response.text = message
|
||||
return response
|
||||
|
||||
|
||||
class TestEnvironment:
|
||||
"""Test environment manager"""
|
||||
|
||||
def __init__(self):
|
||||
self.temp_dirs = []
|
||||
self.mock_patches = []
|
||||
|
||||
def create_temp_dir(self, prefix: str = "aitbc_test_") -> Path:
|
||||
"""Create a temporary directory"""
|
||||
temp_dir = Path(tempfile.mkdtemp(prefix=prefix))
|
||||
self.temp_dirs.append(temp_dir)
|
||||
return temp_dir
|
||||
|
||||
def create_mock_wallet_dir(self) -> Path:
|
||||
"""Create a mock wallet directory"""
|
||||
wallet_dir = self.create_temp_dir("wallet_")
|
||||
(wallet_dir / "wallets").mkdir(exist_ok=True)
|
||||
return wallet_dir
|
||||
|
||||
def create_mock_config_dir(self) -> Path:
|
||||
"""Create a mock config directory"""
|
||||
config_dir = self.create_temp_dir("config_")
|
||||
config_dir.mkdir(exist_ok=True)
|
||||
return config_dir
|
||||
|
||||
def add_patch(self, patch_obj):
|
||||
"""Add a patch to be cleaned up later"""
|
||||
self.mock_patches.append(patch_obj)
|
||||
|
||||
def cleanup(self):
|
||||
"""Clean up all temporary resources"""
|
||||
# Stop all patches
|
||||
for patch_obj in self.mock_patches:
|
||||
try:
|
||||
patch_obj.stop()
|
||||
except:
|
||||
pass
|
||||
|
||||
# Remove temp directories
|
||||
for temp_dir in self.temp_dirs:
|
||||
try:
|
||||
import shutil
|
||||
shutil.rmtree(temp_dir)
|
||||
except:
|
||||
pass
|
||||
|
||||
self.temp_dirs.clear()
|
||||
self.mock_patches.clear()
|
||||
|
||||
|
||||
def create_test_wallet(wallet_dir: Path, name: str, address: str = "test-address") -> Dict[str, Any]:
|
||||
"""Create a test wallet file"""
|
||||
wallet_data = {
|
||||
"name": name,
|
||||
"address": address,
|
||||
"balance": 1000.0,
|
||||
"created_at": "2026-01-01T00:00:00Z",
|
||||
"encrypted": False
|
||||
}
|
||||
|
||||
wallet_file = wallet_dir / "wallets" / f"{name}.json"
|
||||
wallet_file.parent.mkdir(exist_ok=True)
|
||||
|
||||
with open(wallet_file, 'w') as f:
|
||||
json.dump(wallet_data, f, indent=2)
|
||||
|
||||
return wallet_data
|
||||
|
||||
|
||||
def create_test_config(config_dir: Path, coordinator_url: str = "http://localhost:8000") -> Dict[str, Any]:
|
||||
"""Create a test configuration file"""
|
||||
config_data = {
|
||||
"coordinator_url": coordinator_url,
|
||||
"api_key": "test-api-key",
|
||||
"timeout": 30,
|
||||
"blockchain_rpc_url": "http://localhost:8006",
|
||||
"wallet_url": "http://localhost:8002"
|
||||
}
|
||||
|
||||
config_file = config_dir / "config.yaml"
|
||||
with open(config_file, 'w') as f:
|
||||
import yaml
|
||||
yaml.dump(config_data, f, default_flow_style=False)
|
||||
|
||||
return config_data
|
||||
|
||||
|
||||
def mock_api_responses():
|
||||
"""Common mock API responses for testing"""
|
||||
return {
|
||||
'blockchain_info': {
|
||||
'chain_id': 'ait-devnet',
|
||||
'height': 1000,
|
||||
'hash': '0x1234567890abcdef',
|
||||
'timestamp': '2026-01-01T00:00:00Z'
|
||||
},
|
||||
'blockchain_status': {
|
||||
'status': 'syncing',
|
||||
'height': 1000,
|
||||
'peers': 5,
|
||||
'sync_progress': 85.5
|
||||
},
|
||||
'wallet_balance': {
|
||||
'address': 'test-address',
|
||||
'balance': 1000.0,
|
||||
'unlocked': 800.0,
|
||||
'staked': 200.0
|
||||
},
|
||||
'node_info': {
|
||||
'id': 'test-node',
|
||||
'address': 'localhost:8006',
|
||||
'status': 'active',
|
||||
'chains': ['ait-devnet']
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def setup_test_mocks(test_env: TestEnvironment):
|
||||
"""Setup common test mocks"""
|
||||
mocks = {}
|
||||
|
||||
# Mock home directory
|
||||
mock_home = patch('aitbc_cli.commands.wallet.Path.home')
|
||||
mocks['home'] = mock_home.start()
|
||||
mocks['home'].return_value = test_env.create_temp_dir("home_")
|
||||
test_env.add_patch(mock_home)
|
||||
|
||||
# Mock config loading
|
||||
mock_config_load = patch('aitbc_cli.config.Config.load_from_file')
|
||||
mocks['config_load'] = mock_config_load.start()
|
||||
mocks['config_load'].return_value = MockConfig()
|
||||
test_env.add_patch(mock_config_load)
|
||||
|
||||
# Mock API calls
|
||||
mock_httpx = patch('httpx.get')
|
||||
mocks['httpx'] = mock_httpx.start()
|
||||
test_env.add_patch(mock_httpx)
|
||||
|
||||
# Mock authentication
|
||||
mock_auth = patch('aitbc_cli.auth.AuthManager')
|
||||
mocks['auth'] = mock_auth.start()
|
||||
test_env.add_patch(mock_auth)
|
||||
|
||||
return mocks
|
||||
|
||||
|
||||
class CommandTestResult:
|
||||
"""Result of a command test"""
|
||||
|
||||
def __init__(self, command: str, exit_code: int, output: str,
|
||||
error: str = None, duration: float = 0.0):
|
||||
self.command = command
|
||||
self.exit_code = exit_code
|
||||
self.output = output
|
||||
self.error = error
|
||||
self.duration = duration
|
||||
self.success = exit_code == 0
|
||||
|
||||
def __str__(self):
|
||||
status = "✅ PASS" if self.success else "❌ FAIL"
|
||||
return f"{status} [{self.exit_code}] {self.command}"
|
||||
|
||||
def contains(self, text: str) -> bool:
|
||||
"""Check if output contains text"""
|
||||
return text in self.output
|
||||
|
||||
def contains_any(self, texts: list) -> bool:
|
||||
"""Check if output contains any of the texts"""
|
||||
return any(text in self.output for text in texts)
|
||||
|
||||
|
||||
def run_command_test(runner, command_args: list,
|
||||
expected_exit_code: int = 0,
|
||||
expected_text: str = None,
|
||||
timeout: int = 30) -> CommandTestResult:
|
||||
"""Run a command test with validation"""
|
||||
import time
|
||||
|
||||
start_time = time.time()
|
||||
result = runner.invoke(command_args)
|
||||
duration = time.time() - start_time
|
||||
|
||||
test_result = CommandTestResult(
|
||||
command=' '.join(command_args),
|
||||
exit_code=result.exit_code,
|
||||
output=result.output,
|
||||
error=result.stderr,
|
||||
duration=duration
|
||||
)
|
||||
|
||||
# Validate expected exit code
|
||||
if result.exit_code != expected_exit_code:
|
||||
print(f"⚠️ Expected exit code {expected_exit_code}, got {result.exit_code}")
|
||||
|
||||
# Validate expected text
|
||||
if expected_text and expected_text not in result.output:
|
||||
print(f"⚠️ Expected text '{expected_text}' not found in output")
|
||||
|
||||
return test_result
|
||||
|
||||
|
||||
def print_test_header(title: str):
|
||||
"""Print a test header"""
|
||||
print(f"\n{'='*60}")
|
||||
print(f"🧪 {title}")
|
||||
print('='*60)
|
||||
|
||||
|
||||
def print_test_footer(title: str, passed: int, failed: int, total: int):
|
||||
"""Print a test footer"""
|
||||
print(f"\n{'-'*60}")
|
||||
print(f"📊 {title} Results: {passed}/{total} passed ({passed/total*100:.1f}%)")
|
||||
if failed > 0:
|
||||
print(f"❌ {failed} test(s) failed")
|
||||
else:
|
||||
print("🎉 All tests passed!")
|
||||
print('-'*60)
|
||||
Reference in New Issue
Block a user