//! M4: loc frames, F4 confirm typing, identity stays put. use ed25519_dalek::SigningKey; use onionwire::Store; use onionwire::loc::{self, Loc}; use onionwire::tui::{RotateDecision, RotatePrompt}; use rand::rngs::OsRng; fn pk(tag: u8) -> [u8; 32] { let mut k = [0u8; 32]; k[0] = tag; k } fn store() -> (tempfile::TempDir, Store) { let dir = tempfile::tempdir().expect("tempdir"); let store = Store::open_at(dir.path()).expect("open"); (dir, store) } #[test] fn signed_loc_updates_friend_onion_keeps_fingerprint() { let (_dir, store) = store(); let sk = SigningKey::generate(&mut OsRng); let pk = sk.verifying_key().to_bytes(); store .upsert_friend(&pk, "old.onion", Some("alice")) .unwrap(); store.set_friend_prekey(&pk, &[1u8; 32]).unwrap(); let before = store.get_friend(&pk).unwrap().unwrap(); let ts = 2_000_000_000; let loc = loc::sign(&sk.to_bytes(), "new.onion", ts).expect("sign"); assert!( store.apply_loc(&pk, &loc.onion, loc.ts, &loc.sig).unwrap(), "newer signed loc must apply" ); let after = store.get_friend(&pk).unwrap().unwrap(); assert_eq!(after.onion, "new.onion"); assert_eq!(after.fingerprint, before.fingerprint); assert_eq!(after.petname.as_deref(), Some("alice")); assert_eq!(store.friend_count().unwrap(), 1); } #[test] fn unsigned_loc_does_not_change_row() { let (_dir, store) = store(); store.upsert_friend(&pk(1), "old.onion", Some("c")).unwrap(); let loc = Loc { onion: "evil.onion".into(), ts: 2_000_000_000, sig: vec![], }; assert!( !store .apply_loc(&pk(1), &loc.onion, loc.ts, &loc.sig) .unwrap() ); assert_eq!( store.get_friend(&pk(1)).unwrap().unwrap().onion, "old.onion" ); } #[test] fn wrong_key_loc_does_not_change_row() { let (_dir, store) = store(); let owner = SigningKey::generate(&mut OsRng); let owner_pk = owner.verifying_key().to_bytes(); store.upsert_friend(&owner_pk, "old.onion", None).unwrap(); let other = SigningKey::generate(&mut OsRng); let loc = loc::sign(&other.to_bytes(), "hijack.onion", 2_000_000_000).unwrap(); assert!( !store .apply_loc(&owner_pk, &loc.onion, loc.ts, &loc.sig) .unwrap() ); assert_eq!( store.get_friend(&owner_pk).unwrap().unwrap().onion, "old.onion" ); } #[test] fn stale_ts_loc_does_not_change_row() { let (_dir, store) = store(); let sk = SigningKey::generate(&mut OsRng); let pk = sk.verifying_key().to_bytes(); store.upsert_friend(&pk, "old.onion", None).unwrap(); let loc = loc::sign(&sk.to_bytes(), "new.onion", 1).unwrap(); assert!(!store.apply_loc(&pk, &loc.onion, loc.ts, &loc.sig).unwrap()); assert_eq!(store.get_friend(&pk).unwrap().unwrap().onion, "old.onion"); } #[test] fn loc_frame_roundtrip_and_chat_is_not_loc() { let loc = loc::sign(&[7u8; 32], "abc.onion", 42).unwrap(); let bytes = loc::encode(&loc); let parsed = loc::decode(&bytes).expect("loc frame"); assert_eq!(parsed.onion, "abc.onion"); assert_eq!(parsed.ts, 42); assert_eq!(parsed.sig, loc.sig); assert!(loc::decode(b"hello wire").is_none()); } #[test] fn f4_requires_typing_rotate_not_enter() { let mut p = RotatePrompt::new(); assert_eq!(p.on_esc(), RotateDecision::Cancel); assert_eq!( p.on_char('\n'), RotateDecision::Pending, "Enter must not rotate" ); for c in "ROTA".chars() { assert_eq!(p.on_char(c), RotateDecision::Pending); } assert_eq!(p.on_char('T'), RotateDecision::Pending); assert_eq!(p.on_char('E'), RotateDecision::Confirm); } #[test] fn rotate_prompt_text_matches_spec() { let t = onionwire::tui::rotate_screen_text(); assert!(t.contains("Rotate onion address?")); assert!(t.contains("Your identity key stays the same.")); assert!(t.contains("Online friends get a signed location update.")); assert!(t.contains("Offline friends CANNOT find you until they rescan your QR.")); assert!(t.contains("Type ROTATE to confirm")); assert!(t.contains("Esc to cancel")); }