@@ -570,3 +570,210 @@ where
570
570
self . handle_message ( message, lsp_node_id)
571
571
}
572
572
}
573
+
574
+ #[ cfg( test) ]
575
+ mod tests {
576
+ #![ cfg( all( test, feature = "time" ) ) ]
577
+ use core:: time:: Duration ;
578
+
579
+ use super :: * ;
580
+ use crate :: {
581
+ lsps0:: ser:: LSPSRequestId ,
582
+ lsps5:: { msgs:: SetWebhookResponse , service:: DefaultTimeProvider } ,
583
+ tests:: utils:: TestEntropy ,
584
+ } ;
585
+ use bitcoin:: { key:: Secp256k1 , secp256k1:: SecretKey } ;
586
+
587
+ fn setup_test_client (
588
+ time_provider : Arc < dyn TimeProvider > ,
589
+ ) -> (
590
+ LSPS5ClientHandler < Arc < TestEntropy > , Arc < dyn TimeProvider > > ,
591
+ Arc < MessageQueue > ,
592
+ Arc < EventQueue > ,
593
+ PublicKey ,
594
+ PublicKey ,
595
+ ) {
596
+ let test_entropy_source = Arc :: new ( TestEntropy { } ) ;
597
+ let message_queue = Arc :: new ( MessageQueue :: new ( ) ) ;
598
+ let event_queue = Arc :: new ( EventQueue :: new ( ) ) ;
599
+ let client = LSPS5ClientHandler :: new (
600
+ test_entropy_source,
601
+ message_queue. clone ( ) ,
602
+ event_queue. clone ( ) ,
603
+ LSPS5ClientConfig :: default ( ) ,
604
+ time_provider,
605
+ ) ;
606
+
607
+ let secp = Secp256k1 :: new ( ) ;
608
+ let secret_key_1 = SecretKey :: from_slice ( & [ 42u8 ; 32 ] ) . unwrap ( ) ;
609
+ let secret_key_2 = SecretKey :: from_slice ( & [ 43u8 ; 32 ] ) . unwrap ( ) ;
610
+ let peer_1 = PublicKey :: from_secret_key ( & secp, & secret_key_1) ;
611
+ let peer_2 = PublicKey :: from_secret_key ( & secp, & secret_key_2) ;
612
+
613
+ ( client, message_queue, event_queue, peer_1, peer_2)
614
+ }
615
+
616
+ #[ test]
617
+ fn test_per_peer_state_isolation ( ) {
618
+ let ( client, _, _, peer_1, peer_2) = setup_test_client ( Arc :: new ( DefaultTimeProvider ) ) ;
619
+
620
+ let req_id_1 = client
621
+ . set_webhook ( peer_1, "test-app-1" . to_string ( ) , "https://example.com/hook1" . to_string ( ) )
622
+ . unwrap ( ) ;
623
+ let req_id_2 = client
624
+ . set_webhook ( peer_2, "test-app-2" . to_string ( ) , "https://example.com/hook2" . to_string ( ) )
625
+ . unwrap ( ) ;
626
+
627
+ {
628
+ let outer_state_lock = client. per_peer_state . read ( ) . unwrap ( ) ;
629
+
630
+ let peer_1_state = outer_state_lock. get ( & peer_1) . unwrap ( ) . lock ( ) . unwrap ( ) ;
631
+ assert ! ( peer_1_state. pending_set_webhook_requests. contains_key( & req_id_1) ) ;
632
+
633
+ let peer_2_state = outer_state_lock. get ( & peer_2) . unwrap ( ) . lock ( ) . unwrap ( ) ;
634
+ assert ! ( peer_2_state. pending_set_webhook_requests. contains_key( & req_id_2) ) ;
635
+ }
636
+ }
637
+
638
+ #[ test]
639
+ fn test_pending_request_tracking ( ) {
640
+ let ( client, _, _, peer, _) = setup_test_client ( Arc :: new ( DefaultTimeProvider ) ) ;
641
+ const APP_NAME : & str = "test-app" ;
642
+ const WEBHOOK_URL : & str = "https://example.com/hook" ;
643
+ let lsps5_app_name = LSPS5AppName :: from_string ( APP_NAME . to_string ( ) ) . unwrap ( ) ;
644
+ let lsps5_webhook_url = LSPS5WebhookUrl :: from_string ( WEBHOOK_URL . to_string ( ) ) . unwrap ( ) ;
645
+ let set_req_id =
646
+ client. set_webhook ( peer, APP_NAME . to_string ( ) , WEBHOOK_URL . to_string ( ) ) . unwrap ( ) ;
647
+ let list_req_id = client. list_webhooks ( peer) ;
648
+ let remove_req_id = client. remove_webhook ( peer, "test-app" . to_string ( ) ) . unwrap ( ) ;
649
+
650
+ {
651
+ let outer_state_lock = client. per_peer_state . read ( ) . unwrap ( ) ;
652
+ let peer_state = outer_state_lock. get ( & peer) . unwrap ( ) . lock ( ) . unwrap ( ) ;
653
+ assert_eq ! (
654
+ peer_state. pending_set_webhook_requests. get( & set_req_id) . unwrap( ) ,
655
+ & (
656
+ lsps5_app_name. clone( ) ,
657
+ lsps5_webhook_url,
658
+ peer_state. pending_set_webhook_requests. get( & set_req_id) . unwrap( ) . 2 . clone( )
659
+ )
660
+ ) ;
661
+
662
+ assert ! ( peer_state. pending_list_webhooks_requests. contains_key( & list_req_id) ) ;
663
+
664
+ assert_eq ! (
665
+ peer_state. pending_remove_webhook_requests. get( & remove_req_id) . unwrap( ) . 0 ,
666
+ lsps5_app_name
667
+ ) ;
668
+ }
669
+ }
670
+
671
+ #[ test]
672
+ fn test_handle_response_clears_pending_state ( ) {
673
+ let ( client, _, _, peer, _) = setup_test_client ( Arc :: new ( DefaultTimeProvider ) ) ;
674
+
675
+ let req_id = client
676
+ . set_webhook ( peer, "test-app" . to_string ( ) , "https://example.com/hook" . to_string ( ) )
677
+ . unwrap ( ) ;
678
+
679
+ let response = LSPS5Response :: SetWebhook ( SetWebhookResponse {
680
+ num_webhooks : 1 ,
681
+ max_webhooks : 5 ,
682
+ no_change : false ,
683
+ } ) ;
684
+ let response_msg = LSPS5Message :: Response ( req_id. clone ( ) , response) ;
685
+
686
+ {
687
+ let outer_state_lock = client. per_peer_state . read ( ) . unwrap ( ) ;
688
+ let peer_state = outer_state_lock. get ( & peer) . unwrap ( ) . lock ( ) . unwrap ( ) ;
689
+ assert ! ( peer_state. pending_set_webhook_requests. contains_key( & req_id) ) ;
690
+ }
691
+
692
+ client. handle_message ( response_msg, & peer) . unwrap ( ) ;
693
+
694
+ {
695
+ let outer_state_lock = client. per_peer_state . read ( ) . unwrap ( ) ;
696
+ let peer_state = outer_state_lock. get ( & peer) . unwrap ( ) . lock ( ) . unwrap ( ) ;
697
+ assert ! ( !peer_state. pending_set_webhook_requests. contains_key( & req_id) ) ;
698
+ }
699
+ }
700
+
701
+ #[ test]
702
+ fn test_cleanup_expired_responses ( ) {
703
+ let ( client, _, _, _, _) = setup_test_client ( Arc :: new ( DefaultTimeProvider ) ) ;
704
+ let time_provider = & client. time_provider ;
705
+ const OLD_APP_NAME : & str = "test-app-old" ;
706
+ const NEW_APP_NAME : & str = "test-app-new" ;
707
+ const WEBHOOK_URL : & str = "https://example.com/hook" ;
708
+ let lsps5_old_app_name = LSPS5AppName :: from_string ( OLD_APP_NAME . to_string ( ) ) . unwrap ( ) ;
709
+ let lsps5_new_app_name = LSPS5AppName :: from_string ( NEW_APP_NAME . to_string ( ) ) . unwrap ( ) ;
710
+ let lsps5_webhook_url = LSPS5WebhookUrl :: from_string ( WEBHOOK_URL . to_string ( ) ) . unwrap ( ) ;
711
+ let now = time_provider. duration_since_epoch ( ) ;
712
+ let mut peer_state = PeerState :: new ( Duration :: from_secs ( 1800 ) , time_provider. clone ( ) ) ;
713
+ peer_state. last_cleanup = Some ( LSPSDateTime :: new_from_duration_since_epoch (
714
+ now. checked_sub ( Duration :: from_secs ( 120 ) ) . unwrap ( ) ,
715
+ ) ) ;
716
+
717
+ let old_request_id = LSPSRequestId ( "test:request:old" . to_string ( ) ) ;
718
+ let new_request_id = LSPSRequestId ( "test:request:new" . to_string ( ) ) ;
719
+
720
+ // Add an old request (should be removed during cleanup)
721
+ peer_state. pending_set_webhook_requests . insert (
722
+ old_request_id. clone ( ) ,
723
+ (
724
+ lsps5_old_app_name,
725
+ lsps5_webhook_url. clone ( ) ,
726
+ LSPSDateTime :: new_from_duration_since_epoch (
727
+ now. checked_sub ( Duration :: from_secs ( 7200 ) ) . unwrap ( ) ,
728
+ ) ,
729
+ ) , // 2 hours old
730
+ ) ;
731
+
732
+ // Add a recent request (should be kept)
733
+ peer_state. pending_set_webhook_requests . insert (
734
+ new_request_id. clone ( ) ,
735
+ (
736
+ lsps5_new_app_name,
737
+ lsps5_webhook_url,
738
+ LSPSDateTime :: new_from_duration_since_epoch (
739
+ now. checked_sub ( Duration :: from_secs ( 600 ) ) . unwrap ( ) ,
740
+ ) ,
741
+ ) , // 10 minutes old
742
+ ) ;
743
+
744
+ peer_state. cleanup_expired_responses ( ) ;
745
+
746
+ assert ! ( !peer_state. pending_set_webhook_requests. contains_key( & old_request_id) ) ;
747
+ assert ! ( peer_state. pending_set_webhook_requests. contains_key( & new_request_id) ) ;
748
+
749
+ let cleanup_age = if let Some ( last_cleanup) = peer_state. last_cleanup {
750
+ LSPSDateTime :: new_from_duration_since_epoch ( time_provider. duration_since_epoch ( ) )
751
+ . abs_diff ( last_cleanup)
752
+ } else {
753
+ 0
754
+ } ;
755
+ assert ! ( cleanup_age < 10 ) ;
756
+ }
757
+
758
+ #[ test]
759
+ fn test_unknown_request_id_handling ( ) {
760
+ let ( client, _message_queue, _, peer, _) = setup_test_client ( Arc :: new ( DefaultTimeProvider ) ) ;
761
+
762
+ let _valid_req = client
763
+ . set_webhook ( peer, "test-app" . to_string ( ) , "https://example.com/hook" . to_string ( ) )
764
+ . unwrap ( ) ;
765
+
766
+ let unknown_req_id = LSPSRequestId ( "unknown:request:id" . to_string ( ) ) ;
767
+ let response = LSPS5Response :: SetWebhook ( SetWebhookResponse {
768
+ num_webhooks : 1 ,
769
+ max_webhooks : 5 ,
770
+ no_change : false ,
771
+ } ) ;
772
+ let response_msg = LSPS5Message :: Response ( unknown_req_id, response) ;
773
+
774
+ let result = client. handle_message ( response_msg, & peer) ;
775
+ assert ! ( result. is_err( ) ) ;
776
+ let error = result. unwrap_err ( ) ;
777
+ assert ! ( error. err. to_lowercase( ) . contains( "unknown request id" ) ) ;
778
+ }
779
+ }
0 commit comments