@@ -49,6 +49,7 @@ class WcGatewayTest extends TestCase
4949 private $ wcPaymentTokens ;
5050 private $ assetGetter ;
5151 private $ context ;
52+ private $ capturePayPalPayment ;
5253
5354 public function setUp (): void {
5455 parent ::setUp ();
@@ -97,6 +98,7 @@ public function setUp(): void {
9798 $ this ->paymentTokensEndpoint = Mockery::mock (PaymentTokensEndpoint::class);
9899 $ this ->wcPaymentTokens = Mockery::mock (WooCommercePaymentTokens::class);
99100 $ this ->context = Mockery::mock (Context::class);
101+ $ this ->capturePayPalPayment = Mockery::mock (CapturePayPalPayment::class);
100102 }
101103
102104 private function createGateway ()
@@ -119,7 +121,7 @@ private function createGateway()
119121 $ this ->wcPaymentTokens ,
120122 $ this ->assetGetter ,
121123 false ,
122- Mockery:: mock (CapturePayPalPayment::class) ,
124+ $ this -> capturePayPalPayment ,
123125 Mockery::mock (OrderEndpoint::class),
124126 'WC- ' ,
125127 $ this ->context
@@ -146,7 +148,34 @@ private function createSpyGateway(): SpyablePayPalGateway
146148 $ this ->wcPaymentTokens ,
147149 $ this ->assetGetter ,
148150 false ,
149- Mockery::mock (CapturePayPalPayment::class),
151+ $ this ->capturePayPalPayment ,
152+ Mockery::mock (OrderEndpoint::class),
153+ 'WC- ' ,
154+ $ this ->context
155+ );
156+ }
157+
158+ private function createReturnUrlStubGateway (): PayPalGatewayReturnUrlStub
159+ {
160+ return new PayPalGatewayReturnUrlStub (
161+ $ this ->funding_source_renderer ,
162+ $ this ->orderProcessor ,
163+ $ this ->settingsProvider ,
164+ $ this ->sessionHandler ,
165+ $ this ->refundProcessor ,
166+ $ this ->isConnected ,
167+ $ this ->transactionUrlProvider ,
168+ $ this ->subscriptionHelper ,
169+ $ this ->environment ,
170+ $ this ->logger ,
171+ $ this ->apiShopCountry ,
172+ static fn ($ id ) => 'checkoutnow= ' . $ id ,
173+ 'Pay via PayPal ' ,
174+ $ this ->paymentTokensEndpoint ,
175+ $ this ->wcPaymentTokens ,
176+ $ this ->assetGetter ,
177+ false ,
178+ $ this ->capturePayPalPayment ,
150179 Mockery::mock (OrderEndpoint::class),
151180 'WC- ' ,
152181 $ this ->context
@@ -497,6 +526,97 @@ public function test_payment_fields_with_vaulting_disabled_suppresses_saved_meth
497526 $ this ->assertSame ( 0 , $ gateway ->tokenization_script_call_count , 'tokenization_script() must not be called when vaulting is disabled ' );
498527 $ this ->assertSame ( 0 , $ gateway ->saved_payment_methods_call_count , 'saved_payment_methods() must not be called when vaulting is disabled ' );
499528 }
529+
530+ /**
531+ * @runInSeparateProcess
532+ * @preserveGlobalState disabled
533+ * @scenario Current user changes their subscription payment to a saved PayPal
534+ * token they own. The token must be attached to the order without attempting a
535+ * $0 capture (WC Subscriptions zeroes the order total during change-payment
536+ * requests, and PayPal rejects $0 create-order calls with
537+ * CANNOT_BE_ZERO_OR_NEGATIVE).
538+ */
539+ public function test_process_payment_attaches_token_when_ownership_check_passes_for_change_payment (): void {
540+ $ orderId = 1 ;
541+ $ _POST ['woocommerce_change_payment ' ] = '1 ' ;
542+ $ _POST ['wc-ppcp-gateway-payment-token ' ] = '99 ' ;
543+
544+ $ this ->subscriptionHelper ->shouldReceive ('has_subscription ' )->with ($ orderId )->andReturn (true );
545+ $ this ->subscriptionHelper ->shouldReceive ('is_subscription_change_payment ' )->andReturn (true );
546+
547+ $ wcOrder = Mockery::mock (\WC_Order::class);
548+ $ wcOrder ->shouldReceive ('get_id ' )->andReturn ($ orderId );
549+ $ wcOrder ->shouldReceive ('get_meta ' )->andReturn ('' );
550+ when ('wc_get_order ' )->justReturn ($ wcOrder );
551+
552+ $ tokens = Mockery::mock ('alias:WC_Payment_Tokens ' );
553+ $ payment_token = Mockery::mock (\WC_Payment_Token::class);
554+ $ payment_token ->shouldReceive ('get_user_id ' )->andReturn (1 );
555+ $ tokens ->shouldReceive ('get ' )->with (99 )->andReturn ($ payment_token );
556+
557+ when ('get_current_user_id ' )->justReturn (1 );
558+
559+ $ wcOrder ->shouldReceive ('add_payment_token ' )->once ()->with ($ payment_token );
560+ $ wcOrder ->shouldReceive ('save ' )->once ();
561+ $ this ->sessionHandler ->shouldReceive ('destroy_session_data ' )->once ();
562+
563+ $ this ->capturePayPalPayment ->shouldNotReceive ('create_order ' );
564+
565+ $ result = $ this ->createReturnUrlStubGateway ()->process_payment ($ orderId );
566+
567+ $ this ->assertEquals ('success ' , $ result ['result ' ]);
568+ }
569+
570+ /**
571+ * @runInSeparateProcess
572+ * @preserveGlobalState disabled
573+ * @scenario Current user supplies a token id belonging to a different user while
574+ * changing their subscription payment. The token must not be attached, the
575+ * change must fail, and no $0 capture must be attempted either.
576+ */
577+ public function test_process_payment_rejects_token_owned_by_different_user_during_change_payment (): void {
578+ $ orderId = 1 ;
579+ $ _POST ['woocommerce_change_payment ' ] = '1 ' ;
580+ $ _POST ['wc-ppcp-gateway-payment-token ' ] = '99 ' ;
581+
582+ $ this ->subscriptionHelper ->shouldReceive ('has_subscription ' )->with ($ orderId )->andReturn (true );
583+ $ this ->subscriptionHelper ->shouldReceive ('is_subscription_change_payment ' )->andReturn (true );
584+
585+ $ wcOrder = Mockery::mock (\WC_Order::class);
586+ $ wcOrder ->shouldReceive ('get_id ' )->andReturn ($ orderId );
587+ $ wcOrder ->shouldReceive ('get_meta ' )->andReturn ('' );
588+ when ('wc_get_order ' )->justReturn ($ wcOrder );
589+
590+ $ tokens = Mockery::mock ('alias:WC_Payment_Tokens ' );
591+ $ payment_token = Mockery::mock (\WC_Payment_Token::class);
592+ $ payment_token ->shouldReceive ('get_user_id ' )->andReturn (2 );
593+ $ tokens ->shouldReceive ('get ' )->with (99 )->andReturn ($ payment_token );
594+
595+ when ('get_current_user_id ' )->justReturn (1 );
596+ when ('wc_add_notice ' )->justReturn (null );
597+ when ('wc_get_checkout_url ' )->justReturn ('http://example.test/checkout ' );
598+
599+ $ wcOrder ->shouldNotReceive ('add_payment_token ' );
600+ $ wcOrder ->shouldNotReceive ('save ' );
601+ $ this ->sessionHandler ->shouldNotReceive ('destroy_session_data ' );
602+ $ this ->capturePayPalPayment ->shouldNotReceive ('create_order ' );
603+
604+ $ result = $ this ->createReturnUrlStubGateway ()->process_payment ($ orderId );
605+
606+ $ this ->assertEquals ('failure ' , $ result ['result ' ]);
607+ }
608+ }
609+
610+ /**
611+ * Test double that returns a string return URL. The shared WC_Payment_Gateway
612+ * stub returns the order object, which would violate the string type hint of
613+ * PayPalGateway::add_payment_token_to_order().
614+ */
615+ class PayPalGatewayReturnUrlStub extends PayPalGateway
616+ {
617+ protected function get_return_url ( $ order = null ) {
618+ return 'http://example.test/return ' ;
619+ }
500620}
501621
502622/**
0 commit comments