//! M5: wipe messages (keep identity + friends); wipe-all is a new person. use onionwire::Store; use onionwire::tui::{ QuitDecision, QuitPrompt, WipeDecision, WipeKind, WipePrompt, parse_slash, quit_screen_text, wipe_screen_text, }; fn pk(tag: u8) -> [u8; 32] { let mut k = [0u8; 32]; k[0] = tag; k } fn db_contains(home: &std::path::Path, needle: &[u8]) -> bool { let Ok(bytes) = std::fs::read(home.join("onionwire.db")) else { return false; }; bytes.windows(needle.len()).any(|w| w == needle) } #[test] fn wipe_clears_messages_keeps_self_and_friends() { let dir = tempfile::tempdir().expect("tempdir"); let store = Store::open_at_with_passphrase(dir.path(), "onionwire-test").expect("open"); let me = store.self_identity().unwrap(); store .upsert_friend(&pk(1), "a.onion", Some("alice")) .unwrap(); store .append_message(&pk(1), "out", b"secret-log-line-xyz") .unwrap(); store.append_message(&pk(1), "in", b"reply").unwrap(); assert_eq!(store.list_messages(&pk(1)).unwrap().len(), 2); store.wipe_messages().expect("wipe"); assert!(store.list_messages(&pk(1)).unwrap().is_empty()); assert_eq!(store.friend_count().unwrap(), 1); let f = store.get_friend(&pk(1)).unwrap().expect("friend"); assert_eq!(f.petname.as_deref(), Some("alice")); assert_eq!(f.onion, "a.onion"); let me2 = store.self_identity().unwrap(); assert_eq!(me.identity_pk, me2.identity_pk); assert_eq!(me.identity_sk, me2.identity_sk); drop(store); assert!( !db_contains(dir.path(), b"secret-log-line-xyz"), "wipe must overwrite plaintext before VACUUM" ); } #[test] fn wipe_all_removes_dir_so_next_open_is_new_identity() { let dir = tempfile::tempdir().expect("tempdir"); let home = dir.path().join("ow"); let store = Store::open_at_with_passphrase(&home, "onionwire-test").expect("open"); let old_pk = store.self_identity().unwrap().identity_pk; store.upsert_friend(&pk(1), "a.onion", None).unwrap(); store.append_message(&pk(1), "out", b"gone").unwrap(); drop(store); assert!(home.join("onionwire.db").is_file()); Store::wipe_all(&home).expect("wipe-all"); assert!(!home.exists(), "data dir gone"); let store2 = Store::open_at_with_passphrase(&home, "onionwire-test").expect("reopen"); let new_pk = store2.self_identity().unwrap().identity_pk; assert_ne!(old_pk, new_pk, "new identity key = new person"); assert_eq!(store2.friend_count().unwrap(), 0); } #[test] fn slash_wipe_commands() { assert_eq!(parse_slash("/wipe"), Some(WipeKind::Messages)); assert_eq!(parse_slash(" /wipe "), Some(WipeKind::Messages)); assert_eq!(parse_slash("/wipe-all"), Some(WipeKind::All)); assert_eq!(parse_slash("/wipe-all\n"), Some(WipeKind::All)); assert_eq!(parse_slash("wipe"), None); assert_eq!(parse_slash("/rotate"), None); } #[test] fn wipe_requires_typing_not_enter() { let mut p = WipePrompt::messages(); assert_eq!(p.on_esc(), WipeDecision::Cancel); assert_eq!( p.on_char('\n'), WipeDecision::Pending, "Enter must not wipe" ); for c in "WIP".chars() { assert_eq!(p.on_char(c), WipeDecision::Pending); } assert_eq!(p.on_char('E'), WipeDecision::Confirm); } #[test] fn wipe_all_requires_typing_wipeall() { let mut p = WipePrompt::all(); assert_eq!(p.on_char('\n'), WipeDecision::Pending); for c in "WIPEAL".chars() { assert_eq!(p.on_char(c), WipeDecision::Pending); } assert_eq!(p.on_char('L'), WipeDecision::Confirm); } #[test] fn wipe_prompt_text_matches_spec() { let m = wipe_screen_text(WipeKind::Messages); assert!(m.contains("Wipe message log?")); assert!(m.contains("Identity key and friends stay.")); assert!(m.contains("Type WIPE to confirm")); assert!(m.contains("Esc to cancel")); let a = wipe_screen_text(WipeKind::All); assert!(a.contains("Wipe ALL local data?")); assert!(a.contains("This deletes your identity key. You become a new person.")); assert!(a.contains("Type WIPEALL to confirm")); assert!(a.contains("Esc to cancel")); } #[test] fn quit_requires_clear_or_quit_then_enter() { let mut p = QuitPrompt::new(); assert_eq!(p.on_esc(), QuitDecision::Cancel); assert_eq!( p.on_char('\n'), QuitDecision::Pending, "Enter alone must not quit" ); for c in "CLEA".chars() { assert_eq!(p.on_char(c), QuitDecision::Pending); } assert_eq!(p.on_char('R'), QuitDecision::Pending, "CLEAR without Enter"); assert_eq!(p.on_char('\n'), QuitDecision::ClearAndQuit); let mut q = QuitPrompt::new(); for c in "QUIT".chars() { assert_eq!(q.on_char(c), QuitDecision::Pending); } assert_eq!(q.on_char('\n'), QuitDecision::Quit); } #[test] fn quit_prompt_text_matches_spec() { let t = quit_screen_text(); assert!( t.contains("message history can be cleared") || t.contains("Message history can be cleared") ); assert!(t.contains("identity") && t.contains("friends")); assert!(t.contains("not") && t.contains("/wipe-all")); assert!(t.contains("CLEAR")); assert!(t.contains("QUIT")); assert!(t.contains("Esc")); }