A complete blockchain implementation featuring: - synord: Full node with GHOSTDAG consensus - explorer-web: Modern React blockchain explorer with 3D DAG visualization - CLI wallet and tools - Smart contract SDK and example contracts (DEX, NFT, token) - WASM crypto library for browser/mobile
364 lines
10 KiB
Rust
364 lines
10 KiB
Rust
//! Integration tests for SynorNode lifecycle.
|
|
//!
|
|
//! These tests verify:
|
|
//! - Node creation and configuration
|
|
//! - Service startup and shutdown
|
|
//! - State transitions
|
|
//! - Basic RPC connectivity
|
|
//! - Error handling and recovery
|
|
|
|
use std::path::PathBuf;
|
|
use std::time::Duration;
|
|
|
|
use tempfile::TempDir;
|
|
use tokio::time::timeout;
|
|
|
|
use synord::config::NodeConfig;
|
|
use synord::node::{NodeState, SynorNode};
|
|
|
|
/// Test timeout for async operations.
|
|
const TEST_TIMEOUT: Duration = Duration::from_secs(30);
|
|
|
|
/// Creates a test configuration with a temporary data directory.
|
|
fn create_test_config(temp_dir: &TempDir) -> NodeConfig {
|
|
let mut config = NodeConfig::for_network("devnet").unwrap();
|
|
|
|
// Use temporary directory
|
|
config.data_dir = temp_dir.path().to_path_buf();
|
|
|
|
// Disable mining for most tests
|
|
config.mining.enabled = false;
|
|
|
|
// Use random ports to avoid conflicts
|
|
let port_base = 16000 + (std::process::id() % 1000) as u16;
|
|
config.p2p.listen_addr = format!("127.0.0.1:{}", port_base);
|
|
config.rpc.http_addr = format!("127.0.0.1:{}", port_base + 10);
|
|
config.rpc.ws_addr = format!("127.0.0.1:{}", port_base + 11);
|
|
|
|
// No seeds for isolated testing
|
|
config.p2p.seeds = vec![];
|
|
|
|
config
|
|
}
|
|
|
|
// ==================== Configuration Tests ====================
|
|
|
|
#[test]
|
|
fn test_config_for_networks() {
|
|
// Mainnet
|
|
let config = NodeConfig::for_network("mainnet").unwrap();
|
|
assert_eq!(config.chain_id, 1);
|
|
assert_eq!(config.network, "mainnet");
|
|
|
|
// Testnet
|
|
let config = NodeConfig::for_network("testnet").unwrap();
|
|
assert_eq!(config.chain_id, 2);
|
|
assert_eq!(config.network, "testnet");
|
|
|
|
// Devnet
|
|
let config = NodeConfig::for_network("devnet").unwrap();
|
|
assert_eq!(config.chain_id, 3);
|
|
assert_eq!(config.network, "devnet");
|
|
}
|
|
|
|
#[test]
|
|
fn test_config_unknown_network() {
|
|
let result = NodeConfig::for_network("unknown");
|
|
assert!(result.is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn test_config_save_and_load() {
|
|
let temp_dir = TempDir::new().unwrap();
|
|
let config_path = temp_dir.path().join("config.toml");
|
|
|
|
let config = NodeConfig::for_network("devnet").unwrap();
|
|
config.save(&config_path).unwrap();
|
|
|
|
let loaded = NodeConfig::load(&config_path).unwrap();
|
|
assert_eq!(loaded.network, config.network);
|
|
assert_eq!(loaded.chain_id, config.chain_id);
|
|
}
|
|
|
|
#[test]
|
|
fn test_config_paths() {
|
|
let temp_dir = TempDir::new().unwrap();
|
|
let config = create_test_config(&temp_dir);
|
|
|
|
assert_eq!(config.blocks_path(), temp_dir.path().join("blocks"));
|
|
assert_eq!(config.chainstate_path(), temp_dir.path().join("chainstate"));
|
|
assert_eq!(config.contracts_path(), temp_dir.path().join("contracts"));
|
|
assert_eq!(config.keys_path(), temp_dir.path().join("keys"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_config_with_builders() {
|
|
let config = NodeConfig::for_network("devnet")
|
|
.unwrap()
|
|
.with_data_dir(Some(PathBuf::from("/tmp/test")))
|
|
.with_rpc("0.0.0.0", 8080, 8081)
|
|
.with_p2p("0.0.0.0", 9000, vec!["peer1:9000".to_string()])
|
|
.with_mining(true, Some("synor1abc...".to_string()), 4);
|
|
|
|
assert_eq!(config.data_dir, PathBuf::from("/tmp/test"));
|
|
assert_eq!(config.rpc.http_addr, "0.0.0.0:8080");
|
|
assert_eq!(config.rpc.ws_addr, "0.0.0.0:8081");
|
|
assert_eq!(config.p2p.listen_addr, "0.0.0.0:9000");
|
|
assert!(config.mining.enabled);
|
|
assert_eq!(config.mining.threads, 4);
|
|
}
|
|
|
|
// ==================== Node Lifecycle Tests ====================
|
|
|
|
#[tokio::test]
|
|
async fn test_node_creation() {
|
|
let temp_dir = TempDir::new().unwrap();
|
|
let config = create_test_config(&temp_dir);
|
|
|
|
let result = timeout(TEST_TIMEOUT, SynorNode::new(config)).await;
|
|
assert!(result.is_ok(), "Node creation timed out");
|
|
|
|
let node_result = result.unwrap();
|
|
assert!(node_result.is_ok(), "Node creation failed: {:?}", node_result.err());
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_node_initial_state() {
|
|
let temp_dir = TempDir::new().unwrap();
|
|
let config = create_test_config(&temp_dir);
|
|
|
|
let node = SynorNode::new(config).await.unwrap();
|
|
let state = node.state().await;
|
|
|
|
assert_eq!(state, NodeState::Starting);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_node_start_stop() {
|
|
let temp_dir = TempDir::new().unwrap();
|
|
let config = create_test_config(&temp_dir);
|
|
|
|
let node = SynorNode::new(config).await.unwrap();
|
|
|
|
// Start the node
|
|
let start_result = timeout(TEST_TIMEOUT, node.start()).await;
|
|
assert!(start_result.is_ok(), "Node start timed out");
|
|
assert!(start_result.unwrap().is_ok(), "Node start failed");
|
|
|
|
let state = node.state().await;
|
|
assert_eq!(state, NodeState::Running);
|
|
|
|
// Stop the node
|
|
let stop_result = timeout(TEST_TIMEOUT, node.stop()).await;
|
|
assert!(stop_result.is_ok(), "Node stop timed out");
|
|
assert!(stop_result.unwrap().is_ok(), "Node stop failed");
|
|
|
|
let state = node.state().await;
|
|
assert_eq!(state, NodeState::Stopped);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_node_info() {
|
|
let temp_dir = TempDir::new().unwrap();
|
|
let config = create_test_config(&temp_dir);
|
|
let expected_chain_id = config.chain_id;
|
|
|
|
let node = SynorNode::new(config).await.unwrap();
|
|
node.start().await.unwrap();
|
|
|
|
let info = node.info().await;
|
|
|
|
assert_eq!(info.chain_id, expected_chain_id);
|
|
assert_eq!(info.network, "devnet");
|
|
assert!(!info.is_mining); // Mining disabled
|
|
assert!(info.peer_count == 0); // No seeds
|
|
|
|
node.stop().await.unwrap();
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_node_services_accessible() {
|
|
let temp_dir = TempDir::new().unwrap();
|
|
let config = create_test_config(&temp_dir);
|
|
|
|
let node = SynorNode::new(config).await.unwrap();
|
|
|
|
// Services should be accessible even before start
|
|
// These return &Arc<T> directly, not Option
|
|
let _ = node.storage(); // Storage is always created
|
|
let _ = node.network();
|
|
let _ = node.consensus();
|
|
let _ = node.mempool();
|
|
let _ = node.rpc();
|
|
let _ = node.contract();
|
|
assert!(node.miner().is_none()); // Mining disabled (this one is Option)
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_node_with_mining() {
|
|
let temp_dir = TempDir::new().unwrap();
|
|
let mut config = create_test_config(&temp_dir);
|
|
|
|
// Enable mining
|
|
config.mining.enabled = true;
|
|
config.mining.coinbase_address = Some("tsynor1test...".to_string());
|
|
config.mining.threads = 1;
|
|
|
|
let node = SynorNode::new(config).await.unwrap();
|
|
|
|
// Miner should be present
|
|
assert!(node.miner().is_some());
|
|
|
|
node.start().await.unwrap();
|
|
|
|
let info = node.info().await;
|
|
assert!(info.is_mining);
|
|
|
|
node.stop().await.unwrap();
|
|
}
|
|
|
|
// ==================== Directory Creation Tests ====================
|
|
|
|
#[tokio::test]
|
|
async fn test_node_creates_directories() {
|
|
let temp_dir = TempDir::new().unwrap();
|
|
let config = create_test_config(&temp_dir);
|
|
|
|
let blocks_path = config.blocks_path();
|
|
let chainstate_path = config.chainstate_path();
|
|
let contracts_path = config.contracts_path();
|
|
|
|
// Directories shouldn't exist yet
|
|
assert!(!blocks_path.exists());
|
|
|
|
// Create node (this should create directories)
|
|
let _node = SynorNode::new(config).await.unwrap();
|
|
|
|
// Directories should now exist
|
|
assert!(blocks_path.exists(), "blocks directory not created");
|
|
assert!(chainstate_path.exists(), "chainstate directory not created");
|
|
assert!(contracts_path.exists(), "contracts directory not created");
|
|
}
|
|
|
|
// ==================== State Transition Tests ====================
|
|
|
|
#[tokio::test]
|
|
async fn test_state_transitions() {
|
|
let temp_dir = TempDir::new().unwrap();
|
|
let config = create_test_config(&temp_dir);
|
|
|
|
let node = SynorNode::new(config).await.unwrap();
|
|
|
|
// Initial state
|
|
assert_eq!(node.state().await, NodeState::Starting);
|
|
|
|
// After start
|
|
node.start().await.unwrap();
|
|
assert_eq!(node.state().await, NodeState::Running);
|
|
|
|
// After stop
|
|
node.stop().await.unwrap();
|
|
assert_eq!(node.state().await, NodeState::Stopped);
|
|
}
|
|
|
|
// ==================== Error Handling Tests ====================
|
|
|
|
#[tokio::test]
|
|
async fn test_node_double_start() {
|
|
let temp_dir = TempDir::new().unwrap();
|
|
let config = create_test_config(&temp_dir);
|
|
|
|
let node = SynorNode::new(config).await.unwrap();
|
|
|
|
// First start should succeed
|
|
node.start().await.unwrap();
|
|
|
|
// Second start might fail or be idempotent
|
|
// This depends on implementation - just verify no panic
|
|
let _ = node.start().await;
|
|
|
|
node.stop().await.unwrap();
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_node_double_stop() {
|
|
let temp_dir = TempDir::new().unwrap();
|
|
let config = create_test_config(&temp_dir);
|
|
|
|
let node = SynorNode::new(config).await.unwrap();
|
|
node.start().await.unwrap();
|
|
|
|
// First stop
|
|
node.stop().await.unwrap();
|
|
|
|
// Second stop should be safe (idempotent)
|
|
let result = node.stop().await;
|
|
assert!(result.is_ok(), "Double stop should be safe");
|
|
}
|
|
|
|
// ==================== Consensus Config Tests ====================
|
|
|
|
#[test]
|
|
fn test_consensus_config_for_networks() {
|
|
use synord::config::ConsensusConfig;
|
|
|
|
let mainnet = ConsensusConfig::for_network("mainnet");
|
|
let devnet = ConsensusConfig::for_network("devnet");
|
|
|
|
// Devnet should have faster finality
|
|
assert!(devnet.finality_depth < mainnet.finality_depth);
|
|
assert!(devnet.target_time_ms < mainnet.target_time_ms);
|
|
}
|
|
|
|
// ==================== Default Config Tests ====================
|
|
|
|
#[test]
|
|
fn test_storage_config_defaults() {
|
|
use synord::config::StorageConfig;
|
|
|
|
let config = StorageConfig::default();
|
|
assert_eq!(config.db_type, "rocksdb");
|
|
assert!(config.cache_size_mb > 0);
|
|
assert!(!config.pruning.enabled);
|
|
}
|
|
|
|
#[test]
|
|
fn test_p2p_config_defaults() {
|
|
use synord::config::P2PConfig;
|
|
|
|
let config = P2PConfig::default();
|
|
assert!(config.max_inbound > 0);
|
|
assert!(config.max_outbound > 0);
|
|
assert!(config.connection_timeout > 0);
|
|
}
|
|
|
|
#[test]
|
|
fn test_rpc_config_defaults() {
|
|
use synord::config::RpcConfig;
|
|
|
|
let config = RpcConfig::default();
|
|
assert!(config.http_enabled);
|
|
assert!(config.ws_enabled);
|
|
assert!(config.cors);
|
|
}
|
|
|
|
#[test]
|
|
fn test_mining_config_defaults() {
|
|
use synord::config::MiningConfig;
|
|
|
|
let config = MiningConfig::default();
|
|
assert!(!config.enabled);
|
|
assert!(config.coinbase_address.is_none());
|
|
assert!(!config.gpu_enabled);
|
|
}
|
|
|
|
#[test]
|
|
fn test_vm_config_defaults() {
|
|
use synord::config::VmConfig;
|
|
|
|
let config = VmConfig::default();
|
|
assert!(config.enabled);
|
|
assert!(config.max_gas_per_block > 0);
|
|
assert!(config.max_contract_size > 0);
|
|
assert!(config.max_call_depth > 0);
|
|
}
|