//! M7: signed friend-visible profile frames + slash parse. use ed25519_dalek::SigningKey; use onionwire::Store; use onionwire::profile::{self}; use onionwire::tui::{SlashCmd, WipeKind, parse_cmd, parse_slash}; use rand::rngs::OsRng; 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_profile_upserts_friend_row_keeps_fingerprint() { let (_dir, store) = store(); let sk = SigningKey::generate(&mut OsRng); let pk = sk.verifying_key().to_bytes(); store .upsert_friend(&pk, "alice.onion", Some("alice")) .unwrap(); let before = store.get_friend(&pk).unwrap().unwrap(); let prf = profile::sign(&sk.to_bytes(), "Ali", "hi", "", 2_000_000_000).expect("sign"); assert!( store.apply_profile(&pk, &prf).unwrap(), "newer signed profile must apply" ); let after = store.get_friend(&pk).unwrap().unwrap(); assert_eq!(after.fingerprint, before.fingerprint); assert_eq!(after.petname.as_deref(), Some("alice")); assert_eq!(after.onion, "alice.onion"); assert_eq!(store.friend_count().unwrap(), 1); let got = store.friend_profile(&pk).unwrap().expect("row"); assert_eq!(got.display_name, "Ali"); assert_eq!(got.bio, "hi"); assert_eq!(got.xmr_addr, ""); assert_eq!(got.updated_at, 2_000_000_000); } #[test] fn wrong_key_profile_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 first = profile::sign(&owner.to_bytes(), "real", "", "", 2_000_000_000).unwrap(); assert!(store.apply_profile(&owner_pk, &first).unwrap()); let other = SigningKey::generate(&mut OsRng); let spoof = profile::sign(&other.to_bytes(), "evil", "no", "", 2_000_000_001).unwrap(); assert!(!store.apply_profile(&owner_pk, &spoof).unwrap()); let got = store.friend_profile(&owner_pk).unwrap().unwrap(); assert_eq!(got.display_name, "real"); } #[test] fn stale_ts_profile_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 newer = profile::sign(&sk.to_bytes(), "new", "", "", 100).unwrap(); assert!(store.apply_profile(&pk, &newer).unwrap()); let stale = profile::sign(&sk.to_bytes(), "old", "", "", 50).unwrap(); assert!(!store.apply_profile(&pk, &stale).unwrap()); assert_eq!( store.friend_profile(&pk).unwrap().unwrap().display_name, "new" ); } #[test] fn unknown_pubkey_profile_is_ignored() { let (_dir, store) = store(); let sk = SigningKey::generate(&mut OsRng); let pk = sk.verifying_key().to_bytes(); let prf = profile::sign(&sk.to_bytes(), "ghost", "", "", 2_000_000_000).unwrap(); assert!(!store.apply_profile(&pk, &prf).unwrap()); assert!(store.friend_profile(&pk).unwrap().is_none()); assert_eq!(store.friend_count().unwrap(), 0); } #[test] fn bio_over_512_rejected() { let bio = "x".repeat(513); let sk = [7u8; 32]; assert!(profile::sign(&sk, "n", &bio, "", 1).is_err()); let mut out = b"prf n\n".to_vec(); out.extend_from_slice(bio.as_bytes()); out.extend_from_slice(b"\n\n1\n"); out.extend_from_slice("aa".repeat(64).as_bytes()); assert!(profile::decode(&out).is_none()); } #[test] fn newline_in_name_rejected() { let sk = [7u8; 32]; assert!(profile::sign(&sk, "bad\nname", "bio", "", 1).is_err()); assert!(profile::decode(b"prf bad\nname\nbio\n\n1\n00").is_none()); } #[test] fn chat_is_not_profile() { assert!(profile::decode(b"hello wire").is_none()); } #[test] fn self_profile_roundtrip() { let (_dir, store) = store(); store.set_self_profile("me", "a bio", "4abc").unwrap(); let got = store.self_profile().unwrap(); assert_eq!(got.display_name, "me"); assert_eq!(got.bio, "a bio"); assert_eq!(got.xmr_addr, "4abc"); assert!(got.updated_at > 0); store.set_self_profile("", "", "").unwrap(); let empty = store.self_profile().unwrap(); assert_eq!(empty.display_name, ""); assert!(empty.updated_at >= got.updated_at); } #[test] fn slash_profile_who_and_wipe_still_parse() { assert_eq!(parse_cmd("/profile"), Some(SlashCmd::Profile)); assert_eq!(parse_cmd(" /profile "), Some(SlashCmd::Profile)); assert_eq!(parse_cmd("/who"), Some(SlashCmd::Who)); assert_eq!(parse_cmd("/who\n"), Some(SlashCmd::Who)); assert_eq!(parse_cmd("/wipe"), Some(SlashCmd::Wipe(WipeKind::Messages))); assert_eq!(parse_slash("/wipe"), Some(WipeKind::Messages)); assert_eq!(parse_slash("/wipe-all"), Some(WipeKind::All)); assert_eq!(parse_slash("/profile"), None); assert_eq!(parse_cmd("/rotate"), None); } #[test] fn profile_save_empty_name_ok_no_confirm_phrase() { let saved = onionwire::tui::ProfileEditor::new("", "", "").on_enter(); assert_eq!(saved.display_name, ""); assert_eq!(saved.bio, ""); assert_eq!(saved.xmr_addr, ""); } #[test] fn who_empty_says_no_profile_yet() { let t = onionwire::tui::who_overlay_text("abcd", "x.onion", None); assert!(t.contains("no profile yet")); assert!(t.contains("abcd")); assert!(t.contains("x.onion")); }