synor/apps/synord/tests/node_lifecycle.rs
2026-01-08 05:22:24 +05:30

368 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);
}