//! Scenarios 5 and 6: restart recovery and rotating the network secret. #![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)] mod common; use common::{TestAgent, config_with, network, settle, wait_event, wait_for_peers}; use tsunagi::agent::Event; use tsunagi::discovery::SharedMemoryDiscovery; use tsunagi::identity::{NetworkName, NetworkSecret}; use tsunagi::proto::ControlMessage; use tsunagi::{Agent, Error}; #[tokio::test] async fn a_restarted_agent_keeps_its_identity_and_reconnects() { let discovery = SharedMemoryDiscovery::new(); let (name, secret) = network("survives-restart"); let peer = TestAgent::spawn(&discovery).await.unwrap(); let restarting = TestAgent::spawn(&discovery).await.unwrap(); let network_id = peer.agent.join_network(&name, &secret).await.unwrap(); restarting.agent.join_network(&name, &secret).await.unwrap(); wait_for_peers(&peer.agent, network_id, 1).await; let device_id = restarting.agent.endpoint_id(); let old_sockets = restarting.agent.status().await.unwrap().bound_sockets; let mut peer_events = peer.agent.subscribe(); let dir = restarting.stop().await; // The surviving peer notices the session ending. wait_event(&mut peer_events, |event| match event { Event::PeerDisconnected { peer, .. } if *peer == device_id => Some(()), _ => None, }) .await; // Restart from the same state directory. Binding to port zero again means a // different local UDP port, which the refreshed discovery entry covers. let restarted = Agent::spawn(config_with(dir.path(), &discovery)) .await .unwrap(); assert_eq!(restarted.endpoint_id(), device_id); let new_sockets = restarted.status().await.unwrap().bound_sockets; assert_ne!(old_sockets, new_sockets, "a fresh local port is expected"); // The configured network came back up on its own and both sides reconnect. assert!(restarted.is_active(network_id).await); wait_for_peers(&restarted, network_id, 1).await; wait_for_peers(&peer.agent, network_id, 1).await; // And the restored session really works. restarted .send( network_id, peer.agent.endpoint_id(), ControlMessage::Ping { seq: 5, payload: b"back".to_vec(), }, ) .await .unwrap(); let mut events = restarted.subscribe(); wait_event(&mut events, |event| match event { Event::MessageReceived { message: ControlMessage::Pong { seq: 5, payload }, .. } if payload == b"back" => Some(()), _ => None, }) .await; restarted.shutdown().await; peer.agent.shutdown().await; drop(restarted); drop(dir); } #[tokio::test] async fn readiness_does_not_wait_for_anyone_else() { let discovery = SharedMemoryDiscovery::new(); let (name, secret) = network("lonely"); // No peers exist and no relay is reachable. The agent must still come up. let alone = TestAgent::spawn(&discovery).await.unwrap(); let network_id = alone.agent.join_network(&name, &secret).await.unwrap(); let status = alone.agent.network_status(network_id).await.unwrap(); assert!(status.peers.is_empty()); assert_eq!(status.state, tsunagi::agent::NetworkState::Active); alone.agent.shutdown().await; } #[tokio::test] async fn rotating_the_secret_moves_everyone_to_a_new_space() { let discovery = SharedMemoryDiscovery::new(); let name = NetworkName::new("rotate-me").unwrap(); let old_secret = NetworkSecret::generate(); let new_secret = NetworkSecret::generate(); let a = TestAgent::spawn(&discovery).await.unwrap(); let b = TestAgent::spawn(&discovery).await.unwrap(); let a_id = a.agent.endpoint_id(); let old = a.agent.join_network(&name, &old_secret).await.unwrap(); b.agent.join_network(&name, &old_secret).await.unwrap(); wait_for_peers(&a.agent, old, 1).await; // Rotation through the public API: deactivate the old space, join the new. // No dedicated command is needed for this. a.agent.deactivate_network(old).await.unwrap(); let new = a.agent.join_network(&name, &new_secret).await.unwrap(); assert_ne!(old, new); assert_eq!(a.agent.endpoint_id(), a_id, "device identity is untouched"); // B still holds the old secret, so it must not reach the new space. settle().await; let status = a.agent.network_status(new).await.unwrap(); assert!( status.peers.is_empty(), "the old secret must not open the new space" ); assert!( a.agent .send( old, b.agent.endpoint_id(), ControlMessage::Ping { seq: 1, payload: Vec::new() } ) .await .is_err(), "the deactivated network cannot be used any more" ); // Once B rotates too, they meet again in the new space. b.agent.deactivate_network(old).await.unwrap(); let b_new = b.agent.join_network(&name, &new_secret).await.unwrap(); assert_eq!(b_new, new); wait_for_peers(&a.agent, new, 1).await; a.agent.shutdown().await; b.agent.shutdown().await; } #[tokio::test] async fn a_rotated_out_network_does_not_come_back_after_a_restart() { let discovery = SharedMemoryDiscovery::new(); let name = NetworkName::new("no-resurrection").unwrap(); let agent = TestAgent::spawn(&discovery).await.unwrap(); let old = agent .agent .join_network(&name, &NetworkSecret::generate()) .await .unwrap(); let new = agent .agent .join_network(&name, &NetworkSecret::generate()) .await .unwrap(); agent.agent.deactivate_network(old).await.unwrap(); let dir = agent.stop().await; let restarted = Agent::spawn(config_with(dir.path(), &discovery)) .await .unwrap(); assert!(!restarted.is_active(old).await); assert!(restarted.is_active(new).await); assert!(matches!( restarted.network_status(old).await, Err(Error::NetworkNotActive(_)) )); restarted.shutdown().await; drop(restarted); drop(dir); }