use super::spanning_tree::*;
use super::*;
use crate::protocol::{Disconnect, DisconnectReason};
#[tokio::test]
async fn test_disconnect_chain_peer_removal() {
let edges = vec![(0, 1), (1, 2)];
let mut nodes = run_tree_test(3, &edges, false).await;
verify_tree_convergence(&nodes);
let node0_addr = *nodes[0].node.node_addr();
let node1_addr = *nodes[1].node.node_addr();
let node2_addr = *nodes[2].node.node_addr();
assert_eq!(nodes[0].node.peer_count(), 1);
assert!(nodes[0].node.get_peer(&node1_addr).is_some());
let disconnect = Disconnect::new(DisconnectReason::Shutdown);
let plaintext = disconnect.encode();
nodes[1]
.node
.send_encrypted_link_message(&node0_addr, &plaintext)
.await
.expect("Failed to send disconnect");
tokio::time::sleep(Duration::from_millis(50)).await;
process_available_packets(&mut nodes).await;
assert_eq!(
nodes[0].node.peer_count(),
0,
"Node 0 should have no peers after disconnect"
);
assert!(
nodes[0].node.get_peer(&node1_addr).is_none(),
"Node 0 should not have node 1 as a peer"
);
assert!(
nodes[0].node.tree_state().is_root(),
"Isolated node 0 should be root"
);
assert!(
nodes[1].node.get_peer(&node2_addr).is_some(),
"Node 1 should still have node 2"
);
cleanup_nodes(&mut nodes).await;
}
#[tokio::test]
async fn test_disconnect_star_hub_departs() {
let edges = vec![(0, 1), (0, 2), (0, 3)];
let mut nodes = run_tree_test(4, &edges, false).await;
verify_tree_convergence(&nodes);
let hub_addr = *nodes[0].node.node_addr();
let disconnect = Disconnect::new(DisconnectReason::Shutdown);
let plaintext = disconnect.encode();
for spoke_idx in 1..4 {
let spoke_addr = *nodes[spoke_idx].node.node_addr();
nodes[0]
.node
.send_encrypted_link_message(&spoke_addr, &plaintext)
.await
.expect("Failed to send disconnect");
}
tokio::time::sleep(Duration::from_millis(50)).await;
process_available_packets(&mut nodes).await;
for (spoke_idx, spoke) in nodes[1..4].iter().enumerate() {
let spoke_idx = spoke_idx + 1; assert!(
spoke.node.get_peer(&hub_addr).is_none(),
"Spoke {} should have removed hub",
spoke_idx
);
assert_eq!(
spoke.node.peer_count(),
0,
"Spoke {} should have no peers (no spoke-spoke links)",
spoke_idx
);
assert!(
spoke.node.tree_state().is_root(),
"Isolated spoke {} should become root",
spoke_idx
);
}
cleanup_nodes(&mut nodes).await;
}
#[tokio::test]
async fn test_disconnect_chain_partition() {
let edges = vec![(0, 1), (1, 2), (2, 3), (3, 4)];
let mut nodes = run_tree_test(5, &edges, false).await;
verify_tree_convergence(&nodes);
let node2_addr = *nodes[2].node.node_addr();
let node1_addr = *nodes[1].node.node_addr();
let node3_addr = *nodes[3].node.node_addr();
let disconnect = Disconnect::new(DisconnectReason::Shutdown);
let plaintext = disconnect.encode();
nodes[2]
.node
.send_encrypted_link_message(&node1_addr, &plaintext)
.await
.expect("Failed to send disconnect to node 1");
nodes[2]
.node
.send_encrypted_link_message(&node3_addr, &plaintext)
.await
.expect("Failed to send disconnect to node 3");
drain_all_packets(&mut nodes, false).await;
assert!(
nodes[1].node.get_peer(&node2_addr).is_none(),
"Node 1 should not have node 2 as peer"
);
assert!(
nodes[3].node.get_peer(&node2_addr).is_none(),
"Node 3 should not have node 2 as peer"
);
let node0_addr = *nodes[0].node.node_addr();
let node4_addr = *nodes[4].node.node_addr();
assert!(
nodes[0].node.get_peer(&node1_addr).is_some(),
"Node 0 should still have node 1 as peer"
);
assert!(
nodes[3].node.get_peer(&node4_addr).is_some(),
"Node 3 should still have node 4 as peer"
);
let node0_reaches_node4 = nodes[0]
.node
.peers()
.any(|peer| peer.may_reach(&node4_addr));
assert!(
!node0_reaches_node4,
"Node 0 should not see node 4 as reachable after partition"
);
let node4_reaches_node0 = nodes[4]
.node
.peers()
.any(|peer| peer.may_reach(&node0_addr));
assert!(
!node4_reaches_node0,
"Node 4 should not see node 0 as reachable after partition"
);
let node0_reaches_node1 = nodes[0]
.node
.peers()
.any(|peer| peer.may_reach(&node1_addr));
assert!(
node0_reaches_node1,
"Node 0 should still see node 1 as reachable"
);
let node4_reaches_node3 = nodes[4]
.node
.peers()
.any(|peer| peer.may_reach(&node3_addr));
assert!(
node4_reaches_node3,
"Node 4 should still see node 3 as reachable"
);
cleanup_nodes(&mut nodes).await;
}
#[tokio::test]
async fn test_disconnect_clears_session() {
use crate::identity::Identity;
use crate::node::session::{EndToEndState, SessionEntry};
use crate::noise::HandshakeState;
let edges = vec![(0, 1)];
let mut nodes = run_tree_test(2, &edges, false).await;
verify_tree_convergence(&nodes);
let node0_addr = *nodes[0].node.node_addr();
let node1_addr = *nodes[1].node.node_addr();
let remote_identity = Identity::generate();
{
let our_identity = nodes[1].node.identity();
let mut initiator =
HandshakeState::new_initiator(our_identity.keypair(), remote_identity.pubkey_full());
let mut responder = HandshakeState::new_responder(remote_identity.keypair());
let mut init_epoch = [0u8; 8];
rand::Rng::fill_bytes(&mut rand::rng(), &mut init_epoch);
initiator.set_local_epoch(init_epoch);
let mut resp_epoch = [0u8; 8];
rand::Rng::fill_bytes(&mut rand::rng(), &mut resp_epoch);
responder.set_local_epoch(resp_epoch);
let msg1 = initiator.write_message_1().unwrap();
responder.read_message_1(&msg1).unwrap();
let msg2 = responder.write_message_2().unwrap();
initiator.read_message_2(&msg2).unwrap();
let session = initiator.into_session().unwrap();
let entry = SessionEntry::new(
node0_addr,
remote_identity.pubkey_full(),
EndToEndState::Established(session),
1_000,
true,
);
nodes[1].node.sessions.insert(node0_addr, entry);
}
assert_eq!(
nodes[1].node.session_count(),
1,
"Session should exist before disconnect"
);
assert_eq!(
nodes[1].node.peer_count(),
1,
"Peer should exist before disconnect"
);
let disconnect = crate::protocol::Disconnect::new(DisconnectReason::Shutdown);
nodes[0]
.node
.send_encrypted_link_message(&node1_addr, &disconnect.encode())
.await
.expect("Failed to send disconnect");
tokio::time::sleep(Duration::from_millis(50)).await;
process_available_packets(&mut nodes).await;
assert_eq!(
nodes[1].node.peer_count(),
0,
"Peer should be removed after disconnect"
);
assert_eq!(
nodes[1].node.session_count(),
0,
"Session must be cleaned up when peer is removed (regression: issue #5)"
);
cleanup_nodes(&mut nodes).await;
}
#[tokio::test]
async fn test_disconnect_all_reason_codes() {
let reasons = vec![
DisconnectReason::Shutdown,
DisconnectReason::Restart,
DisconnectReason::ProtocolError,
DisconnectReason::TransportFailure,
DisconnectReason::ResourceExhaustion,
];
for reason in reasons {
let edges = vec![(0, 1)];
let mut nodes = run_tree_test(2, &edges, false).await;
verify_tree_convergence(&nodes);
let node0_addr = *nodes[0].node.node_addr();
let node1_addr = *nodes[1].node.node_addr();
let disconnect = Disconnect::new(reason);
let plaintext = disconnect.encode();
nodes[0]
.node
.send_encrypted_link_message(&node1_addr, &plaintext)
.await
.expect("Failed to send disconnect");
tokio::time::sleep(Duration::from_millis(50)).await;
process_available_packets(&mut nodes).await;
assert!(
nodes[1].node.get_peer(&node0_addr).is_none(),
"Node 1 should remove peer for reason {:?}",
reason
);
cleanup_nodes(&mut nodes).await;
}
}