//! 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 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); }