woocommerce-paypal-payments/tests/PHPUnit/StoreSync/Endpoint/ReplaceCartEndpointTest.php
2026-05-27 15:50:15 +02:00

611 lines
23 KiB
PHP

<?php
declare( strict_types = 1 );
namespace WooCommerce\PayPalCommerce\StoreSync\Endpoint;
use Mockery;
use WP_REST_Request;
use WooCommerce\PayPalCommerce\StoreSync\Response\CartResponse;
use WooCommerce\PayPalCommerce\StoreSync\Schema\PayPalCart;
use WooCommerce\PayPalCommerce\StoreSync\Validation\StoreValidation;
/**
* @covers \WooCommerce\PayPalCommerce\StoreSync\Endpoint\ReplaceCartEndpoint
*/
class ReplaceCartEndpointTest extends AgenticEndpointTestCase {
private function make_sut( array $mocks ): ReplaceCartEndpoint {
return new ReplaceCartEndpoint(
$mocks['auth_provider'],
$mocks['session_handler'],
$mocks['session_manager'],
$mocks['response_factory'],
$mocks['validation_processor'],
$mocks['logger'],
$mocks['order_manager'],
$mocks['store_data']
);
}
public function test_replace_cart_returns_200_ok_on_successful_replacement(): void {
$cart_id = 't_mock_cart_id_12345';
$ec_token = 'EC-12345TOKEN';
$created_time = time();
$existing_cart_data = $this->cart()
->with_item_fixture( 'old_item' )
->to_array();
$replacement_cart_data = $this->cart()
->with_item_fixture( 'new_item_2' )
->with_item_fixture( 'new_item_3' )
->to_array();
$mocks = $this->create_mocks();
$session_handler = $mocks['session_handler'];
$response_factory = $mocks['response_factory'];
$existing_cart = PayPalCart::from_array( $existing_cart_data, new StoreValidation() );
// Mock session handler.
$session_handler->allows( 'load_cart_session' )
->with( $cart_id )
->andReturn(
array(
'cart' => $existing_cart,
'ec_token' => $ec_token,
'created' => $created_time,
)
);
$session_handler->allows( 'update_cart_session' )
->andReturn( true );
// Mock response factory.
$response_factory->allows( 'from_cart' )
->andReturnUsing( fn( $cart ) => CartResponse::create( $cart, '' ) );
// Mock order_manager behavior
$order_manager = $mocks['order_manager'];
$order_manager->allows( 'update_order' )->andReturn( true );
// Mock validation_processor to return valid cart
$validation_processor = $mocks['validation_processor'];
$validation_processor->allows( 'validate_cart' )->andReturnUsing( fn( $cart ) => $cart );
$endpoint = $this->make_sut( $mocks );
$request = new WP_REST_Request( 'PUT', "/wp-json/paypal/v1/merchant-cart/{$cart_id}" );
$request->set_param( 'cart_id', $cart_id );
$request->set_body( json_encode( $replacement_cart_data ) );
$response = $endpoint->replace_cart( $request );
$data = $response->get_data();
$this->assertIsArray( $data );
$this->assertArrayHasKey( 'status', $data );
$this->assertSame( 200, $response->get_status() );
}
public function test_replace_cart_returns_error_when_cart_not_found(): void {
$cart_id = 't_nonexistent_cart_id';
$replacement_data = $this->cart()
->with_item( 'TEST-001', 2 )
->to_array();
$mocks = $this->create_mocks();
$session_handler = $mocks['session_handler'];
// Mock session handler - return null for non-existent cart.
$session_handler->allows( 'load_cart_session' )
->with( $cart_id )
->andReturn( null );
$endpoint = $this->make_sut( $mocks );
$request = new WP_REST_Request( 'PUT', "/wp-json/paypal/v1/merchant-cart/{$cart_id}" );
$request->set_param( 'cart_id', $cart_id );
$request->set_body( json_encode( $replacement_data ) );
$response = $endpoint->replace_cart( $request );
$data = $response->get_data();
$this->assertIsArray( $data );
$this->assertNotSame( 200, $response->get_status() );
$this->assert_error_response( $data );
}
public function test_replace_cart_completely_replaces_items(): void {
$cart_id = 't_mock_cart_id_12345';
$ec_token = 'EC-12345TOKEN';
$created_time = time();
$existing_cart_data = $this->cart()
->with_item_fixture( 'old_item' )
->with_shipping_fixture( 'us_old' )
->to_array();
$replacement_cart_data = $this->cart()
->with_item_fixture( 'eur_item' )
->with_shipping_fixture( 'de_new' )
->to_array();
$mocks = $this->create_mocks();
$session_handler = $mocks['session_handler'];
$response_factory = $mocks['response_factory'];
$existing_cart = PayPalCart::from_array( $existing_cart_data, new StoreValidation() );
// Mock load existing cart.
$session_handler->allows( 'load_cart_session' )
->with( $cart_id )
->andReturn(
array(
'cart' => $existing_cart,
'ec_token' => $ec_token,
'created' => $created_time,
)
);
// Verify that update receives completely new data (full replacement).
$session_handler->shouldReceive( 'update_cart_session' )
->once()
->withArgs( function ( $received_cart_id, $new_cart ) use ( $cart_id ) {
if ( $received_cart_id !== $cart_id || ! ( $new_cart instanceof PayPalCart ) ) {
return false;
}
$cart_array = $new_cart->to_array();
// Should have NEW item only (not old).
if ( count( $cart_array['items'] ) !== 1 ) {
return false;
}
if ( $cart_array['items'][0]['item_id'] !== 'NEW-ITEM-002' ) {
return false;
}
// Should have NEW shipping info.
if ( ( $cart_array['shipping_address']['address_line_1'] ?? '' ) !== '456 New Ave' ) {
return false;
}
return true;
} )
->andReturn( true );
// Mock response factory.
$response_factory->allows( 'from_cart' )
->andReturnUsing( fn( $cart ) => CartResponse::create( $cart, '' ) );
// Mock order_manager behavior
$order_manager = $mocks['order_manager'];
$order_manager->allows( 'update_order' )->andReturn( true );
// Mock validation_processor to return valid cart
$validation_processor = $mocks['validation_processor'];
$validation_processor->allows( 'validate_cart' )->andReturnUsing( fn( $cart ) => $cart );
$endpoint = $this->make_sut( $mocks );
$request = new WP_REST_Request( 'PUT', "/wp-json/paypal/v1/merchant-cart/{$cart_id}" );
$request->set_param( 'cart_id', $cart_id );
$request->set_body( json_encode( $replacement_cart_data ) );
$response = $endpoint->replace_cart( $request );
$this->assertSame( 200, $response->get_status() );
}
public function test_replace_cart_returns_error_when_replacement_fails(): void {
$cart_id = 't_mock_cart_id_12345';
$ec_token = 'EC-12345TOKEN';
$created_time = time();
$existing_cart_data = $this->cart()
->with_item_fixture( 'old_item' )
->to_array();
$replacement_data = $this->cart()
->with_item_fixture( 'new_item_2' )
->to_array();
$mocks = $this->create_mocks();
$session_handler = $mocks['session_handler'];
$response_factory = $mocks['response_factory'];
$existing_cart = PayPalCart::from_array( $existing_cart_data, new StoreValidation() );
// Mock load existing cart.
$session_handler->allows( 'load_cart_session' )
->with( $cart_id )
->andReturn(
array(
'cart' => $existing_cart,
'ec_token' => $ec_token,
'created' => $created_time,
)
);
// Mock replacement operation fails.
$session_handler->allows( 'update_cart_session' )
->andReturn( false );
// Mock order_manager behavior - should succeed since we're testing session update failure
$order_manager = $mocks['order_manager'];
$order_manager->allows( 'update_order' )->andReturn( true );
// Mock validation_processor to return valid cart
$validation_processor = $mocks['validation_processor'];
$validation_processor->allows( 'validate_cart' )->andReturnUsing( fn( $cart ) => $cart );
$endpoint = $this->make_sut( $mocks );
$request = new WP_REST_Request( 'PUT', "/wp-json/paypal/v1/merchant-cart/{$cart_id}" );
$request->set_param( 'cart_id', $cart_id );
$request->set_body( json_encode( $replacement_data ) );
$response = $endpoint->replace_cart( $request );
$data = $response->get_data();
$this->assertIsArray( $data );
$this->assertNotSame( 200, $response->get_status() );
$this->assert_error_response( $data );
}
/**
* GIVEN a cart session that has no ec_token (POST order creation had previously failed)
* WHEN a PUT /merchant-cart/{id} request is made with a valid, issue-free cart
* THEN create_order() is called to recover the missing PayPal order token
* AND the response is a 200 with the new token injected into the payment method data
*/
public function test_creates_paypal_order_when_session_has_no_token_and_cart_is_valid(): void {
$cart_id = 't_mock_cart_id_recover';
$created_time = time();
$new_token = 'EC-NEWTOKEN-001';
$cart_data = $this->cart()->with_item_fixture( 'new_item_2' )->to_array();
$mocks = $this->create_mocks();
$session_handler = $mocks['session_handler'];
$order_manager = $mocks['order_manager'];
$existing_cart = PayPalCart::from_array( $cart_data, new StoreValidation() );
// Session has no ec_token — the previous POST failed to create a PayPal order.
$session_handler->allows( 'load_cart_session' )
->with( $cart_id )
->andReturn(
array(
'cart' => $existing_cart,
'ec_token' => '',
'created' => $created_time,
)
);
$session_handler->allows( 'update_cart_session' )->andReturn( true );
// The cart has no validation issues, so create_order MUST be called once.
$order_manager->shouldReceive( 'create_order' )
->once()
->andReturn( $new_token );
$mocks['validation_processor']->allows( 'validate_cart' )->andReturnUsing( fn( $c ) => $c );
$mocks['response_factory']->allows( 'from_cart' )
->andReturnUsing( fn( $cart, $id ) => \WooCommerce\PayPalCommerce\StoreSync\Response\CartResponse::create( $cart, $id ) );
// Build a StorePayPalCart mock that captures set_paypal_order.
$store_cart_mock = Mockery::mock( \WooCommerce\PayPalCommerce\StoreSync\StoreData\StorePayPalCart::class );
$store_cart_mock->allows( 'paypal_cart' )->andReturn( $existing_cart );
$store_cart_mock->allows( 'validation' )->andReturn( new StoreValidation() );
$store_cart_mock->allows( 'get_validation_issues' )->andReturn( array() );
$store_cart_mock->allows( 'get_items' )->andReturn( array() );
$store_cart_mock->allows( 'get_customer' )->andReturn( array() );
$store_cart_mock->allows( 'get_shipping_address' )->andReturn( array() );
$store_cart_mock->allows( 'get_billing_address' )->andReturn( null );
$store_cart_mock->allows( 'get_totals' )->andReturn( null );
$store_cart_mock->allows( 'get_payment_method' )->andReturn( array( 'type' => 'paypal', 'token' => $new_token ) );
$store_cart_mock->allows( 'set_paypal_order' );
$store_data_mock = Mockery::mock( \WooCommerce\PayPalCommerce\StoreSync\StoreData\StoreData::class );
$store_data_mock->allows( 'create_cart' )->andReturn( $store_cart_mock );
$mocks['store_data'] = $store_data_mock;
$endpoint = $this->make_sut( $mocks );
$request = new WP_REST_Request( 'PUT', "/wp-json/paypal/v1/merchant-cart/{$cart_id}" );
$request->set_param( 'cart_id', $cart_id );
$request->set_body( json_encode( $cart_data ) );
$response = $endpoint->replace_cart( $request );
$this->assertSame( 200, $response->get_status() );
}
/**
* GIVEN a cart session that already has a valid ec_token from a previous successful POST
* WHEN a PUT /merchant-cart/{id} request arrives
* THEN create_order() is NOT called (the existing token must be preserved)
* AND set_paypal_order() is NOT called on the cart (response must not mention existing tokens)
*/
public function test_does_not_create_paypal_order_when_session_already_has_token(): void {
$cart_id = 't_mock_cart_id_has_token';
$existing_token = 'EC-EXISTING-TOKEN';
$created_time = time();
$cart_data = $this->cart()->with_item_fixture( 'new_item_2' )->to_array();
$mocks = $this->create_mocks();
$session_handler = $mocks['session_handler'];
$order_manager = $mocks['order_manager'];
$existing_cart = PayPalCart::from_array( $cart_data, new StoreValidation() );
// Session already holds a valid token.
$session_handler->allows( 'load_cart_session' )
->with( $cart_id )
->andReturn(
array(
'cart' => $existing_cart,
'ec_token' => $existing_token,
'created' => $created_time,
)
);
$session_handler->allows( 'update_cart_session' )->andReturn( true );
// create_order must never be called when the session already has a token.
$order_manager->shouldNotReceive( 'create_order' );
$mocks['validation_processor']->allows( 'validate_cart' )->andReturnUsing( fn( $c ) => $c );
$mocks['response_factory']->allows( 'from_cart' )
->andReturnUsing( fn( $cart, $id ) => \WooCommerce\PayPalCommerce\StoreSync\Response\CartResponse::create( $cart, $id ) );
$store_cart_mock = Mockery::mock( \WooCommerce\PayPalCommerce\StoreSync\StoreData\StorePayPalCart::class );
$store_cart_mock->allows( 'paypal_cart' )->andReturn( $existing_cart );
$store_cart_mock->allows( 'validation' )->andReturn( new StoreValidation() );
$store_cart_mock->allows( 'get_validation_issues' )->andReturn( array() );
$store_cart_mock->allows( 'get_items' )->andReturn( array() );
$store_cart_mock->allows( 'get_customer' )->andReturn( array() );
$store_cart_mock->allows( 'get_shipping_address' )->andReturn( array() );
$store_cart_mock->allows( 'get_billing_address' )->andReturn( null );
$store_cart_mock->allows( 'get_totals' )->andReturn( null );
$store_cart_mock->allows( 'get_payment_method' )->andReturn( array( 'type' => 'paypal' ) );
$store_cart_mock->allows( 'set_paypal_order' );
$store_data_mock = Mockery::mock( \WooCommerce\PayPalCommerce\StoreSync\StoreData\StoreData::class );
$store_data_mock->allows( 'create_cart' )->andReturn( $store_cart_mock );
$mocks['store_data'] = $store_data_mock;
$endpoint = $this->make_sut( $mocks );
$request = new WP_REST_Request( 'PUT', "/wp-json/paypal/v1/merchant-cart/{$cart_id}" );
$request->set_param( 'cart_id', $cart_id );
$request->set_body( json_encode( $cart_data ) );
$response = $endpoint->replace_cart( $request );
$this->assertSame( 200, $response->get_status() );
}
/**
* GIVEN a cart session with no ec_token
* WHEN a PUT request arrives but the cart has validation issues (e.g. out-of-stock item)
* THEN create_order() is NOT called (a broken cart must not create a PayPal order)
* AND the response is still 200 (validation issues are surfaced in the payload, not as HTTP errors)
*/
public function test_does_not_create_paypal_order_when_cart_has_validation_issues(): void {
$cart_id = 't_mock_cart_id_invalid';
$created_time = time();
$cart_data = $this->cart()->with_item_fixture( 'new_item_2' )->to_array();
$mocks = $this->create_mocks();
$session_handler = $mocks['session_handler'];
$order_manager = $mocks['order_manager'];
$existing_cart = PayPalCart::from_array( $cart_data, new StoreValidation() );
// Session has no ec_token.
$session_handler->allows( 'load_cart_session' )
->with( $cart_id )
->andReturn(
array(
'cart' => $existing_cart,
'ec_token' => '',
'created' => $created_time,
)
);
$session_handler->allows( 'update_cart_session' )->andReturn( true );
// Cart has a validation issue — create_order must never be called.
$order_manager->shouldNotReceive( 'create_order' );
$mocks['validation_processor']->allows( 'validate_cart' )->andReturnUsing( fn( $c ) => $c );
$mocks['response_factory']->allows( 'from_cart' )
->andReturnUsing( fn( $cart, $id ) => \WooCommerce\PayPalCommerce\StoreSync\Response\CartResponse::create( $cart, $id ) );
// Build a validation with at least one issue.
$validation_with_issue = new StoreValidation();
$validation_with_issue->add_invalid_product( 'Item is out of stock' );
$store_cart_mock = Mockery::mock( \WooCommerce\PayPalCommerce\StoreSync\StoreData\StorePayPalCart::class );
$store_cart_mock->allows( 'paypal_cart' )->andReturn( $existing_cart );
$store_cart_mock->allows( 'validation' )->andReturn( $validation_with_issue );
$store_cart_mock->allows( 'get_validation_issues' )->andReturn( $validation_with_issue->all() );
$store_cart_mock->allows( 'get_items' )->andReturn( array() );
$store_cart_mock->allows( 'get_customer' )->andReturn( array() );
$store_cart_mock->allows( 'get_shipping_address' )->andReturn( array() );
$store_cart_mock->allows( 'get_billing_address' )->andReturn( null );
$store_cart_mock->allows( 'get_totals' )->andReturn( null );
$store_cart_mock->allows( 'get_payment_method' )->andReturn( array( 'type' => 'paypal' ) );
$store_cart_mock->allows( 'set_paypal_order' );
$store_data_mock = Mockery::mock( \WooCommerce\PayPalCommerce\StoreSync\StoreData\StoreData::class );
$store_data_mock->allows( 'create_cart' )->andReturn( $store_cart_mock );
$mocks['store_data'] = $store_data_mock;
$endpoint = $this->make_sut( $mocks );
$request = new WP_REST_Request( 'PUT', "/wp-json/paypal/v1/merchant-cart/{$cart_id}" );
$request->set_param( 'cart_id', $cart_id );
$request->set_body( json_encode( $cart_data ) );
$response = $endpoint->replace_cart( $request );
$this->assertSame( 200, $response->get_status() );
}
/**
* GIVEN a cart session with no ec_token and a valid cart
* WHEN create_order() succeeds and returns a new token
* THEN update_cart_session() is called with that new token as the third argument
* AND the new token is persisted in the session for future requests
*/
public function test_new_token_is_stored_in_session_when_created(): void {
$cart_id = 't_mock_cart_id_persist';
$created_time = time();
$new_token = 'EC-STORED-TOKEN-999';
$cart_data = $this->cart()->with_item_fixture( 'new_item_2' )->to_array();
$mocks = $this->create_mocks();
$session_handler = $mocks['session_handler'];
$order_manager = $mocks['order_manager'];
$existing_cart = PayPalCart::from_array( $cart_data, new StoreValidation() );
$session_handler->allows( 'load_cart_session' )
->with( $cart_id )
->andReturn(
array(
'cart' => $existing_cart,
'ec_token' => '',
'created' => $created_time,
)
);
// The new token must be forwarded to update_cart_session as the third argument.
$session_handler->shouldReceive( 'update_cart_session' )
->once()
->withArgs(
function ( $received_id, $received_cart, $received_token ) use ( $cart_id, $new_token ) {
return $received_id === $cart_id
&& $received_cart instanceof PayPalCart
&& $received_token === $new_token;
}
)
->andReturn( true );
$order_manager->allows( 'create_order' )->andReturn( $new_token );
$mocks['validation_processor']->allows( 'validate_cart' )->andReturnUsing( fn( $c ) => $c );
$mocks['response_factory']->allows( 'from_cart' )
->andReturnUsing( fn( $cart, $id ) => \WooCommerce\PayPalCommerce\StoreSync\Response\CartResponse::create( $cart, $id ) );
$store_cart_mock = Mockery::mock( \WooCommerce\PayPalCommerce\StoreSync\StoreData\StorePayPalCart::class );
$store_cart_mock->allows( 'paypal_cart' )->andReturn( $existing_cart );
$store_cart_mock->allows( 'validation' )->andReturn( new StoreValidation() );
$store_cart_mock->allows( 'get_validation_issues' )->andReturn( array() );
$store_cart_mock->allows( 'get_items' )->andReturn( array() );
$store_cart_mock->allows( 'get_customer' )->andReturn( array() );
$store_cart_mock->allows( 'get_shipping_address' )->andReturn( array() );
$store_cart_mock->allows( 'get_billing_address' )->andReturn( null );
$store_cart_mock->allows( 'get_totals' )->andReturn( null );
$store_cart_mock->allows( 'get_payment_method' )->andReturn( array( 'type' => 'paypal', 'token' => $new_token ) );
$store_cart_mock->allows( 'set_paypal_order' );
$store_data_mock = Mockery::mock( \WooCommerce\PayPalCommerce\StoreSync\StoreData\StoreData::class );
$store_data_mock->allows( 'create_cart' )->andReturn( $store_cart_mock );
$mocks['store_data'] = $store_data_mock;
$endpoint = $this->make_sut( $mocks );
$request = new WP_REST_Request( 'PUT', "/wp-json/paypal/v1/merchant-cart/{$cart_id}" );
$request->set_param( 'cart_id', $cart_id );
$request->set_body( json_encode( $cart_data ) );
$response = $endpoint->replace_cart( $request );
$this->assertSame( 200, $response->get_status() );
}
/**
* GIVEN a cart session with no ec_token and a valid cart
* WHEN create_order() fails and returns an empty/falsy value
* THEN set_paypal_order() is NOT called on the cart (no broken token in response)
* AND the session update proceeds without a token (null passed as third argument)
* AND the response is still 200
*/
public function test_no_token_set_on_cart_when_order_creation_fails(): void {
$cart_id = 't_mock_cart_id_fail_order';
$created_time = time();
$cart_data = $this->cart()->with_item_fixture( 'new_item_2' )->to_array();
$mocks = $this->create_mocks();
$session_handler = $mocks['session_handler'];
$order_manager = $mocks['order_manager'];
$existing_cart = PayPalCart::from_array( $cart_data, new StoreValidation() );
$session_handler->allows( 'load_cart_session' )
->with( $cart_id )
->andReturn(
array(
'cart' => $existing_cart,
'ec_token' => '',
'created' => $created_time,
)
);
// update_cart_session must receive null as token when order creation failed.
$session_handler->shouldReceive( 'update_cart_session' )
->once()
->withArgs(
function ( $received_id, $received_cart, $received_token ) use ( $cart_id ) {
return $received_id === $cart_id
&& $received_cart instanceof PayPalCart
&& $received_token === null;
}
)
->andReturn( true );
// create_order returns falsy — simulates API failure.
$order_manager->allows( 'create_order' )->andReturn( '' );
$mocks['validation_processor']->allows( 'validate_cart' )->andReturnUsing( fn( $c ) => $c );
$mocks['response_factory']->allows( 'from_cart' )
->andReturnUsing( fn( $cart, $id ) => \WooCommerce\PayPalCommerce\StoreSync\Response\CartResponse::create( $cart, $id ) );
$store_cart_mock = Mockery::mock( \WooCommerce\PayPalCommerce\StoreSync\StoreData\StorePayPalCart::class );
$store_cart_mock->allows( 'paypal_cart' )->andReturn( $existing_cart );
$store_cart_mock->allows( 'validation' )->andReturn( new StoreValidation() );
$store_cart_mock->allows( 'get_validation_issues' )->andReturn( array() );
$store_cart_mock->allows( 'get_items' )->andReturn( array() );
$store_cart_mock->allows( 'get_customer' )->andReturn( array() );
$store_cart_mock->allows( 'get_shipping_address' )->andReturn( array() );
$store_cart_mock->allows( 'get_billing_address' )->andReturn( null );
$store_cart_mock->allows( 'get_totals' )->andReturn( null );
$store_cart_mock->allows( 'get_payment_method' )->andReturn( array( 'type' => 'paypal' ) );
$store_cart_mock->allows( 'set_paypal_order' );
$store_data_mock = Mockery::mock( \WooCommerce\PayPalCommerce\StoreSync\StoreData\StoreData::class );
$store_data_mock->allows( 'create_cart' )->andReturn( $store_cart_mock );
$mocks['store_data'] = $store_data_mock;
$endpoint = $this->make_sut( $mocks );
$request = new WP_REST_Request( 'PUT', "/wp-json/paypal/v1/merchant-cart/{$cart_id}" );
$request->set_param( 'cart_id', $cart_id );
$request->set_body( json_encode( $cart_data ) );
$response = $endpoint->replace_cart( $request );
$this->assertSame( 200, $response->get_status() );
}
}