]> Untitled Git - bdk-cli/commitdiff
Add payjoin persistence test
authorMshehu5 <musheu@gmail.com>
Fri, 29 May 2026 10:53:44 +0000 (11:53 +0100)
committerMshehu5 <musheu@gmail.com>
Thu, 2 Jul 2026 11:30:30 +0000 (12:30 +0100)
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

index 69ead104a5d0c9580bfe2c00fbde826b72a814e3..f21524d3f2cc8e29a6479fdf2a24ead1a21f5e15 100644 (file)
@@ -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<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);
+    }
+}