Files
aitbc/tests/integration/test_blockchain_nodes.py
oib fb4b9a2e49 refactor: clean up tests/ root — delete junk, sort into subdirs
Deleted (7 files):
- test_discovery.py, test_windsurf_integration.py (trivial assert True stubs)
- pytest_simple.ini (unused --collect-only config)
- conftest_path.py (duplicate of conftest.py path setup)
- conftest_fixtures.py, conftest_full.py (unused conftest variants)

Moved to integration/ (6 files):
- test_blockchain_final.py, test_blockchain_nodes.py, test_blockchain_simple.py
- test_basic_integration.py, test_integration_simple.py, test_working_integration.py

Moved to fixtures/:
- mock_blockchain_node.py

tests/ root now has only conftest.py and README.md.
2026-02-13 23:32:58 +01:00

327 lines
12 KiB
Python

#!/usr/bin/env python3
"""
Test script for AITBC blockchain nodes
Tests both nodes for functionality and consistency
"""
import httpx
import json
import time
import sys
from typing import Dict, Any, Optional
# Configuration
NODES = {
"node1": {"url": "http://127.0.0.1:8082", "name": "Node 1"},
"node2": {"url": "http://127.0.0.1:8081", "name": "Node 2"},
}
# Test addresses
TEST_ADDRESSES = {
"alice": "aitbc1alice00000000000000000000000000000000000",
"bob": "aitbc1bob0000000000000000000000000000000000000",
"charlie": "aitbc1charl0000000000000000000000000000000000",
}
def print_header(message: str):
"""Print test header"""
print(f"\n{'='*60}")
print(f" {message}")
print(f"{'='*60}")
def print_step(message: str):
"""Print test step"""
print(f"\n{message}")
def print_success(message: str):
"""Print success message"""
print(f"{message}")
def print_error(message: str):
"""Print error message"""
print(f"{message}")
def print_warning(message: str):
"""Print warning message"""
print(f"⚠️ {message}")
def check_node_health(node_name: str, node_config: Dict[str, str]) -> bool:
"""Check if node is responsive"""
try:
response = httpx.get(f"{node_config['url']}/openapi.json", timeout=5)
if response.status_code == 200:
print_success(f"{node_config['name']} is responsive")
return True
else:
print_error(f"{node_config['name']} returned status {response.status_code}")
return False
except Exception as e:
print_error(f"{node_config['name']} is not responding: {e}")
return False
def get_chain_head(node_name: str, node_config: Dict[str, str]) -> Optional[Dict[str, Any]]:
"""Get current chain head from node"""
try:
response = httpx.get(f"{node_config['url']}/rpc/head", timeout=5)
if response.status_code == 200:
return response.json()
else:
print_error(f"Failed to get chain head from {node_config['name']}: {response.status_code}")
return None
except Exception as e:
print_error(f"Error getting chain head from {node_config['name']}: {e}")
return None
def get_balance(node_name: str, node_config: Dict[str, str], address: str) -> Optional[int]:
"""Get balance for an address"""
try:
response = httpx.get(f"{node_config['url']}/rpc/getBalance/{address}", timeout=5)
if response.status_code == 200:
data = response.json()
return data.get("balance", 0)
else:
print_error(f"Failed to get balance from {node_config['name']}: {response.status_code}")
return None
except Exception as e:
print_error(f"Error getting balance from {node_config['name']}: {e}")
return None
def mint_faucet(node_name: str, node_config: Dict[str, str], address: str, amount: int) -> bool:
"""Mint tokens to an address (devnet only)"""
try:
response = httpx.post(
f"{node_config['url']}/rpc/admin/mintFaucet",
json={"address": address, "amount": amount},
timeout=5
)
if response.status_code == 200:
print_success(f"Minted {amount} tokens to {address} on {node_config['name']}")
return True
else:
print_error(f"Failed to mint on {node_config['name']}: {response.status_code}")
print(f"Response: {response.text}")
return False
except Exception as e:
print_error(f"Error minting on {node_config['name']}: {e}")
return False
def send_transaction(node_name: str, node_config: Dict[str, str], tx: Dict[str, Any]) -> Optional[str]:
"""Send a transaction"""
try:
response = httpx.post(
f"{node_config['url']}/rpc/sendTx",
json=tx,
timeout=5
)
if response.status_code == 200:
data = response.json()
return data.get("tx_hash")
else:
print_error(f"Failed to send transaction on {node_config['name']}: {response.status_code}")
print(f"Response: {response.text}")
return None
except Exception as e:
print_error(f"Error sending transaction on {node_config['name']}: {e}")
return None
def wait_for_block(node_name: str, node_config: Dict[str, str], target_height: int, timeout: int = 30) -> bool:
"""Wait for node to reach a target block height"""
start_time = time.time()
while time.time() - start_time < timeout:
head = get_chain_head(node_name, node_config)
if head and head.get("height", 0) >= target_height:
return True
time.sleep(1)
return False
def test_node_connectivity():
"""Test if both nodes are running and responsive"""
print_header("Testing Node Connectivity")
all_healthy = True
for node_name, node_config in NODES.items():
if not check_node_health(node_name, node_config):
all_healthy = False
assert all_healthy, "Not all nodes are healthy"
def test_chain_consistency():
"""Test if both nodes have consistent chain heads"""
print_header("Testing Chain Consistency")
heads = {}
for node_name, node_config in NODES.items():
print_step(f"Getting chain head from {node_config['name']}")
head = get_chain_head(node_name, node_config)
if head:
heads[node_name] = head
print(f" Height: {head.get('height', 'unknown')}")
print(f" Hash: {head.get('hash', 'unknown')[:16]}...")
else:
print_error(f"Failed to get chain head from {node_config['name']}")
if len(heads) == len(NODES):
# Compare heights
heights = [head.get("height", 0) for head in heads.values()]
if len(set(heights)) == 1:
print_success("Both nodes have the same block height")
else:
print_error(f"Node heights differ: {heights}")
# Compare hashes
hashes = [head.get("hash", "") for head in heads.values()]
if len(set(hashes)) == 1:
print_success("Both nodes have the same chain hash")
else:
print_warning("Nodes have different chain hashes (may be syncing)")
assert len(heads) == len(NODES), "Failed to get chain heads from all nodes"
def test_faucet_and_balances():
"""Test faucet minting and balance queries"""
print_header("Testing Faucet and Balances")
# Test on node1
print_step("Testing faucet on Node 1")
if mint_faucet("node1", NODES["node1"], TEST_ADDRESSES["alice"], 1000):
time.sleep(2) # Wait for block
# Check balance on both nodes
for node_name, node_config in NODES.items():
balance = get_balance(node_name, node_config, TEST_ADDRESSES["alice"])
if balance is not None:
print(f" {node_config['name']} balance for alice: {balance}")
if balance >= 1000:
print_success(f"Balance correct on {node_config['name']}")
else:
print_error(f"Balance incorrect on {node_config['name']}")
else:
print_error(f"Failed to get balance from {node_config['name']}")
# Test on node2
print_step("Testing faucet on Node 2")
if mint_faucet("node2", NODES["node2"], TEST_ADDRESSES["bob"], 500):
time.sleep(2) # Wait for block
# Check balance on both nodes
for node_name, node_config in NODES.items():
balance = get_balance(node_name, node_config, TEST_ADDRESSES["bob"])
if balance is not None:
print(f" {node_config['name']} balance for bob: {balance}")
if balance >= 500:
print_success(f"Balance correct on {node_config['name']}")
else:
print_error(f"Balance incorrect on {node_config['name']}")
else:
print_error(f"Failed to get balance from {node_config['name']}")
def test_transaction_submission():
"""Test transaction submission between addresses"""
print_header("Testing Transaction Submission")
# First ensure alice has funds
print_step("Ensuring alice has funds")
mint_faucet("node1", NODES["node1"], TEST_ADDRESSES["alice"], 2000)
time.sleep(2)
# Create a transfer transaction (simplified - normally needs proper signing)
print_step("Submitting transfer transaction")
tx = {
"type": "TRANSFER",
"sender": TEST_ADDRESSES["alice"],
"nonce": 0,
"fee": 10,
"payload": {
"to": TEST_ADDRESSES["bob"],
"amount": 100
},
"sig": None # In devnet, signature might be optional
}
tx_hash = send_transaction("node1", NODES["node1"], tx)
if tx_hash:
print_success(f"Transaction submitted: {tx_hash[:16]}...")
time.sleep(3) # Wait for inclusion
# Check final balances
print_step("Checking final balances")
for node_name, node_config in NODES.items():
alice_balance = get_balance(node_name, node_config, TEST_ADDRESSES["alice"])
bob_balance = get_balance(node_name, node_config, TEST_ADDRESSES["bob"])
if alice_balance is not None and bob_balance is not None:
print(f" {node_config['name']}: alice={alice_balance}, bob={bob_balance}")
else:
print_error("Failed to submit transaction")
def test_block_production():
"""Test that nodes are producing blocks"""
print_header("Testing Block Production")
initial_heights = {}
for node_name, node_config in NODES.items():
head = get_chain_head(node_name, node_config)
if head:
initial_heights[node_name] = head.get("height", 0)
print(f" {node_config['name']} initial height: {initial_heights[node_name]}")
print_step("Waiting for new blocks...")
time.sleep(10) # Wait for block production (2s block time)
final_heights = {}
for node_name, node_config in NODES.items():
head = get_chain_head(node_name, node_config)
if head:
final_heights[node_name] = head.get("height", 0)
print(f" {node_config['name']} final height: {final_heights[node_name]}")
# Check if blocks were produced
for node_name in NODES:
if node_name in initial_heights and node_name in final_heights:
produced = final_heights[node_name] - initial_heights[node_name]
if produced > 0:
print_success(f"{NODES[node_name]['name']} produced {produced} block(s)")
else:
print_error(f"{NODES[node_name]['name']} produced no blocks")
def main():
"""Run all tests"""
print_header("AITBC Blockchain Node Test Suite")
tests = [
("Node Connectivity", test_node_connectivity),
("Chain Consistency", test_chain_consistency),
("Faucet and Balances", test_faucet_and_balances),
("Transaction Submission", test_transaction_submission),
("Block Production", test_block_production),
]
results = {}
for test_name, test_func in tests:
try:
results[test_name] = test_func()
except Exception as e:
print_error(f"Test '{test_name}' failed with exception: {e}")
results[test_name] = False
# Summary
print_header("Test Summary")
passed = sum(1 for result in results.values() if result)
total = len(results)
for test_name, result in results.items():
status = "✅ PASSED" if result else "❌ FAILED"
print(f"{test_name:.<40} {status}")
print(f"\nOverall: {passed}/{total} tests passed")
if passed == total:
print_success("All tests passed! 🎉")
return 0
else:
print_error("Some tests failed. Check the logs above.")
return 1
if __name__ == "__main__":
sys.exit(main())