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<i64> = 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<i64> = 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);
+ }
+}