@@ -489,6 +489,18 @@ mod recurring_active_tests {
489489 // Setup mock record and test active vs paused states...
490490 }
491491}
492+ // ── #735: transfer_recurring_payer ───────────────────────────────────────────
493+
494+ fn transfer_setup(
495+ ) -> (
496+ Env ,
497+ crate :: contract:: VeriTixPayClient < ' static > ,
498+ Address ,
499+ Address ,
500+ Address ,
501+ u32 ,
502+ Address ,
503+ ) {
492504// ── #749: recurring execution window ─────────────────────────────────────────
493505
494506#[ test]
@@ -509,6 +521,79 @@ fn test_execute_recurring_within_execution_window_succeeds() {
509521 soroban_sdk:: token:: StellarAssetClient :: new ( & e, & token) . mint ( & payer, & 1000 ) ;
510522
511523 let id = client. setup_recurring ( & payer, & payee, & token, & 100 , & 100 , & 5 ) ;
524+ ( e, client, payer, payee, token, id, contract_id)
525+ }
526+
527+ #[ test]
528+ fn test_transfer_recurring_payer_succeeds_with_both_auth ( ) {
529+ use soroban_sdk:: testutils:: Address as _;
530+ let ( e, client, payer, _payee, _token, id, contract_id) = transfer_setup ( ) ;
531+ let new_payer = Address :: generate ( & e) ;
532+
533+ // Both the current payer (caller) and the new payer authenticate.
534+ client. transfer_recurring_payer ( & payer, & id, & new_payer) ;
535+
536+ let record = read_recurring_record ( & e, & contract_id, id) ;
537+ assert_eq ! ( record. payer, new_payer) ;
538+ }
539+
540+ #[ test]
541+ #[ should_panic( expected = "not the payer" ) ]
542+ fn test_transfer_recurring_payer_old_payer_only_panics ( ) {
543+ use soroban_sdk:: testutils:: Address as _;
544+ let ( e, client, _payer, _payee, _token, id, _contract_id) = transfer_setup ( ) ;
545+ let new_payer = Address :: generate ( & e) ;
546+
547+ // A caller that is not the current payer is refused, so a transfer that
548+ // only involves the old payer's authorization cannot succeed.
549+ let intruder = Address :: generate ( & e) ;
550+ client. transfer_recurring_payer ( & intruder, & id, & new_payer) ;
551+ }
552+
553+ #[ test]
554+ #[ should_panic( expected = "not the payer" ) ]
555+ fn test_transfer_recurring_payer_new_payer_only_panics ( ) {
556+ use soroban_sdk:: testutils:: Address as _;
557+ let ( e, client, _payer, payee, _token, id, _contract_id) = transfer_setup ( ) ;
558+ let new_payer = Address :: generate ( & e) ;
559+
560+ // The payee is not the payer, so authorizing only the new payer still fails.
561+ client. transfer_recurring_payer ( & payee, & id, & new_payer) ;
562+ }
563+
564+ #[ test]
565+ #[ should_panic( expected = "new payer must differ from current payer" ) ]
566+ fn test_transfer_recurring_payer_same_address_panics ( ) {
567+ let ( _e, client, payer, _payee, _token, id, _contract_id) = transfer_setup ( ) ;
568+
569+ client. transfer_recurring_payer ( & payer, & id, & payer) ;
570+ }
571+
572+ #[ test]
573+ fn test_transfer_recurring_payer_updates_payer_index ( ) {
574+ use soroban_sdk:: testutils:: Address as _;
575+ let ( e, client, payer, _payee, _token, id, _contract_id) = transfer_setup ( ) ;
576+ let new_payer = Address :: generate ( & e) ;
577+
578+ client. transfer_recurring_payer ( & payer, & id, & new_payer) ;
579+
580+ // The recurring id moves from the old payer's index to the new payer's.
581+ let old_ids = client. get_recurring_by_payer ( & payer) ;
582+ assert_eq ! ( old_ids. len( ) , 0 ) ;
583+ let new_ids = client. get_recurring_by_payer ( & new_payer) ;
584+ assert_eq ! ( new_ids. len( ) , 1 ) ;
585+ assert_eq ! ( new_ids. get( 0 ) . unwrap( ) , id) ;
586+ }
587+
588+ #[ test]
589+ #[ should_panic( expected = "recurring is not active" ) ]
590+ fn test_transfer_recurring_payer_inactive_recurring_panics ( ) {
591+ use soroban_sdk:: testutils:: Address as _;
592+ let ( e, client, payer, _payee, _token, id, _contract_id) = transfer_setup ( ) ;
593+ let new_payer = Address :: generate ( & e) ;
594+
595+ client. cancel_recurring ( & payer, & id) ;
596+ client. transfer_recurring_payer ( & payer, & id, & new_payer) ;
512597
513598 // Configure a 1000-ledger execution window.
514599 client. set_recurring_execution_window ( & admin, & 1000 ) ;
0 commit comments