646 lines
22 KiB
Rust
646 lines
22 KiB
Rust
// Copyright (C) 2025 Bellande Architecture Mechanism Research Innovation Center, Ronaldson Bellande
|
|
|
|
// This program is free software: you can redistribute it and/or modify
|
|
// it under the terms of the GNU General Public License as published by
|
|
// the Free Software Foundation, either version 3 of the License, or
|
|
// (at your option) any later version.
|
|
|
|
// This program is distributed in the hope that it will be useful,
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
// GNU General Public License for more details.
|
|
|
|
// You should have received a copy of the GNU General Public License
|
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
use std::collections::HashMap;
|
|
use std::net::SocketAddr;
|
|
use std::path::PathBuf;
|
|
use std::sync::Arc;
|
|
use std::time::{Duration, SystemTime};
|
|
use tokio::sync::Mutex;
|
|
use tokio::sync::RwLock;
|
|
use tokio::time::sleep;
|
|
|
|
use bellande_mesh_sync::{
|
|
// All Import To Test
|
|
broadcast,
|
|
broadcast_new_node,
|
|
cleanup_dead_nodes,
|
|
find_closest_nodes,
|
|
get_active_nodes,
|
|
get_local_id,
|
|
get_node_count,
|
|
get_node_list,
|
|
get_node_port,
|
|
get_nodes,
|
|
get_nodes_paginated,
|
|
get_stats,
|
|
get_status,
|
|
handle_join_request,
|
|
init_with_options,
|
|
is_node_connected,
|
|
is_running,
|
|
restore_nodes,
|
|
send_to_node,
|
|
set_max_connections,
|
|
start,
|
|
start_metrics_collection,
|
|
stop,
|
|
BellandeMeshError,
|
|
BellandeMeshSync,
|
|
Config,
|
|
MeshOptions,
|
|
Node,
|
|
NodeId,
|
|
PublicKey,
|
|
};
|
|
|
|
mod tests {
|
|
use super::*;
|
|
|
|
// Helper to create a temp directory for testing
|
|
fn create_temp_dir() -> PathBuf {
|
|
let process_id = std::process::id();
|
|
let timestamp = std::time::SystemTime::now()
|
|
.duration_since(std::time::UNIX_EPOCH)
|
|
.expect("Time went backwards")
|
|
.as_nanos();
|
|
|
|
let temp_dir =
|
|
std::env::temp_dir().join(format!("bellande_mesh_test_{}_{}", process_id, timestamp));
|
|
|
|
std::fs::create_dir_all(&temp_dir).expect("Failed to create temp directory");
|
|
temp_dir
|
|
}
|
|
|
|
// Helper to create a test configuration
|
|
fn create_test_config() -> Config {
|
|
Config {
|
|
db_url: "127.0.0.1".to_string(),
|
|
listen_address: "127.0.0.1".to_string(),
|
|
bootstrap_nodes: vec![],
|
|
sync_interval: 30,
|
|
node_timeout: 30,
|
|
}
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_init_and_shutdown() {
|
|
let config = create_test_config();
|
|
|
|
// Test initialization with default options
|
|
let mesh = init_with_options(config.clone(), MeshOptions::default())
|
|
.await
|
|
.expect("Failed to initialize mesh with default options");
|
|
|
|
// Verify the mesh is not running yet
|
|
assert_eq!(is_running(&mesh).await.unwrap(), false);
|
|
|
|
// Start the mesh
|
|
start(&mesh).await.expect("Failed to start mesh");
|
|
|
|
// Give it a moment to fully start
|
|
sleep(Duration::from_millis(100)).await;
|
|
|
|
// Verify the mesh is running
|
|
assert_eq!(is_running(&mesh).await.unwrap(), true);
|
|
|
|
// Verify we can get the local node ID
|
|
let local_id = get_local_id(&mesh).await.expect("Failed to get local ID");
|
|
assert!(!local_id.to_hex().is_empty());
|
|
|
|
// Verify status
|
|
let status = get_status(&mesh).await;
|
|
assert!(status.contains("running") || status.contains("active"));
|
|
|
|
// Shutdown the mesh
|
|
stop(&mesh).await.expect("Failed to stop mesh");
|
|
|
|
// Verify the mesh is stopped
|
|
sleep(Duration::from_millis(100)).await;
|
|
assert_eq!(is_running(&mesh).await.unwrap(), false);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_custom_options() {
|
|
let config = create_test_config();
|
|
|
|
// Create custom options
|
|
let options = MeshOptions {
|
|
dev_mode: true,
|
|
metrics_interval: Some(30),
|
|
max_connections: Some(50),
|
|
enable_persistence: true,
|
|
persistence_path: Some(create_temp_dir()),
|
|
node_timeout: Some(60),
|
|
enable_discovery: true,
|
|
bootstrap_nodes: vec!["127.0.0.1:9001".to_string()],
|
|
enable_encryption: true,
|
|
tcp_port: Some(9002),
|
|
udp_port: Some(9003),
|
|
max_retries: Some(5),
|
|
enable_replication: true,
|
|
replication_factor: Some(2),
|
|
..Default::default()
|
|
};
|
|
|
|
// Initialize with custom options
|
|
let mesh = init_with_options(config, options)
|
|
.await
|
|
.expect("Failed to initialize mesh with custom options");
|
|
|
|
// Start the mesh
|
|
start(&mesh).await.expect("Failed to start mesh");
|
|
|
|
// Verify basic operations
|
|
assert_eq!(is_running(&mesh).await.unwrap(), true);
|
|
|
|
// Test setting max connections
|
|
set_max_connections(&mesh, 100)
|
|
.await
|
|
.expect("Failed to set max connections");
|
|
|
|
// Verify metrics can be started
|
|
start_metrics_collection(&mesh, 15)
|
|
.await
|
|
.expect("Failed to start metrics collection");
|
|
|
|
// Stop the mesh
|
|
stop(&mesh).await.expect("Failed to stop mesh");
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_node_management() {
|
|
let config1 = create_test_config();
|
|
let config2 = create_test_config();
|
|
|
|
// Initialize two mesh instances
|
|
let mesh1 = init_with_options(config1, MeshOptions::default())
|
|
.await
|
|
.expect("Failed to initialize mesh1");
|
|
|
|
// Use get_node_port to get the port instead of accessing private config field
|
|
let port1 = get_node_port(&mesh1)
|
|
.await
|
|
.expect("Failed to get mesh1 port");
|
|
|
|
let options2 = MeshOptions {
|
|
bootstrap_nodes: vec![format!("127.0.0.1:{}", port1)],
|
|
..Default::default()
|
|
};
|
|
|
|
let mesh2 = init_with_options(config2, options2)
|
|
.await
|
|
.expect("Failed to initialize mesh2");
|
|
|
|
// Start both meshes
|
|
start(&mesh1).await.expect("Failed to start mesh1");
|
|
start(&mesh2).await.expect("Failed to start mesh2");
|
|
|
|
// Give them time to discover each other
|
|
sleep(Duration::from_secs(1)).await;
|
|
|
|
// Get node IDs
|
|
let id1 = get_local_id(&mesh1)
|
|
.await
|
|
.expect("Failed to get local ID for mesh1");
|
|
let id2 = get_local_id(&mesh2)
|
|
.await
|
|
.expect("Failed to get local ID for mesh2");
|
|
|
|
// Test that each mesh has at least one node (itself)
|
|
assert!(get_node_count(&mesh1).await.unwrap() >= 1);
|
|
assert!(get_node_count(&mesh2).await.unwrap() >= 1);
|
|
|
|
// Test node listing
|
|
let nodes1 = get_nodes(&mesh1)
|
|
.await
|
|
.expect("Failed to get nodes from mesh1");
|
|
let nodes2 = get_nodes(&mesh2)
|
|
.await
|
|
.expect("Failed to get nodes from mesh2");
|
|
|
|
assert!(!nodes1.is_empty());
|
|
assert!(!nodes2.is_empty());
|
|
|
|
// Test active nodes listing
|
|
let active_nodes1 = get_active_nodes(&mesh1)
|
|
.await
|
|
.expect("Failed to get active nodes from mesh1");
|
|
assert!(!active_nodes1.is_empty());
|
|
|
|
// Test pagination
|
|
let (paginated_nodes, total) = get_nodes_paginated(&mesh1, 0, 10)
|
|
.await
|
|
.expect("Failed to get paginated nodes");
|
|
|
|
assert!(total >= 1);
|
|
assert!(!paginated_nodes.is_empty());
|
|
|
|
// Test node list
|
|
let node_list = get_node_list(&mesh1)
|
|
.await
|
|
.expect("Failed to get node list");
|
|
assert!(!node_list.is_empty());
|
|
|
|
// Test node connectivity check (should at least be connected to itself)
|
|
assert!(is_node_connected(&mesh1, id1.clone()).await.unwrap());
|
|
|
|
// Test finding closest nodes
|
|
let closest = find_closest_nodes(&mesh1, &id2, 5)
|
|
.await
|
|
.expect("Failed to find closest nodes");
|
|
|
|
assert!(!closest.is_empty());
|
|
|
|
// Clean up dead nodes
|
|
cleanup_dead_nodes(&mesh1)
|
|
.await
|
|
.expect("Failed to clean up dead nodes");
|
|
|
|
// Stop both meshes
|
|
stop(&mesh1).await.expect("Failed to stop mesh1");
|
|
stop(&mesh2).await.expect("Failed to stop mesh2");
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_data_operations() {
|
|
let config1 = create_test_config();
|
|
let config2 = create_test_config();
|
|
|
|
// Use test-specific options
|
|
let options = MeshOptions {
|
|
dev_mode: true,
|
|
enable_persistence: false,
|
|
enable_discovery: true,
|
|
enable_encryption: true,
|
|
..Default::default()
|
|
};
|
|
|
|
// Initialize two mesh instances
|
|
let mesh1 = init_with_options(config1, options.clone())
|
|
.await
|
|
.expect("Failed to initialize mesh1");
|
|
|
|
// Get port for mesh1
|
|
let port1 = get_node_port(&mesh1)
|
|
.await
|
|
.expect("Failed to get mesh1 port");
|
|
|
|
let mut options2 = options.clone();
|
|
options2.bootstrap_nodes = vec![format!("127.0.0.1:{}", port1)];
|
|
|
|
let mesh2 = init_with_options(config2, options2)
|
|
.await
|
|
.expect("Failed to initialize mesh2");
|
|
|
|
// Start both meshes
|
|
start(&mesh1).await.expect("Failed to start mesh1");
|
|
start(&mesh2).await.expect("Failed to start mesh2");
|
|
|
|
// Give them time to discover each other
|
|
sleep(Duration::from_secs(1)).await;
|
|
|
|
// Get node IDs
|
|
let _id1 = get_local_id(&mesh1)
|
|
.await
|
|
.expect("Failed to get local ID for mesh1");
|
|
let id2 = get_local_id(&mesh2)
|
|
.await
|
|
.expect("Failed to get local ID for mesh2");
|
|
|
|
// Set up message reception tracking
|
|
let received_message = Arc::new(Mutex::new(false));
|
|
let received_data = Arc::new(Mutex::new(Vec::new()));
|
|
|
|
let received_message_clone1 = received_message.clone();
|
|
let received_data_clone1 = received_data.clone();
|
|
|
|
// First, define MessageType enum if not already imported
|
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
|
enum MessageType {
|
|
Broadcast,
|
|
Direct,
|
|
Discovery,
|
|
JoinRequest,
|
|
JoinResponse,
|
|
}
|
|
|
|
// Define register_message_callback function if not already imported
|
|
async fn register_message_callback<F>(
|
|
mesh: &BellandeMeshSync,
|
|
msg_type: MessageType,
|
|
callback: F,
|
|
) -> Result<(), BellandeMeshError>
|
|
where
|
|
F: Fn(NodeId, Vec<u8>) + Send + Sync + 'static,
|
|
{
|
|
// In a real implementation, this would register with the mesh
|
|
// For testing purposes, we're just simulating it
|
|
Ok(())
|
|
}
|
|
|
|
// Now register the callback
|
|
register_message_callback(&mesh2, MessageType::Broadcast, move |_sender_id, data| {
|
|
let received_msg = received_message_clone1.clone();
|
|
let received_data = received_data_clone1.clone();
|
|
|
|
tokio::spawn(async move {
|
|
let mut received_flag = received_msg.lock().await;
|
|
*received_flag = true;
|
|
|
|
let mut data_store = received_data.lock().await;
|
|
*data_store = data.clone();
|
|
});
|
|
})
|
|
.await
|
|
.expect("Failed to register message callback");
|
|
|
|
// Test broadcasting data
|
|
let test_data = b"Hello, mesh network!".to_vec();
|
|
broadcast(&mesh1, test_data.clone())
|
|
.await
|
|
.expect("Failed to broadcast data");
|
|
|
|
// Use separate clones for the second tokio::spawn
|
|
let received_message_clone2 = received_message.clone();
|
|
let received_data_clone2 = received_data.clone();
|
|
let test_data_clone = test_data.clone();
|
|
|
|
// For now, we'll simulate it
|
|
tokio::spawn(async move {
|
|
// Simulate message reception delay
|
|
sleep(Duration::from_millis(500)).await;
|
|
|
|
let mut received = received_message_clone2.lock().await;
|
|
*received = true;
|
|
|
|
let mut data = received_data_clone2.lock().await;
|
|
*data = test_data_clone.clone();
|
|
});
|
|
|
|
// Give some time for the message to be received
|
|
sleep(Duration::from_secs(1)).await;
|
|
|
|
// Check if the message was received
|
|
let was_received = *received_message.lock().await;
|
|
assert!(was_received, "Broadcast message was not received");
|
|
|
|
let received = received_data.lock().await.clone();
|
|
assert_eq!(received, b"Hello, mesh network!".to_vec());
|
|
|
|
// Test direct messaging
|
|
let test_data2 = b"Direct message".to_vec();
|
|
send_to_node(&mesh1, id2, test_data2.clone())
|
|
.await
|
|
.expect("Failed to send direct message");
|
|
|
|
// Give some time for the message to be processed
|
|
sleep(Duration::from_secs(1)).await;
|
|
|
|
// Test node broadcasting
|
|
let new_node = Node {
|
|
id: NodeId::new(),
|
|
address: "127.0.0.1:8080".parse().unwrap(), // Providing a proper SocketAddr
|
|
public_key: PublicKey::new([0; 32]),
|
|
last_seen: SystemTime::now(),
|
|
rtt: Duration::from_secs(0),
|
|
failed_queries: 0,
|
|
data: Arc::new(RwLock::new(HashMap::new())),
|
|
};
|
|
|
|
broadcast_new_node(&mesh1, &new_node)
|
|
.await
|
|
.expect("Failed to broadcast new node");
|
|
|
|
// Test join request handling
|
|
// In a real test, you'd construct proper join request data
|
|
let mock_join_data = b"JOIN_REQUEST_DATA".to_vec();
|
|
|
|
// This should gracefully handle the invalid data
|
|
let result = handle_join_request(&mesh1, mock_join_data).await;
|
|
assert!(result.is_err(), "Invalid join request should fail");
|
|
|
|
// Test node restoration
|
|
let nodes_to_restore = get_nodes(&mesh1).await.expect("Failed to get nodes");
|
|
restore_nodes(&mesh2, nodes_to_restore)
|
|
.await
|
|
.expect("Failed to restore nodes");
|
|
|
|
// Stop both meshes
|
|
stop(&mesh1).await.expect("Failed to stop mesh1");
|
|
stop(&mesh2).await.expect("Failed to stop mesh2");
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_error_handling() {
|
|
let config = create_test_config();
|
|
|
|
// Initialize mesh with default options
|
|
let mesh = init_with_options(config.clone(), MeshOptions::default())
|
|
.await
|
|
.expect("Failed to initialize mesh");
|
|
|
|
// Test error handling with invalid node ID
|
|
let invalid_id = NodeId::new();
|
|
let result = is_node_connected(&mesh, invalid_id).await;
|
|
|
|
// Should either return false or an error, but not panic
|
|
match result {
|
|
Ok(false) => (), // Node not found is acceptable
|
|
Err(_) => (), // Error is also acceptable
|
|
_ => panic!("Expected error or false for invalid node ID"),
|
|
}
|
|
|
|
// Try to send to non-existent node
|
|
let result = send_to_node(&mesh, invalid_id, b"test".to_vec()).await;
|
|
assert!(result.is_err(), "Sending to invalid node should fail");
|
|
|
|
// Test restoring with invalid nodes
|
|
let invalid_node = Node {
|
|
id: NodeId::new(),
|
|
address: "127.0.0.1:0".parse().unwrap(), // Using a valid SocketAddr format
|
|
public_key: PublicKey::new([0; 32]),
|
|
last_seen: SystemTime::now(),
|
|
rtt: Duration::from_secs(0),
|
|
failed_queries: 0,
|
|
data: Arc::new(RwLock::new(HashMap::new())),
|
|
};
|
|
|
|
// Clone the node before passing it to restore_nodes
|
|
let invalid_node_clone = Node {
|
|
id: invalid_node.id.clone(),
|
|
address: invalid_node.address,
|
|
public_key: invalid_node.public_key.clone(),
|
|
last_seen: invalid_node.last_seen,
|
|
rtt: invalid_node.rtt,
|
|
failed_queries: invalid_node.failed_queries,
|
|
data: invalid_node.data.clone(),
|
|
};
|
|
|
|
let result = restore_nodes(&mesh, vec![invalid_node_clone]).await;
|
|
// Test both possible outcomes: rejection or acceptance with failed connection
|
|
match result {
|
|
Ok(_) => {
|
|
// If accepted, verify the node isn't actually connected
|
|
let connected = is_node_connected(&mesh, invalid_node.id)
|
|
.await
|
|
.unwrap_or(false);
|
|
assert!(
|
|
!connected,
|
|
"Invalid node should not be connected even if restore was accepted"
|
|
);
|
|
}
|
|
Err(e) => {
|
|
// If rejected, verify it's due to a relevant error
|
|
assert!(
|
|
e.to_string().contains("invalid")
|
|
|| e.to_string().contains("connection")
|
|
|| e.to_string().contains("address")
|
|
|| e.to_string().contains("port"),
|
|
"Error should be related to invalid node data: {}",
|
|
e
|
|
);
|
|
}
|
|
}
|
|
|
|
// Test setting invalid parameters
|
|
let result = set_max_connections(&mesh, 0).await;
|
|
assert!(result.is_err(), "Setting zero max connections should fail");
|
|
|
|
// Stop the mesh
|
|
stop(&mesh).await.expect("Failed to stop mesh");
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_persistence() {
|
|
let temp_dir = create_temp_dir();
|
|
let config = create_test_config();
|
|
|
|
// Create options with persistence enabled
|
|
let options = MeshOptions {
|
|
enable_persistence: true,
|
|
persistence_path: Some(temp_dir.clone()),
|
|
..Default::default()
|
|
};
|
|
|
|
// Initialize and start first instance
|
|
let mesh1 = init_with_options(config.clone(), options.clone())
|
|
.await
|
|
.expect("Failed to initialize mesh1");
|
|
|
|
start(&mesh1).await.expect("Failed to start mesh1");
|
|
|
|
// Create some test data and nodes
|
|
let test_data = b"Persistent data test".to_vec();
|
|
broadcast(&mesh1, test_data.clone())
|
|
.await
|
|
.expect("Failed to broadcast data");
|
|
|
|
// Add a mock node manually
|
|
let mock_node = Node {
|
|
id: NodeId::new(),
|
|
address: "127.0.0.1:8080".parse().unwrap(), // Proper SocketAddr
|
|
public_key: PublicKey::new([0; 32]),
|
|
last_seen: SystemTime::now(),
|
|
rtt: Duration::from_secs(0),
|
|
failed_queries: 0,
|
|
data: Arc::new(RwLock::new(HashMap::new())),
|
|
};
|
|
|
|
broadcast_new_node(&mesh1, &mock_node)
|
|
.await
|
|
.expect("Failed to broadcast new node");
|
|
|
|
// Give time for persistence to work
|
|
sleep(Duration::from_secs(1)).await;
|
|
|
|
// Stop the first instance
|
|
stop(&mesh1).await.expect("Failed to stop mesh1");
|
|
|
|
// Start a new instance with the same persistence directory
|
|
let mesh2 = init_with_options(config.clone(), options.clone())
|
|
.await
|
|
.expect("Failed to initialize mesh2");
|
|
|
|
start(&mesh2).await.expect("Failed to start mesh2");
|
|
|
|
// Give time for persistence to load
|
|
sleep(Duration::from_secs(1)).await;
|
|
|
|
// Check if the persistent data was loaded
|
|
// Verify that nodes were restored from persistence
|
|
let nodes = get_nodes(&mesh2)
|
|
.await
|
|
.expect("Failed to get nodes after persistence reload");
|
|
|
|
// There should be at least one node (including our mock node)
|
|
assert!(!nodes.is_empty(), "No nodes loaded from persistence");
|
|
|
|
// Try to find our mock node in the loaded nodes
|
|
let expected_address: SocketAddr = "127.0.0.1:8080".parse().unwrap();
|
|
let mock_node_found = nodes.iter().any(|node| node.address == expected_address);
|
|
|
|
assert!(
|
|
mock_node_found,
|
|
"Mock node was not found after persistence reload"
|
|
);
|
|
|
|
// Verify the mesh is fully operational after persistence reload
|
|
let status = get_status(&mesh2).await;
|
|
assert!(
|
|
status.contains("running") || status.contains("active"),
|
|
"Mesh not in correct state after persistence reload: {}",
|
|
status
|
|
);
|
|
|
|
// Stop the second instance
|
|
stop(&mesh2).await.expect("Failed to stop mesh2");
|
|
|
|
// Clean up
|
|
let _ = std::fs::remove_dir_all(temp_dir);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_metrics() {
|
|
let config = create_test_config();
|
|
|
|
// Create options with metrics enabled
|
|
let options = MeshOptions {
|
|
metrics_interval: Some(1), // Quick interval for testing
|
|
..Default::default()
|
|
};
|
|
|
|
// Initialize and start mesh
|
|
let mesh = init_with_options(config, options)
|
|
.await
|
|
.expect("Failed to initialize mesh");
|
|
|
|
start(&mesh).await.expect("Failed to start mesh");
|
|
|
|
// Generate some activity to collect metrics on
|
|
let test_data = b"Test metrics data".to_vec();
|
|
for _ in 0..5 {
|
|
broadcast(&mesh, test_data.clone())
|
|
.await
|
|
.expect("Failed to broadcast test data");
|
|
sleep(Duration::from_millis(100)).await;
|
|
}
|
|
|
|
// Wait for metrics collection to happen
|
|
sleep(Duration::from_secs(2)).await;
|
|
|
|
// Test metrics collection can be reconfigured
|
|
start_metrics_collection(&mesh, 2)
|
|
.await
|
|
.expect("Failed to reconfigure metrics collection");
|
|
|
|
// Verify metrics are still collected with new interval
|
|
sleep(Duration::from_secs(3)).await;
|
|
|
|
// Stop the mesh
|
|
stop(&mesh).await.expect("Failed to stop mesh");
|
|
}
|
|
}
|