From 262ce86573a98bfe33cb92d52d2da0acece32538 Mon Sep 17 00:00:00 2001 From: Mshehu5 Date: Fri, 29 May 2026 11:53:44 +0100 Subject: [PATCH] Add payjoin persistence test Cover the new payjoin persistence paths with focused unit tests. Exercise replay protection, sender and receiver event persistence, session pruning and history rendering. These cases were chosen because the PR's new behavior is concentrated in the persistence layer and session history path --- src/payjoin/db.rs | 306 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 306 insertions(+) diff --git a/src/payjoin/db.rs b/src/payjoin/db.rs index 69ead10..f21524d 100644 --- a/src/payjoin/db.rs +++ b/src/payjoin/db.rs @@ -527,3 +527,309 @@ impl SessionPersister for ReceiverPersister { Ok(()) } } + +#[cfg(test)] +mod tests { + use std::path::PathBuf; + use std::time::{SystemTime, UNIX_EPOCH}; + + use payjoin::HpkeKeyPair; + use payjoin::persist::SessionPersister as _; + use payjoin::receive::v2::SessionOutcome as ReceiverSessionOutcome; + use payjoin::send::v2::SessionOutcome as SenderSessionOutcome; + + use super::*; + + fn sample_receiver_pubkey() -> HpkePublicKey { + HpkeKeyPair::gen_keypair().1 + } + + fn unique_test_datadir(label: &str) -> PathBuf { + let nanos = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("clock should be after unix epoch") + .as_nanos(); + std::env::temp_dir().join(format!( + "bdk-cli-payjoin-{label}-{}-{nanos}", + std::process::id() + )) + } + + #[test] + fn insert_input_seen_before_reports_replays() { + let db = Database::create(":memory:").expect("in-memory database should open"); + let input = OutPoint::null(); + + assert!( + !db.insert_input_seen_before(input) + .expect("first insert should succeed"), + "first observation should not be treated as a replay" + ); + assert!( + db.insert_input_seen_before(input) + .expect("second insert should succeed"), + "second observation should be treated as a replay" + ); + } + + #[test] + fn persisters_round_trip_events_and_transition_sessions() { + let db = Arc::new(Database::create(":memory:").expect("in-memory database should open")); + + let sender = SenderPersister::new(db.clone(), sample_receiver_pubkey()) + .expect("sender session should be created"); + sender + .save_event(SenderSessionEvent::PostedOriginalPsbt()) + .expect("sender event should persist"); + sender + .save_event(SenderSessionEvent::Closed(SenderSessionOutcome::Failure)) + .expect("sender close event should persist"); + + let sender_events: Vec<_> = sender.load().expect("sender events should load").collect(); + assert_eq!( + sender_events, + vec![ + SenderSessionEvent::PostedOriginalPsbt(), + SenderSessionEvent::Closed(SenderSessionOutcome::Failure), + ] + ); + + let active_sender_ids = db + .get_send_session_ids() + .expect("active sender ids should load"); + assert_eq!(active_sender_ids.len(), 1); + assert_eq!( + db.get_send_session_receiver_pk(&active_sender_ids[0]) + .expect("receiver pubkey should load") + .to_compressed_bytes() + .len(), + 33 + ); + + sender.close().expect("sender session should close"); + assert!( + db.get_send_session_ids() + .expect("active sender ids should load") + .is_empty() + ); + assert_eq!( + db.get_inactive_send_session_ids() + .expect("inactive sender ids should load") + .len(), + 1 + ); + + let receiver = + ReceiverPersister::new(db.clone()).expect("receiver session should be created"); + receiver + .save_event(ReceiverSessionEvent::CheckedBroadcastSuitability()) + .expect("receiver event should persist"); + receiver + .save_event(ReceiverSessionEvent::Closed(ReceiverSessionOutcome::Cancel)) + .expect("receiver close event should persist"); + + let receiver_events: Vec<_> = receiver + .load() + .expect("receiver events should load") + .collect(); + assert_eq!( + receiver_events, + vec![ + ReceiverSessionEvent::CheckedBroadcastSuitability(), + ReceiverSessionEvent::Closed(ReceiverSessionOutcome::Cancel), + ] + ); + + receiver.close().expect("receiver session should close"); + assert!( + db.get_recv_session_ids() + .expect("active receiver ids should load") + .is_empty() + ); + assert_eq!( + db.get_inactive_recv_session_ids() + .expect("inactive receiver ids should load") + .len(), + 1 + ); + } + + #[test] + fn prune_expired_sessions_drops_stale_send_and_receive_rows() { + let db = Database::create(":memory:").expect("in-memory database should open"); + let stale_timestamp = now() - SESSION_RETENTION_SECS - 1; + let fresh_timestamp = now(); + let receiver_pubkey = sample_receiver_pubkey(); + + db.conn() + .execute( + "INSERT INTO send_sessions (session_id, receiver_pubkey, completed_at) + VALUES (?1, ?2, ?3)", + params![ + 1_i64, + receiver_pubkey.to_compressed_bytes(), + stale_timestamp + ], + ) + .expect("stale completed send session should insert"); + db.conn() + .execute( + "INSERT INTO send_sessions (session_id, receiver_pubkey, completed_at) + VALUES (?1, ?2, NULL)", + params![2_i64, receiver_pubkey.to_compressed_bytes()], + ) + .expect("stale active send session should insert"); + db.conn() + .execute( + "INSERT INTO send_session_events (session_id, event_data, created_at) + VALUES (?1, ?2, ?3)", + params![ + 2_i64, + serde_json::to_string(&SenderSessionEvent::PostedOriginalPsbt()) + .expect("event should serialize"), + stale_timestamp + ], + ) + .expect("stale send event should insert"); + db.conn() + .execute( + "INSERT INTO send_sessions (session_id, receiver_pubkey, completed_at) + VALUES (?1, ?2, NULL)", + params![3_i64, receiver_pubkey.to_compressed_bytes()], + ) + .expect("fresh active send session should insert"); + db.conn() + .execute( + "INSERT INTO send_session_events (session_id, event_data, created_at) + VALUES (?1, ?2, ?3)", + params![ + 3_i64, + serde_json::to_string(&SenderSessionEvent::PostedOriginalPsbt()) + .expect("event should serialize"), + fresh_timestamp + ], + ) + .expect("fresh send event should insert"); + + db.conn() + .execute( + "INSERT INTO receive_sessions (session_id, completed_at) VALUES (?1, ?2)", + params![4_i64, stale_timestamp], + ) + .expect("stale completed receive session should insert"); + db.conn() + .execute( + "INSERT INTO receive_sessions (session_id, completed_at) VALUES (?1, NULL)", + params![5_i64], + ) + .expect("stale active receive session should insert"); + db.conn() + .execute( + "INSERT INTO receive_session_events (session_id, event_data, created_at) + VALUES (?1, ?2, ?3)", + params![ + 5_i64, + serde_json::to_string(&ReceiverSessionEvent::CheckedBroadcastSuitability()) + .expect("event should serialize"), + stale_timestamp + ], + ) + .expect("stale receive event should insert"); + db.conn() + .execute( + "INSERT INTO receive_sessions (session_id, completed_at) VALUES (?1, NULL)", + params![6_i64], + ) + .expect("fresh active receive session should insert"); + db.conn() + .execute( + "INSERT INTO receive_session_events (session_id, event_data, created_at) + VALUES (?1, ?2, ?3)", + params![ + 6_i64, + serde_json::to_string(&ReceiverSessionEvent::CheckedBroadcastSuitability()) + .expect("event should serialize"), + fresh_timestamp + ], + ) + .expect("fresh receive event should insert"); + + db.prune_expired_sessions().expect("pruning should succeed"); + + let remaining_send_ids: Vec = db + .conn() + .prepare("SELECT session_id FROM send_sessions ORDER BY session_id") + .expect("statement should prepare") + .query_map([], |row| row.get(0)) + .expect("query should execute") + .map(|row| row.expect("row should decode")) + .collect(); + assert_eq!(remaining_send_ids, vec![3]); + + let remaining_receive_ids: Vec = db + .conn() + .prepare("SELECT session_id FROM receive_sessions ORDER BY session_id") + .expect("statement should prepare") + .query_map([], |row| row.get(0)) + .expect("query should execute") + .map(|row| row.expect("row should decode")) + .collect(); + assert_eq!(remaining_receive_ids, vec![6]); + } + + #[test] + fn history_lists_active_and_completed_sessions() { + let datadir = unique_test_datadir("history"); + let wallet_name = "history-wallet"; + let db = open_payjoin_db(Some(datadir.clone()), wallet_name) + .expect("database should open in temp directory"); + + let active_sender = SenderPersister::new(db.clone(), sample_receiver_pubkey()) + .expect("active sender should be created"); + let active_receiver = + ReceiverPersister::new(db.clone()).expect("active receiver should be created"); + + let completed_sender = SenderPersister::new(db.clone(), sample_receiver_pubkey()) + .expect("completed sender should be created"); + completed_sender + .close() + .expect("completed sender should close"); + + let completed_receiver = + ReceiverPersister::new(db.clone()).expect("completed receiver should be created"); + completed_receiver + .close() + .expect("completed receiver should close"); + + let active_send_ids = db + .get_send_session_ids() + .expect("active sender ids should load"); + let active_recv_ids = db + .get_recv_session_ids() + .expect("active receiver ids should load"); + let inactive_send_ids = db + .get_inactive_send_session_ids() + .expect("inactive sender ids should load"); + let inactive_recv_ids = db + .get_inactive_recv_session_ids() + .expect("inactive receiver ids should load"); + + let table = crate::payjoin::PayjoinManager::history(Some(datadir.clone()), wallet_name) + .expect("history should render"); + + assert!(table.contains("Sender")); + assert!(table.contains("Receiver")); + assert!(table.contains(&active_send_ids[0].to_string())); + assert!(table.contains(&active_recv_ids[0].to_string())); + assert!(table.contains(&inactive_send_ids[0].0.to_string())); + assert!(table.contains(&inactive_recv_ids[0].0.to_string())); + assert!(table.contains("Not Completed")); + + drop(active_sender); + drop(active_receiver); + drop(completed_sender); + drop(completed_receiver); + drop(db); + let _ = std::fs::remove_dir_all(datadir); + } +} -- 2.49.0