mirror of
https://github.com/woocommerce/woocommerce-paypal-payments.git
synced 2026-07-30 02:17:46 +08:00
405 lines
13 KiB
PHP
405 lines
13 KiB
PHP
<?php
|
|
|
|
declare( strict_types = 1 );
|
|
|
|
namespace WooCommerce\PayPalCommerce\StoreSync\Helper;
|
|
|
|
use Brain\Monkey;
|
|
use Mockery;
|
|
use WC_Cart;
|
|
use WooCommerce\PayPalCommerce\StoreSync\Config\StoreCurrencyValue;
|
|
use WooCommerce\PayPalCommerce\StoreSync\StoreSyncTestCase;
|
|
|
|
/**
|
|
* @covers \WooCommerce\PayPalCommerce\StoreSync\Helper\ShippingOptionsBuilder
|
|
*/
|
|
class ShippingOptionsBuilderTest extends StoreSyncTestCase {
|
|
|
|
private ShippingOptionsBuilder $sut;
|
|
|
|
public function setUp(): void {
|
|
parent::setUp();
|
|
$store_currency = Mockery::mock( StoreCurrencyValue::class );
|
|
$store_currency->allows( 'value' )->andReturn( 'USD' );
|
|
$this->sut = new ShippingOptionsBuilder( $store_currency );
|
|
}
|
|
|
|
// -------------------------------------------------------------------------
|
|
// Helpers
|
|
// -------------------------------------------------------------------------
|
|
|
|
/**
|
|
* Creates a WC shipping rate stub with the given id, label, and cost.
|
|
*
|
|
* @return \WC_Shipping_Rate|Mockery\MockInterface
|
|
*/
|
|
private function create_shipping_rate_stub( string $id, string $label, float $cost ): object {
|
|
$rate = Mockery::mock( 'WC_Shipping_Rate' );
|
|
$rate->allows( 'get_id' )->andReturn( $id );
|
|
$rate->allows( 'get_label' )->andReturn( $label );
|
|
$rate->allows( 'get_cost' )->andReturn( $cost );
|
|
|
|
return $rate;
|
|
}
|
|
|
|
/**
|
|
* Wires up the WC() global stub, returning a fully-configured mock.
|
|
*
|
|
* @param array $packages Value returned by WC()->shipping()->get_packages().
|
|
* @param array|null $chosen Value returned by WC()->session->get('chosen_shipping_methods'),
|
|
* or null to simulate no session entry.
|
|
*/
|
|
private function stub_wc( array $packages, ?array $chosen ): void {
|
|
$shipping_mock = Mockery::mock( 'WC_Shipping' );
|
|
$shipping_mock->allows( 'get_packages' )->andReturn( $packages );
|
|
|
|
$session_mock = Mockery::mock( 'WC_Session' );
|
|
$session_mock->allows( 'get' )
|
|
->with( 'chosen_shipping_methods' )
|
|
->andReturn( $chosen );
|
|
|
|
$wc_mock = Mockery::mock( 'WooCommerce' );
|
|
$wc_mock->allows( 'shipping' )->andReturn( $shipping_mock );
|
|
$wc_mock->session = $session_mock;
|
|
|
|
Monkey\Functions\when( 'WC' )->justReturn( $wc_mock );
|
|
}
|
|
|
|
/**
|
|
* Builds one WC package array with the given rates.
|
|
*
|
|
* @param array $rates Keyed by rate id.
|
|
*/
|
|
private function make_package( array $rates ): array {
|
|
return array( 'rates' => $rates );
|
|
}
|
|
|
|
// -------------------------------------------------------------------------
|
|
// Rule 1 — null cart
|
|
// -------------------------------------------------------------------------
|
|
|
|
/**
|
|
* @scenario Cart is null
|
|
*
|
|
* Given no active WooCommerce cart exists
|
|
* When build() is called with null
|
|
* Then an empty array is returned without consulting WC() at all
|
|
*/
|
|
public function test_build_returns_empty_array_when_cart_is_null(): void {
|
|
$builder = $this->sut;
|
|
|
|
$result = $builder->build( null );
|
|
|
|
$this->assertSame( [], $result );
|
|
}
|
|
|
|
// -------------------------------------------------------------------------
|
|
// Rule 2 — empty packages
|
|
// -------------------------------------------------------------------------
|
|
|
|
/**
|
|
* @scenario Shipping packages are empty
|
|
*
|
|
* Given a valid WC cart is present
|
|
* And WC()->shipping()->get_packages() returns an empty array
|
|
* When build() is called
|
|
* Then an empty array is returned
|
|
*/
|
|
public function test_build_returns_empty_array_when_no_shipping_packages(): void {
|
|
$this->stub_wc( [], [ 'flat_rate:1' ] );
|
|
|
|
$wc_cart = Mockery::mock( WC_Cart::class );
|
|
$builder = $this->sut;
|
|
|
|
$result = $builder->build( $wc_cart );
|
|
|
|
$this->assertSame( [], $result );
|
|
}
|
|
|
|
// -------------------------------------------------------------------------
|
|
// Rule 3 — one package, one rate
|
|
// -------------------------------------------------------------------------
|
|
|
|
/**
|
|
* @scenario Single package with a single shipping rate
|
|
*
|
|
* Given a cart with one shipping package containing one rate
|
|
* And that rate is selected via the session
|
|
* When build() is called
|
|
* Then exactly one shipping option is returned
|
|
* And it has the correct id, name, price, and isSelected=true
|
|
*/
|
|
public function test_build_returns_single_option_for_one_package_one_rate(): void {
|
|
$rate = $this->create_shipping_rate_stub( 'flat_rate:1', 'Flat Rate', 5.0 );
|
|
$packages = array( $this->make_package( array( 'flat_rate:1' => $rate ) ) );
|
|
|
|
$this->stub_wc( $packages, array( 'flat_rate:1' ) );
|
|
|
|
$wc_cart = Mockery::mock( WC_Cart::class );
|
|
$builder = $this->sut;
|
|
|
|
$result = $builder->build( $wc_cart );
|
|
|
|
$this->assertCount( 1, $result );
|
|
$this->assertSame( 'flat_rate:1', $result[0]['id'] );
|
|
$this->assertSame( 'Flat Rate', $result[0]['name'] );
|
|
$this->assertMoneyValue( $result[0]['price'], 5.0, 'USD' );
|
|
$this->assertTrue( $result[0]['is_selected'] );
|
|
}
|
|
|
|
// -------------------------------------------------------------------------
|
|
// Rule 4 — exactly one is_selected per response
|
|
// -------------------------------------------------------------------------
|
|
|
|
/**
|
|
* @scenario Single package with multiple rates, one chosen
|
|
*
|
|
* Given a cart with one shipping package containing two rates
|
|
* And the session chosen method is the second rate
|
|
* When build() is called
|
|
* Then two options are returned
|
|
* And only the chosen rate has is_selected=true
|
|
* And the other rate has is_selected=false
|
|
*/
|
|
public function test_build_marks_only_chosen_rate_as_selected(): void {
|
|
$rate_flat = $this->create_shipping_rate_stub( 'flat_rate:1', 'Flat Rate', 5.0 );
|
|
$rate_free = $this->create_shipping_rate_stub( 'free_shipping:1', 'Free Shipping', 0.0 );
|
|
|
|
$packages = array(
|
|
$this->make_package(
|
|
array(
|
|
'flat_rate:1' => $rate_flat,
|
|
'free_shipping:1' => $rate_free,
|
|
)
|
|
),
|
|
);
|
|
|
|
$this->stub_wc( $packages, array( 'free_shipping:1' ) );
|
|
|
|
$wc_cart = Mockery::mock( WC_Cart::class );
|
|
$builder = $this->sut;
|
|
|
|
$result = $builder->build( $wc_cart );
|
|
|
|
$this->assertCount( 2, $result );
|
|
|
|
$by_id = array();
|
|
foreach ( $result as $option ) {
|
|
$by_id[ $option['id'] ] = $option;
|
|
}
|
|
|
|
$this->assertFalse( $by_id['flat_rate:1']['is_selected'] );
|
|
$this->assertTrue( $by_id['free_shipping:1']['is_selected'] );
|
|
}
|
|
|
|
// -------------------------------------------------------------------------
|
|
// Rule 5 — default to first rate when no session value
|
|
// -------------------------------------------------------------------------
|
|
|
|
/**
|
|
* @scenario No chosen shipping method in session
|
|
*
|
|
* Given a cart with available shipping rates
|
|
* And the session has no chosen_shipping_methods entry (returns null)
|
|
* When build() is called
|
|
* Then the first available rate is marked as selected
|
|
* And all other rates are not selected
|
|
*/
|
|
public function test_build_selects_first_rate_when_no_chosen_shipping_methods_in_session(): void {
|
|
$rate_first = $this->create_shipping_rate_stub( 'flat_rate:1', 'Standard', 5.0 );
|
|
$rate_second = $this->create_shipping_rate_stub( 'express:1', 'Express', 15.0 );
|
|
|
|
$packages = array(
|
|
$this->make_package(
|
|
array(
|
|
'flat_rate:1' => $rate_first,
|
|
'express:1' => $rate_second,
|
|
)
|
|
),
|
|
);
|
|
|
|
// No chosen methods in session
|
|
$this->stub_wc( $packages, null );
|
|
|
|
$wc_cart = Mockery::mock( WC_Cart::class );
|
|
$builder = $this->sut;
|
|
|
|
$result = $builder->build( $wc_cart );
|
|
|
|
$this->assertCount( 2, $result );
|
|
|
|
// First in iteration order should be selected
|
|
$this->assertTrue( $result[0]['is_selected'] );
|
|
$this->assertFalse( $result[1]['is_selected'] );
|
|
}
|
|
|
|
/**
|
|
* @scenario Chosen shipping methods session is an empty array
|
|
*
|
|
* Given a cart with available shipping rates
|
|
* And WC()->session->get('chosen_shipping_methods') returns an empty array
|
|
* When build() is called
|
|
* Then the first available rate is selected by default
|
|
*/
|
|
public function test_build_selects_first_rate_when_chosen_shipping_methods_is_empty_array(): void {
|
|
$rate = $this->create_shipping_rate_stub( 'flat_rate:1', 'Flat Rate', 5.0 );
|
|
$packages = array( $this->make_package( array( 'flat_rate:1' => $rate ) ) );
|
|
|
|
$this->stub_wc( $packages, array() );
|
|
|
|
$wc_cart = Mockery::mock( WC_Cart::class );
|
|
$builder = $this->sut;
|
|
|
|
$result = $builder->build( $wc_cart );
|
|
|
|
$this->assertCount( 1, $result );
|
|
$this->assertTrue( $result[0]['is_selected'] );
|
|
}
|
|
|
|
// -------------------------------------------------------------------------
|
|
// Rule 6 — amount formatted as 2-decimal string
|
|
// -------------------------------------------------------------------------
|
|
|
|
/**
|
|
* @dataProvider amount_formatting_provider
|
|
*
|
|
* @scenario Amount is formatted as a decimal string with exactly two places
|
|
*
|
|
* Given a shipping rate with a given float cost
|
|
* When build() is called
|
|
* Then the amount in the returned option is a string with exactly two decimal places
|
|
*/
|
|
public function test_build_formats_amount_as_two_decimal_string(
|
|
float $cost,
|
|
string $expected_amount
|
|
): void {
|
|
$rate = $this->create_shipping_rate_stub( 'flat_rate:1', 'Test Rate', $cost );
|
|
$packages = array( $this->make_package( array( 'flat_rate:1' => $rate ) ) );
|
|
|
|
$this->stub_wc( $packages, array( 'flat_rate:1' ) );
|
|
|
|
$wc_cart = Mockery::mock( WC_Cart::class );
|
|
$builder = $this->sut;
|
|
|
|
$result = $builder->build( $wc_cart );
|
|
|
|
$this->assertCount( 1, $result );
|
|
$this->assertMoneyValue( $result[0]['price'], (float) $expected_amount );
|
|
}
|
|
|
|
/**
|
|
* @return array<string, array{float, string}>
|
|
*/
|
|
public function amount_formatting_provider(): array {
|
|
return array(
|
|
'whole number cost becomes two decimal string' => array( 5.0, '5.00' ),
|
|
'one decimal place is padded' => array( 7.5, '7.50' ),
|
|
'already two decimal places is preserved' => array( 12.99, '12.99' ),
|
|
'zero cost is formatted correctly' => array( 0.0, '0.00' ),
|
|
'three decimals are rounded to two' => array( 9.999, '10.00' ),
|
|
'large amount is formatted correctly' => array( 100.0, '100.00' ),
|
|
);
|
|
}
|
|
|
|
// -------------------------------------------------------------------------
|
|
// Rule 7 — currency from get_woocommerce_currency()
|
|
// -------------------------------------------------------------------------
|
|
|
|
/**
|
|
* @scenario Currency is taken from the WooCommerce store setting
|
|
*
|
|
* Given the store currency is set to EUR
|
|
* And a shipping package with one rate exists
|
|
* When build() is called
|
|
* Then the currency field of every returned option reflects the store currency
|
|
*/
|
|
public function test_build_uses_currency_from_get_woocommerce_currency(): void {
|
|
$store_currency = Mockery::mock( StoreCurrencyValue::class );
|
|
$store_currency->allows( 'value' )->andReturn( 'EUR' );
|
|
$builder = new ShippingOptionsBuilder( $store_currency );
|
|
|
|
$rate = $this->create_shipping_rate_stub( 'flat_rate:1', 'Standard', 8.0 );
|
|
$packages = array( $this->make_package( array( 'flat_rate:1' => $rate ) ) );
|
|
|
|
$this->stub_wc( $packages, array( 'flat_rate:1' ) );
|
|
|
|
$wc_cart = Mockery::mock( WC_Cart::class );
|
|
|
|
$result = $builder->build( $wc_cart );
|
|
|
|
$this->assertCount( 1, $result );
|
|
$this->assertSame( 'EUR', $result[0]['price']['currency_code'] );
|
|
}
|
|
|
|
/**
|
|
* @scenario Currency is applied to all options when multiple rates are returned
|
|
*
|
|
* Given a store currency of GBP
|
|
* And a shipping package with two rates
|
|
* When build() is called
|
|
* Then every returned option has currency equal to GBP
|
|
*/
|
|
public function test_build_applies_currency_to_all_options(): void {
|
|
$store_currency = Mockery::mock( StoreCurrencyValue::class );
|
|
$store_currency->allows( 'value' )->andReturn( 'GBP' );
|
|
$builder = new ShippingOptionsBuilder( $store_currency );
|
|
|
|
$rate_a = $this->create_shipping_rate_stub( 'flat_rate:1', 'Standard', 5.0 );
|
|
$rate_b = $this->create_shipping_rate_stub( 'express:1', 'Express', 15.0 );
|
|
|
|
$packages = array(
|
|
$this->make_package(
|
|
array(
|
|
'flat_rate:1' => $rate_a,
|
|
'express:1' => $rate_b,
|
|
)
|
|
),
|
|
);
|
|
|
|
$this->stub_wc( $packages, array( 'flat_rate:1' ) );
|
|
|
|
$wc_cart = Mockery::mock( WC_Cart::class );
|
|
|
|
$result = $builder->build( $wc_cart );
|
|
|
|
$this->assertCount( 2, $result );
|
|
foreach ( $result as $option ) {
|
|
$this->assertSame( 'GBP', $option['price']['currency_code'] );
|
|
}
|
|
}
|
|
|
|
// -------------------------------------------------------------------------
|
|
// Return-shape contract
|
|
// -------------------------------------------------------------------------
|
|
|
|
/**
|
|
* @scenario Each returned option contains all required keys
|
|
*
|
|
* Given a cart with one shipping package and one rate
|
|
* When build() is called
|
|
* Then each option array contains id, name, price (with currency_code and value), and isSelected
|
|
*/
|
|
public function test_build_option_contains_all_required_keys(): void {
|
|
$rate = $this->create_shipping_rate_stub( 'flat_rate:1', 'Flat Rate', 5.0 );
|
|
$packages = array( $this->make_package( array( 'flat_rate:1' => $rate ) ) );
|
|
|
|
$this->stub_wc( $packages, array( 'flat_rate:1' ) );
|
|
|
|
$wc_cart = Mockery::mock( WC_Cart::class );
|
|
$builder = $this->sut;
|
|
|
|
$result = $builder->build( $wc_cart );
|
|
|
|
$this->assertCount( 1, $result );
|
|
$option = $result[0];
|
|
|
|
$this->assertArrayHasKey( 'id', $option );
|
|
$this->assertArrayHasKey( 'name', $option );
|
|
$this->assertArrayHasKey( 'price', $option );
|
|
$this->assertMoneyValue( $option['price'] );
|
|
$this->assertArrayHasKey( 'is_selected', $option );
|
|
$this->assertIsBool( $option['is_selected'] );
|
|
$this->assertIsString( $option['id'] );
|
|
$this->assertIsString( $option['name'] );
|
|
}
|
|
}
|