@@ -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