2026-09-10 14:25:31 -04:00
|
|
|
//! 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");
|
2026-09-10 17:19:50 -04:00
|
|
|
let store = Store::open_at_with_passphrase(dir.path(), "onionwire-test").expect("open");
|
2026-09-10 14:25:31 -04:00
|
|
|
(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"));
|
|
|
|
|
}
|