woocommerce-paypal-payments/tests/PHPUnit/StoreSync/StoreData/StoreCartItemTest.php
2026-05-13 18:45:31 +02:00

456 lines
17 KiB
PHP

<?php
/**
* Tests for StoreCartItem.
*/
declare( strict_types = 1 );
namespace WooCommerce\PayPalCommerce\StoreSync\StoreData;
use Mockery;
use WC_Product;
use WooCommerce\PayPalCommerce\StoreSync\Config\StoreCurrencyValue;
use WooCommerce\PayPalCommerce\StoreSync\Helper\ProductManager;
use WooCommerce\PayPalCommerce\StoreSync\Schema\CartItem;
use WooCommerce\PayPalCommerce\StoreSync\Schema\Money;
use WooCommerce\PayPalCommerce\StoreSync\Validation\StoreValidation;
use WooCommerce\PayPalCommerce\StoreSync\StoreSyncTestCase;
use function Brain\Monkey\Functions\when;
/**
* @covers \WooCommerce\PayPalCommerce\StoreSync\StoreData\StoreCartItem
*/
class StoreCartItemTest extends StoreSyncTestCase {
private ProductManager $product_manager;
public function setUp(): void {
parent::setUp();
when( 'wc_get_price_excluding_tax' )->alias(
function ( $product ) {
return (float) $product->get_price();
}
);
when( 'wp_strip_all_tags' )->returnArg( 1 );
$currency = Mockery::mock( StoreCurrencyValue::class );
$currency->allows( 'value' )->andReturn( 'USD' );
$this->product_manager = new ProductManager( $currency );
}
// -------------------------------------------------------------------------
// Helpers
// -------------------------------------------------------------------------
/**
* Creates a CartItem schema stub with an optional price Money instance.
*
* @param Money|null $price The price to return from price().
* @return CartItem&\Mockery\MockInterface
*/
private function make_cart_item_schema( ?Money $price = null ): CartItem {
$stub = Mockery::mock( CartItem::class );
$stub->allows( 'price' )->andReturn( $price );
return $stub;
}
/**
* Creates a WC_Product stub returning the given price string and optional extra fields.
*
* @param string $price_string The value returned by get_price().
* @param string $name The value returned by get_name().
* @param string $short_description The value returned by get_short_description().
* @param int $parent_id The value returned by get_parent_id().
* @return WC_Product&\Mockery\MockInterface
*/
private function make_product(
string $price_string,
string $name = 'Product Name',
string $short_description = '',
int $parent_id = 0
): WC_Product {
$stub = Mockery::mock( WC_Product::class );
$stub->allows( 'get_price' )->andReturn( $price_string );
$stub->allows( 'get_name' )->andReturn( $name );
$stub->allows( 'get_description' )->andReturn( '' );
$stub->allows( 'get_short_description' )->andReturn( $short_description );
$stub->allows( 'get_parent_id' )->andReturn( $parent_id );
return $stub;
}
/**
* Creates a StoreCurrencyValue stub returning the given currency code.
*
* @param string $currency The currency code to return.
* @return StoreCurrencyValue&\Mockery\MockInterface
*/
private function make_currency( string $currency ): StoreCurrencyValue {
$stub = Mockery::mock( StoreCurrencyValue::class );
$stub->allows( 'value' )->andReturn( $currency );
return $stub;
}
private function make_product_manager(): ProductManager {
return $this->product_manager;
}
/**
* Creates a Money instance with the given value and currency via from_array().
*
* @param string $value Decimal string (e.g., '9.99').
* @param string $currency ISO 4217 code.
* @return Money
*/
private function make_money( string $value, string $currency ): Money {
return Money::from_array(
array( 'currency_code' => $currency, 'value' => $value ),
new StoreValidation()
);
}
// -------------------------------------------------------------------------
// Group 1 — real_price()
// -------------------------------------------------------------------------
/**
* GIVEN a WC_Product returning a price string
* WHEN real_price() is called
* THEN a float representation of the product price is returned
*/
public function test_real_price_returns_float_from_product_get_price(): void {
$schema = $this->make_cart_item_schema();
$product = $this->make_product( '19.99' );
$sut = new StoreCartItem( 0, $schema, $product, $this->make_currency( 'USD' ), $this->make_product_manager() );
$this->assertSame( 19.99, $sut->real_price() );
}
// -------------------------------------------------------------------------
// Group 2 — assumed_price()
// -------------------------------------------------------------------------
/**
* GIVEN a CartItem schema with no price
* WHEN assumed_price() is called
* THEN null is returned
*/
public function test_assumed_price_returns_null_when_schema_has_no_price(): void {
$schema = $this->make_cart_item_schema( null );
$product = $this->make_product( '9.99' );
$sut = new StoreCartItem( 0, $schema, $product, $this->make_currency( 'USD' ), $this->make_product_manager() );
$this->assertNull( $sut->assumed_price_as_money() );
}
/**
* GIVEN a CartItem schema with a Money price
* WHEN assumed_price() is called
* THEN the Money instance from the schema is returned
*/
public function test_assumed_price_returns_money_from_schema(): void {
$money = $this->make_money( '9.99', 'USD' );
$schema = $this->make_cart_item_schema( $money );
$product = $this->make_product( '9.99' );
$sut = new StoreCartItem( 0, $schema, $product, $this->make_currency( 'USD' ), $this->make_product_manager() );
$this->assertSame( $money, $sut->assumed_price_as_money() );
}
// -------------------------------------------------------------------------
// Group 3 — is_price_correct()
// -------------------------------------------------------------------------
/**
* GIVEN a CartItem schema with no assumed price
* WHEN is_price_correct() is called
* THEN true is returned because no price claim was made
*/
public function test_is_price_correct_returns_true_when_no_assumed_price(): void {
$schema = $this->make_cart_item_schema( null );
$product = $this->make_product( '9.99' );
$sut = new StoreCartItem( 0, $schema, $product, $this->make_currency( 'USD' ), $this->make_product_manager() );
$this->assertTrue( $sut->is_price_correct() );
}
/**
* GIVEN a CartItem with an assumed price that matches the store price
* WHEN is_price_correct() is called
* THEN true is returned
*
* @dataProvider matching_price_provider
*
* @param string $product_price The WC_Product get_price() return value.
* @param string $assumed_value The Money value the agent provided.
*/
public function test_is_price_correct_returns_true_when_assumed_price_matches_store_price(
string $product_price,
string $assumed_value
): void {
$money = $this->make_money( $assumed_value, 'USD' );
$schema = $this->make_cart_item_schema( $money );
$product = $this->make_product( $product_price );
$sut = new StoreCartItem( 0, $schema, $product, $this->make_currency( 'USD' ), $this->make_product_manager() );
$this->assertTrue( $sut->is_price_correct() );
}
public function matching_price_provider(): array {
return array(
'exact match two decimals' => array( '9.99', '9.99' ),
'product integer, assumed decimal' => array( '10', '10.00' ),
'product has trailing zeros, assumed clean' => array( '25.50', '25.50' ),
'product float, assumed same float string' => array( '0.99', '0.99' ),
);
}
/**
* GIVEN a CartItem with an assumed price that differs from the store price
* WHEN is_price_correct() is called
* THEN false is returned
*
* @dataProvider mismatched_price_provider
*
* @param string $product_price The WC_Product get_price() return value.
* @param string $assumed_value The Money value the agent provided.
*/
public function test_is_price_correct_returns_false_when_assumed_price_differs_from_store_price(
string $product_price,
string $assumed_value
): void {
$money = $this->make_money( $assumed_value, 'USD' );
$schema = $this->make_cart_item_schema( $money );
$product = $this->make_product( $product_price );
$sut = new StoreCartItem( 0, $schema, $product, $this->make_currency( 'USD' ), $this->make_product_manager() );
$this->assertFalse( $sut->is_price_correct() );
}
public function mismatched_price_provider(): array {
return array(
'price raised by one cent' => array( '10.00', '9.99' ),
'price lowered by one cent' => array( '9.99', '10.00' ),
'significantly different' => array( '49.99', '39.99' ),
'zero vs non-zero' => array( '0.00', '9.99' ),
);
}
/**
* GIVEN a product price that would differ from an assumed price due to floating-point
* representation (e.g., 0.1 + 0.2 != 0.3 as floats)
* WHEN is_price_correct() is called
* THEN the comparison uses formatted decimals, avoiding floating-point drift
*/
public function test_is_price_correct_avoids_floating_point_drift(): void {
// 0.1 + 0.2 as a float would be 0.30000000000000004; the store returns '0.30'
$money = $this->make_money( '0.30', 'USD' );
$schema = $this->make_cart_item_schema( $money );
$product = $this->make_product( '0.30' );
$sut = new StoreCartItem( 0, $schema, $product, $this->make_currency( 'USD' ), $this->make_product_manager() );
$this->assertTrue( $sut->is_price_correct() );
}
// -------------------------------------------------------------------------
// Group 4 — is_currency_correct()
// -------------------------------------------------------------------------
/**
* GIVEN a CartItem schema with no assumed price (no currency claim)
* WHEN is_currency_correct() is called
* THEN true is returned
*/
public function test_is_currency_correct_returns_true_when_no_assumed_price(): void {
$schema = $this->make_cart_item_schema( null );
$product = $this->make_product( '9.99' );
$sut = new StoreCartItem( 0, $schema, $product, $this->make_currency( 'USD' ), $this->make_product_manager() );
$this->assertTrue( $sut->is_currency_correct() );
}
/**
* GIVEN a CartItem with an assumed price in the same currency as the store
* WHEN is_currency_correct() is called
* THEN true is returned
*/
public function test_is_currency_correct_returns_true_when_assumed_currency_matches_store(): void {
$money = $this->make_money( '9.99', 'EUR' );
$schema = $this->make_cart_item_schema( $money );
$product = $this->make_product( '9.99' );
$sut = new StoreCartItem( 0, $schema, $product, $this->make_currency( 'EUR' ), $this->make_product_manager() );
$this->assertTrue( $sut->is_currency_correct() );
}
/**
* GIVEN a CartItem with an assumed price in a different currency than the store
* WHEN is_currency_correct() is called
* THEN false is returned
*/
public function test_is_currency_correct_returns_false_when_assumed_currency_differs_from_store(): void {
$money = $this->make_money( '9.99', 'USD' );
$schema = $this->make_cart_item_schema( $money );
$product = $this->make_product( '9.99' );
$sut = new StoreCartItem( 0, $schema, $product, $this->make_currency( 'EUR' ), $this->make_product_manager() );
$this->assertFalse( $sut->is_currency_correct() );
}
// -------------------------------------------------------------------------
// Group 5 — schema()
// -------------------------------------------------------------------------
/**
* GIVEN a StoreCartItem constructed with a CartItem schema
* WHEN schema() is called
* THEN the exact same CartItem instance is returned
*/
public function test_schema_returns_injected_cart_item(): void {
$schema = $this->make_cart_item_schema();
$product = $this->make_product( '9.99' );
$sut = new StoreCartItem( 0, $schema, $product, $this->make_currency( 'USD' ), $this->make_product_manager() );
$this->assertSame( $schema, $sut->paypal_item() );
}
// -------------------------------------------------------------------------
// Group 6 — to_array()
// -------------------------------------------------------------------------
/**
* GIVEN a StoreCartItem with a real_price that differs from the schema price
* WHEN to_array() is called
* THEN the price in the result reflects the real store price, not the schema price
*/
public function test_to_array_price_uses_real_store_price_not_schema_price(): void {
$schema = CartItem::from_array(
array(
'quantity' => 2,
'item_id' => 'sku-1',
'price' => array( 'currency_code' => 'USD', 'value' => '5.00' ),
),
new StoreValidation()
);
$product = $this->make_product( '12.50' );
$sut = new StoreCartItem( 0, $schema, $product, $this->make_currency( 'USD' ), $this->make_product_manager() );
$result = $sut->to_array();
$this->assertSame( '12.50', $result['price']['value'] );
$this->assertSame( 'USD', $result['price']['currency_code'] );
}
/**
* GIVEN a StoreCartItem whose product has a name
* WHEN to_array() is called
* THEN the name in the result comes from WC_Product::get_name(), not the schema input
*/
public function test_to_array_name_comes_from_product_not_schema(): void {
$schema = CartItem::from_array(
array( 'quantity' => 1, 'name' => 'Schema Name' ),
new StoreValidation()
);
$product = $this->make_product( '9.99', 'Real Product Name' );
$sut = new StoreCartItem( 0, $schema, $product, $this->make_currency( 'USD' ), $this->make_product_manager() );
$result = $sut->to_array();
$this->assertSame( 'Real Product Name', $result['name'] );
}
/**
* GIVEN a StoreCartItem whose product has a non-empty short description
* WHEN to_array() is called
* THEN description is present and contains the product short description
*/
public function test_to_array_description_present_when_product_has_short_description(): void {
$schema = CartItem::from_array( array( 'quantity' => 1 ), new StoreValidation() );
$product = $this->make_product( '9.99', 'Product', 'A fine item.' );
$sut = new StoreCartItem( 0, $schema, $product, $this->make_currency( 'USD' ), $this->make_product_manager() );
$result = $sut->to_array();
$this->assertArrayHasKey( 'description', $result );
$this->assertSame( 'A fine item.', $result['description'] );
}
/**
* GIVEN a StoreCartItem whose product has an empty short description
* WHEN to_array() is called
* THEN description is absent from the result
*/
public function test_to_array_description_absent_when_product_has_no_short_description(): void {
$schema = CartItem::from_array( array( 'quantity' => 1 ), new StoreValidation() );
$product = $this->make_product( '9.99', 'Product', '' );
$sut = new StoreCartItem( 0, $schema, $product, $this->make_currency( 'USD' ), $this->make_product_manager() );
$result = $sut->to_array();
$this->assertArrayNotHasKey( 'description', $result );
}
/**
* GIVEN a StoreCartItem whose product has a non-zero parent_id (i.e., a variation)
* WHEN to_array() is called
* THEN parent_id is present in the result as a string
*/
public function test_to_array_parent_id_present_as_string_when_product_has_parent(): void {
$schema = CartItem::from_array( array( 'quantity' => 1 ), new StoreValidation() );
$product = $this->make_product( '9.99', 'Product', '', 42 );
$sut = new StoreCartItem( 0, $schema, $product, $this->make_currency( 'USD' ), $this->make_product_manager() );
$result = $sut->to_array();
$this->assertArrayHasKey( 'parent_id', $result );
$this->assertSame( '42', $result['parent_id'] );
}
/**
* GIVEN a StoreCartItem whose product has a parent_id of zero (i.e., a top-level product)
* WHEN to_array() is called
* THEN parent_id is absent from the result
*/
public function test_to_array_parent_id_absent_when_product_has_no_parent(): void {
$schema = CartItem::from_array( array( 'quantity' => 1 ), new StoreValidation() );
$product = $this->make_product( '9.99', 'Product', '', 0 );
$sut = new StoreCartItem( 0, $schema, $product, $this->make_currency( 'USD' ), $this->make_product_manager() );
$result = $sut->to_array();
$this->assertArrayNotHasKey( 'parent_id', $result );
}
/**
* GIVEN a StoreCartItem with schema fields like quantity, item_id, variant_id, and
* selected_attributes
* WHEN to_array() is called
* THEN those fields are preserved from the CartItem schema in the result
*/
public function test_to_array_preserves_schema_fields_quantity_item_id_variant_id_selected_attributes(): void {
$schema = CartItem::from_array(
array(
'quantity' => 3,
'item_id' => 'sku-99',
'variant_id' => 'var-5',
'selected_attributes' => array(
array( 'name' => 'Size', 'value' => 'L' ),
),
),
new StoreValidation()
);
$product = $this->make_product( '20.00', 'T-Shirt' );
$sut = new StoreCartItem( 0, $schema, $product, $this->make_currency( 'EUR' ), $this->make_product_manager() );
$result = $sut->to_array();
$this->assertSame( 3, $result['quantity'] );
$this->assertSame( 'sku-99', $result['item_id'] );
$this->assertSame( 'var-5', $result['variant_id'] );
$this->assertSame( 'Size', $result['selected_attributes'][0]['name'] );
$this->assertSame( 'L', $result['selected_attributes'][0]['value'] );
$this->assertSame( 'EUR', $result['price']['currency_code'] );
}
}