Skip to content

Commit 54ae896

Browse files
committed
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
1 parent 9b4e38d commit 54ae896

1 file changed

Lines changed: 306 additions & 0 deletions

File tree

src/payjoin/db.rs

Lines changed: 306 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -527,3 +527,309 @@ impl SessionPersister for ReceiverPersister {
527527
Ok(())
528528
}
529529
}
530+
531+
#[cfg(test)]
532+
mod tests {
533+
use std::path::PathBuf;
534+
use std::time::{SystemTime, UNIX_EPOCH};
535+
536+
use payjoin::HpkeKeyPair;
537+
use payjoin::persist::SessionPersister as _;
538+
use payjoin::receive::v2::SessionOutcome as ReceiverSessionOutcome;
539+
use payjoin::send::v2::SessionOutcome as SenderSessionOutcome;
540+
541+
use super::*;
542+
543+
fn sample_receiver_pubkey() -> HpkePublicKey {
544+
HpkeKeyPair::gen_keypair().1
545+
}
546+
547+
fn unique_test_datadir(label: &str) -> PathBuf {
548+
let nanos = SystemTime::now()
549+
.duration_since(UNIX_EPOCH)
550+
.expect("clock should be after unix epoch")
551+
.as_nanos();
552+
std::env::temp_dir().join(format!(
553+
"bdk-cli-payjoin-{label}-{}-{nanos}",
554+
std::process::id()
555+
))
556+
}
557+
558+
#[test]
559+
fn insert_input_seen_before_reports_replays() {
560+
let db = Database::create(":memory:").expect("in-memory database should open");
561+
let input = OutPoint::null();
562+
563+
assert!(
564+
!db.insert_input_seen_before(input)
565+
.expect("first insert should succeed"),
566+
"first observation should not be treated as a replay"
567+
);
568+
assert!(
569+
db.insert_input_seen_before(input)
570+
.expect("second insert should succeed"),
571+
"second observation should be treated as a replay"
572+
);
573+
}
574+
575+
#[test]
576+
fn persisters_round_trip_events_and_transition_sessions() {
577+
let db = Arc::new(Database::create(":memory:").expect("in-memory database should open"));
578+
579+
let sender = SenderPersister::new(db.clone(), sample_receiver_pubkey())
580+
.expect("sender session should be created");
581+
sender
582+
.save_event(SenderSessionEvent::PostedOriginalPsbt())
583+
.expect("sender event should persist");
584+
sender
585+
.save_event(SenderSessionEvent::Closed(SenderSessionOutcome::Failure))
586+
.expect("sender close event should persist");
587+
588+
let sender_events: Vec<_> = sender.load().expect("sender events should load").collect();
589+
assert_eq!(
590+
sender_events,
591+
vec![
592+
SenderSessionEvent::PostedOriginalPsbt(),
593+
SenderSessionEvent::Closed(SenderSessionOutcome::Failure),
594+
]
595+
);
596+
597+
let active_sender_ids = db
598+
.get_send_session_ids()
599+
.expect("active sender ids should load");
600+
assert_eq!(active_sender_ids.len(), 1);
601+
assert_eq!(
602+
db.get_send_session_receiver_pk(&active_sender_ids[0])
603+
.expect("receiver pubkey should load")
604+
.to_compressed_bytes()
605+
.len(),
606+
33
607+
);
608+
609+
sender.close().expect("sender session should close");
610+
assert!(
611+
db.get_send_session_ids()
612+
.expect("active sender ids should load")
613+
.is_empty()
614+
);
615+
assert_eq!(
616+
db.get_inactive_send_session_ids()
617+
.expect("inactive sender ids should load")
618+
.len(),
619+
1
620+
);
621+
622+
let receiver =
623+
ReceiverPersister::new(db.clone()).expect("receiver session should be created");
624+
receiver
625+
.save_event(ReceiverSessionEvent::CheckedBroadcastSuitability())
626+
.expect("receiver event should persist");
627+
receiver
628+
.save_event(ReceiverSessionEvent::Closed(ReceiverSessionOutcome::Cancel))
629+
.expect("receiver close event should persist");
630+
631+
let receiver_events: Vec<_> = receiver
632+
.load()
633+
.expect("receiver events should load")
634+
.collect();
635+
assert_eq!(
636+
receiver_events,
637+
vec![
638+
ReceiverSessionEvent::CheckedBroadcastSuitability(),
639+
ReceiverSessionEvent::Closed(ReceiverSessionOutcome::Cancel),
640+
]
641+
);
642+
643+
receiver.close().expect("receiver session should close");
644+
assert!(
645+
db.get_recv_session_ids()
646+
.expect("active receiver ids should load")
647+
.is_empty()
648+
);
649+
assert_eq!(
650+
db.get_inactive_recv_session_ids()
651+
.expect("inactive receiver ids should load")
652+
.len(),
653+
1
654+
);
655+
}
656+
657+
#[test]
658+
fn prune_expired_sessions_drops_stale_send_and_receive_rows() {
659+
let db = Database::create(":memory:").expect("in-memory database should open");
660+
let stale_timestamp = now() - SESSION_RETENTION_SECS - 1;
661+
let fresh_timestamp = now();
662+
let receiver_pubkey = sample_receiver_pubkey();
663+
664+
db.conn()
665+
.execute(
666+
"INSERT INTO send_sessions (session_id, receiver_pubkey, completed_at)
667+
VALUES (?1, ?2, ?3)",
668+
params![
669+
1_i64,
670+
receiver_pubkey.to_compressed_bytes(),
671+
stale_timestamp
672+
],
673+
)
674+
.expect("stale completed send session should insert");
675+
db.conn()
676+
.execute(
677+
"INSERT INTO send_sessions (session_id, receiver_pubkey, completed_at)
678+
VALUES (?1, ?2, NULL)",
679+
params![2_i64, receiver_pubkey.to_compressed_bytes()],
680+
)
681+
.expect("stale active send session should insert");
682+
db.conn()
683+
.execute(
684+
"INSERT INTO send_session_events (session_id, event_data, created_at)
685+
VALUES (?1, ?2, ?3)",
686+
params![
687+
2_i64,
688+
serde_json::to_string(&SenderSessionEvent::PostedOriginalPsbt())
689+
.expect("event should serialize"),
690+
stale_timestamp
691+
],
692+
)
693+
.expect("stale send event should insert");
694+
db.conn()
695+
.execute(
696+
"INSERT INTO send_sessions (session_id, receiver_pubkey, completed_at)
697+
VALUES (?1, ?2, NULL)",
698+
params![3_i64, receiver_pubkey.to_compressed_bytes()],
699+
)
700+
.expect("fresh active send session should insert");
701+
db.conn()
702+
.execute(
703+
"INSERT INTO send_session_events (session_id, event_data, created_at)
704+
VALUES (?1, ?2, ?3)",
705+
params![
706+
3_i64,
707+
serde_json::to_string(&SenderSessionEvent::PostedOriginalPsbt())
708+
.expect("event should serialize"),
709+
fresh_timestamp
710+
],
711+
)
712+
.expect("fresh send event should insert");
713+
714+
db.conn()
715+
.execute(
716+
"INSERT INTO receive_sessions (session_id, completed_at) VALUES (?1, ?2)",
717+
params![4_i64, stale_timestamp],
718+
)
719+
.expect("stale completed receive session should insert");
720+
db.conn()
721+
.execute(
722+
"INSERT INTO receive_sessions (session_id, completed_at) VALUES (?1, NULL)",
723+
params![5_i64],
724+
)
725+
.expect("stale active receive session should insert");
726+
db.conn()
727+
.execute(
728+
"INSERT INTO receive_session_events (session_id, event_data, created_at)
729+
VALUES (?1, ?2, ?3)",
730+
params![
731+
5_i64,
732+
serde_json::to_string(&ReceiverSessionEvent::CheckedBroadcastSuitability())
733+
.expect("event should serialize"),
734+
stale_timestamp
735+
],
736+
)
737+
.expect("stale receive event should insert");
738+
db.conn()
739+
.execute(
740+
"INSERT INTO receive_sessions (session_id, completed_at) VALUES (?1, NULL)",
741+
params![6_i64],
742+
)
743+
.expect("fresh active receive session should insert");
744+
db.conn()
745+
.execute(
746+
"INSERT INTO receive_session_events (session_id, event_data, created_at)
747+
VALUES (?1, ?2, ?3)",
748+
params![
749+
6_i64,
750+
serde_json::to_string(&ReceiverSessionEvent::CheckedBroadcastSuitability())
751+
.expect("event should serialize"),
752+
fresh_timestamp
753+
],
754+
)
755+
.expect("fresh receive event should insert");
756+
757+
db.prune_expired_sessions().expect("pruning should succeed");
758+
759+
let remaining_send_ids: Vec<i64> = db
760+
.conn()
761+
.prepare("SELECT session_id FROM send_sessions ORDER BY session_id")
762+
.expect("statement should prepare")
763+
.query_map([], |row| row.get(0))
764+
.expect("query should execute")
765+
.map(|row| row.expect("row should decode"))
766+
.collect();
767+
assert_eq!(remaining_send_ids, vec![3]);
768+
769+
let remaining_receive_ids: Vec<i64> = db
770+
.conn()
771+
.prepare("SELECT session_id FROM receive_sessions ORDER BY session_id")
772+
.expect("statement should prepare")
773+
.query_map([], |row| row.get(0))
774+
.expect("query should execute")
775+
.map(|row| row.expect("row should decode"))
776+
.collect();
777+
assert_eq!(remaining_receive_ids, vec![6]);
778+
}
779+
780+
#[test]
781+
fn history_lists_active_and_completed_sessions() {
782+
let datadir = unique_test_datadir("history");
783+
let wallet_name = "history-wallet";
784+
let db = open_payjoin_db(Some(datadir.clone()), wallet_name)
785+
.expect("database should open in temp directory");
786+
787+
let active_sender = SenderPersister::new(db.clone(), sample_receiver_pubkey())
788+
.expect("active sender should be created");
789+
let active_receiver =
790+
ReceiverPersister::new(db.clone()).expect("active receiver should be created");
791+
792+
let completed_sender = SenderPersister::new(db.clone(), sample_receiver_pubkey())
793+
.expect("completed sender should be created");
794+
completed_sender
795+
.close()
796+
.expect("completed sender should close");
797+
798+
let completed_receiver =
799+
ReceiverPersister::new(db.clone()).expect("completed receiver should be created");
800+
completed_receiver
801+
.close()
802+
.expect("completed receiver should close");
803+
804+
let active_send_ids = db
805+
.get_send_session_ids()
806+
.expect("active sender ids should load");
807+
let active_recv_ids = db
808+
.get_recv_session_ids()
809+
.expect("active receiver ids should load");
810+
let inactive_send_ids = db
811+
.get_inactive_send_session_ids()
812+
.expect("inactive sender ids should load");
813+
let inactive_recv_ids = db
814+
.get_inactive_recv_session_ids()
815+
.expect("inactive receiver ids should load");
816+
817+
let table = crate::payjoin::PayjoinManager::history(Some(datadir.clone()), wallet_name)
818+
.expect("history should render");
819+
820+
assert!(table.contains("Sender"));
821+
assert!(table.contains("Receiver"));
822+
assert!(table.contains(&active_send_ids[0].to_string()));
823+
assert!(table.contains(&active_recv_ids[0].to_string()));
824+
assert!(table.contains(&inactive_send_ids[0].0.to_string()));
825+
assert!(table.contains(&inactive_recv_ids[0].0.to_string()));
826+
assert!(table.contains("Not Completed"));
827+
828+
drop(active_sender);
829+
drop(active_receiver);
830+
drop(completed_sender);
831+
drop(completed_receiver);
832+
drop(db);
833+
let _ = std::fs::remove_dir_all(datadir);
834+
}
835+
}

0 commit comments

Comments
 (0)