onionwire/tests/wipe.rs
Sirius DevOps 45c105de8a
All checks were successful
ci / test (pull_request) Successful in 3m1s
fix(store): drop payments on /wipe; document non-forensic
/wipe now deletes payments as well as chat, checkpoints WAL, and
enables sqlite secure_delete (flash still not crypto-shred).
2026-09-10 20:22:31 -04:00

207 lines
7.1 KiB
Rust

//! M5: wipe messages (keep identity + friends); wipe-all is a new person.
use onionwire::tui::{
parse_slash, quit_screen_text, wipe_screen_text, QuitDecision, QuitPrompt, WipeDecision,
WipeKind, WipePrompt,
};
use onionwire::{PaymentWrite, Store};
// Official mainnet standard from Monero docs (same fixture as tests/pay.rs).
const MAINNET_STD: &str = "4AdUndXHHZ6cfufTMvppY6JwXNouMBzSkbLYfpAV5Usx3skxNgYeYTRj5UzqtReoS44qo9mtmXCqY45DJ852K5Jv2684Rge";
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_clears_payments_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
.insert_payment(
&pk(1),
PaymentWrite {
dir: "out",
kind: "receipt",
amount_atomic: "1000000000000",
address: MAINNET_STD,
memo: "counterparty-memo-xyz",
txid: Some("aabbccddeeff00112233445566778899aabbccddeeff00112233445566778899"),
verified: true,
},
)
.unwrap();
assert_eq!(store.list_messages(&pk(1)).unwrap().len(), 1);
assert_eq!(store.list_payments(&pk(1)).unwrap().len(), 1);
store.wipe_messages().expect("wipe");
assert!(store.list_messages(&pk(1)).unwrap().is_empty());
assert!(
store.list_payments(&pk(1)).unwrap().is_empty(),
"wipe must drop payments, not only chat"
);
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"));
let me2 = store.self_identity().unwrap();
assert_eq!(me.identity_pk, me2.identity_pk);
drop(store);
assert!(
!db_contains(dir.path(), b"counterparty-memo-xyz"),
"wipe must not leave payment memo in the db file"
);
}
#[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 chat and payments history?"));
assert!(m.contains("Identity key and friends stay."));
assert!(m.contains("Not a forensic erase."));
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"));
}