@@ -570,3 +570,210 @@ where
570570 self . handle_message ( message, lsp_node_id)
571571 }
572572}
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