//! M4 live: F4 rotate onion locator; identity stays; online peer updates. use std::time::{Duration, Instant}; use onionwire::Store; use onionwire::node::Node; const AFTER: &[u8] = b"after rotate"; #[tokio::test(flavor = "multi_thread")] #[ignore = "needs live Tor network"] async fn rotate_changes_onion_not_identity_and_notifies_online_peer() { let _ = tracing_subscriber::fmt() .with_env_filter( tracing_subscriber::EnvFilter::try_from_default_env().unwrap_or_else(|_| "info".into()), ) .with_test_writer() .try_init(); let root = tempfile::tempdir().expect("tempdir"); let alice_home = root.path().join("alice"); let bob_home = root.path().join("bob"); let carol_home = root.path().join("carol"); eprintln!("starting alice + bob…"); unsafe { std::env::set_var("ONIONWIRE_STORE_PASSPHRASE", "onionwire-test"); } let (alice, bob) = tokio::join!( Node::start(alice_home.clone()), Node::start(bob_home.clone()) ); let alice = alice.expect("alice"); let bob = bob.expect("bob"); let a_qr = alice.qr_payload().expect("alice qr"); let b_qr = bob.qr_payload().expect("bob qr"); alice.add_friend_from_qr(&b_qr).expect("alice adds bob"); bob.add_friend_from_qr(&a_qr).expect("bob adds alice"); let carol = Store::open_at_with_passphrase(&carol_home, "onionwire-test").expect("carol store"); let a_pk = alice.identity_pk(); carol .upsert_friend(&a_pk, &alice.onion(), Some("alice")) .expect("carol knows alice"); carol .set_friend_prekey(&a_pk, &[0u8; 32]) .expect("carol prekey placeholder"); let fp_before = alice.identity_pk(); let onion_before = alice.onion(); eprintln!("alice onion before={onion_before}"); let outcome = alice.rotate().await.expect("rotate"); let onion_after = alice.onion(); eprintln!( "alice onion after={onion_after} notified={}/{}", outcome.notified, outcome.friends ); assert_eq!(alice.identity_pk(), fp_before, "identity key never changes"); assert_ne!(onion_after, onion_before, "onion locator must change"); assert_eq!(outcome.friends, 1, "only bob is a friend"); assert_eq!(outcome.notified, 1, "online bob got loc"); let deadline = Instant::now() + Duration::from_secs(30); let bob_row = loop { let row = bob.friend(&fp_before).expect("bob row"); if row.onion == onion_after { break row; } if Instant::now() >= deadline { panic!( "bob locator not updated; have {} want {onion_after}", row.onion ); } tokio::time::sleep(Duration::from_millis(200)).await; }; assert_eq!(bob_row.pubkey, fp_before); assert_eq!(bob_row.fingerprint, to_hex(&fp_before)); bob.send(&fp_before, AFTER) .await .expect("bob dials new locator"); let deadline = Instant::now() + Duration::from_secs(30); loop { let msgs = alice.list_messages(&bob.identity_pk()).expect("alice msgs"); if msgs.iter().any(|m| m.dir == "in" && m.plaintext == AFTER) { break; } if Instant::now() >= deadline { panic!("alice missing post-rotate message; got {msgs:?}"); } tokio::time::sleep(Duration::from_millis(200)).await; } assert!( alice.connect_onion(&onion_before).await.is_err(), "old HS must be hard-cut" ); let c_row = carol.get_friend(&a_pk).unwrap().unwrap(); assert_eq!(c_row.onion, onion_before, "offline carol stays stale"); assert_eq!(c_row.petname.as_deref(), Some("alice")); let new_qr = alice.qr_payload().expect("new qr"); let p = onionwire::qr::decode(&new_qr).expect("decode"); carol .upsert_friend(&p.pubkey, &p.onion, None) .expect("rescan"); assert_eq!(carol.friend_count().unwrap(), 1); let c2 = carol.get_friend(&a_pk).unwrap().unwrap(); assert_eq!(c2.onion, onion_after); assert_eq!(c2.petname.as_deref(), Some("alice")); assert_eq!(c2.fingerprint, c_row.fingerprint); } fn to_hex(bytes: &[u8]) -> String { bytes.iter().map(|b| format!("{b:02x}")).collect() }