mirror of
https://github.com/woocommerce/woocommerce-paypal-payments.git
synced 2026-08-07 04:10:25 +08:00
SettingsModel exposes ten typed-bool getters (get_save_paypal_and_venmo,
get_authorize_only, get_capture_virtual_orders, get_instant_payments_only,
get_enable_contact_module, get_save_card_details, get_enable_pay_now,
get_enable_logging, get_stay_updated, get_payment_level_processing) that
return $this->data[$key] directly. On PHP 8.x this throws
Return value must be of type bool, string returned
whenever the stored option holds a non-bool value -- the shape produced by
the legacy classic-WooCommerce 'yes'/'no' settings format, by direct DB
writes, and by some third-party import/export tools that re-serialise
options without preserving scalar types.
Reported in production for get_save_paypal_and_venmo, which fatals during
WcSubscriptionsModule boot and prevents the PPCP REST routes (including
the webhook endpoint at /wp-json/paypal/v1/incoming) from registering.
Two-part defence:
1. Wrap each typed-bool getter with the existing DataSanitizer::sanitize_bool()
(filter_var FILTER_VALIDATE_BOOLEAN). Pure pass-through for real bools;
correctly handles 'yes'/'no'/'1'/'0'/'true'/'false'/'on'/'off' strings
that were observed in production. Each getter also gets a per-field
default via ?? so a missing key falls back to the value declared in
get_defaults() instead of producing a PHP warning.
2. Add an explicit 'vault_enabled' case to SettingsTabMigration so the
legacy 'yes'/'no' string is coerced to bool before from_array() hands
it to set_save_paypal_and_venmo(bool $save). Without this the
migration either TypeErrors on the typed setter (strict mode) or
persists 'no' as true (weak mode, where every non-empty non-'0'
string casts to true).
Tests:
- New tests/PHPUnit/Settings/Data/SettingsModelTest covers every bool
getter against 14 input shapes (real bools, legacy yes/no strings,
'1'/'0', 'true'/'false', 'on'/'off', empty/unrecognised strings,
integers), plus a per-field 'getter returns declared default when
option is empty' matrix.
- Extends SettingsTabMigrationTest with vault_enabled coverage for the
same 14 shapes, asserting both the resulting type and value.
Full PPCP Settings + PpcpSettings suites pass (240 tests, 304 assertions).
156 lines
5.4 KiB
PHP
156 lines
5.4 KiB
PHP
<?php
|
|
|
|
declare( strict_types = 1 );
|
|
|
|
namespace WooCommerce\PayPalCommerce\Settings\Service\Migration;
|
|
|
|
use Mockery;
|
|
use WooCommerce\PayPalCommerce\Settings\Data\SettingsModel;
|
|
use WooCommerce\PayPalCommerce\TestCase;
|
|
|
|
/**
|
|
* Tests the capture_for_virtual_only → capture_virtual_orders migration
|
|
* in SettingsTabMigration.
|
|
*/
|
|
class SettingsTabMigrationTest extends TestCase {
|
|
|
|
/**
|
|
* @dataProvider capture_for_virtual_only_cases
|
|
*
|
|
* @param array $legacy_settings Legacy woocommerce-ppcp-settings values.
|
|
* @param bool|null $expected_authorize Expected authorize_only value, or null if absent.
|
|
* @param bool|null $expected_capture Expected capture_virtual_orders value, or null if
|
|
* absent.
|
|
*/
|
|
public function test_capture_for_virtual_only_migration(
|
|
array $legacy_settings,
|
|
?bool $expected_authorize,
|
|
?bool $expected_capture
|
|
): void {
|
|
$captured_data = null;
|
|
$settings_model = Mockery::mock( SettingsModel::class );
|
|
$settings_model->shouldReceive( 'from_array' )
|
|
->once()
|
|
->withArgs( function ( array $data ) use ( &$captured_data ) {
|
|
$captured_data = $data;
|
|
|
|
return true;
|
|
} );
|
|
$settings_model->shouldReceive( 'save' )->once();
|
|
|
|
$migration = new SettingsTabMigration( $legacy_settings, $settings_model );
|
|
$migration->migrate();
|
|
|
|
if ( $expected_authorize !== null ) {
|
|
$this->assertArrayHasKey( 'authorize_only', $captured_data );
|
|
$this->assertSame( $expected_authorize, $captured_data['authorize_only'] );
|
|
}
|
|
|
|
if ( $expected_capture !== null ) {
|
|
$this->assertArrayHasKey( 'capture_virtual_orders', $captured_data );
|
|
$this->assertSame( $expected_capture, $captured_data['capture_virtual_orders'] );
|
|
} else {
|
|
$this->assertArrayNotHasKey( 'capture_virtual_orders', $captured_data );
|
|
}
|
|
}
|
|
|
|
public function capture_for_virtual_only_cases(): array {
|
|
return array(
|
|
'authorize + capture_for_virtual_only true' => array(
|
|
'legacy_settings' => array(
|
|
'intent' => 'authorize',
|
|
'capture_for_virtual_only' => true,
|
|
),
|
|
'expected_authorize' => true,
|
|
'expected_capture' => true,
|
|
),
|
|
'authorize + capture_for_virtual_only false' => array(
|
|
'legacy_settings' => array(
|
|
'intent' => 'authorize',
|
|
'capture_for_virtual_only' => false,
|
|
),
|
|
'expected_authorize' => true,
|
|
'expected_capture' => false,
|
|
),
|
|
'capture intent without virtual-only flag' => array(
|
|
'legacy_settings' => array(
|
|
'intent' => 'capture',
|
|
),
|
|
'expected_authorize' => false,
|
|
'expected_capture' => null,
|
|
),
|
|
'capture + capture_for_virtual_only true' => array(
|
|
'legacy_settings' => array(
|
|
'intent' => 'capture',
|
|
'capture_for_virtual_only' => true,
|
|
),
|
|
'expected_authorize' => false,
|
|
'expected_capture' => true,
|
|
),
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Legacy classic-WooCommerce settings stored the vault_enabled flag as the
|
|
* string "yes" or "no". The new SettingsModel slot is a typed bool, so the
|
|
* migration must coerce the value before it reaches the typed setter.
|
|
*
|
|
* Without coercion the migration either throws a TypeError (strict mode)
|
|
* or persists "no" as true (weak mode), which downstream causes
|
|
* SettingsModel::get_save_paypal_and_venmo() -- declared `: bool` -- to
|
|
* fatal on PHP 8.x with "Return value must be of type bool, string
|
|
* returned" the next time the setting is read.
|
|
*
|
|
* @dataProvider vault_enabled_cases
|
|
*
|
|
* @param mixed $legacy_value Legacy vault_enabled value, in any of the
|
|
* shapes the option has historically held.
|
|
* @param bool|null $expected Expected save_paypal_and_venmo after
|
|
* migration, or null if the key should be
|
|
* absent from the captured payload.
|
|
*/
|
|
public function test_vault_enabled_migration( $legacy_value, ?bool $expected ): void {
|
|
$legacy_settings = ( $legacy_value === '__missing__' )
|
|
? array()
|
|
: array( 'vault_enabled' => $legacy_value );
|
|
|
|
$captured_data = null;
|
|
$settings_model = Mockery::mock( SettingsModel::class );
|
|
$settings_model->shouldReceive( 'from_array' )
|
|
->once()
|
|
->withArgs( function ( array $data ) use ( &$captured_data ) {
|
|
$captured_data = $data;
|
|
|
|
return true;
|
|
} );
|
|
$settings_model->shouldReceive( 'save' )->once();
|
|
|
|
$migration = new SettingsTabMigration( $legacy_settings, $settings_model );
|
|
$migration->migrate();
|
|
|
|
if ( $expected === null ) {
|
|
$this->assertArrayNotHasKey( 'save_paypal_and_venmo', $captured_data );
|
|
|
|
return;
|
|
}
|
|
|
|
$this->assertArrayHasKey( 'save_paypal_and_venmo', $captured_data );
|
|
$this->assertSame( $expected, $captured_data['save_paypal_and_venmo'] );
|
|
$this->assertIsBool( $captured_data['save_paypal_and_venmo'] );
|
|
}
|
|
|
|
public function vault_enabled_cases(): array {
|
|
return array(
|
|
'legacy string yes coerces to true' => array( 'yes', true ),
|
|
'legacy string no coerces to false' => array( 'no', false ),
|
|
'legacy string 1 coerces to true' => array( '1', true ),
|
|
'legacy string 0 coerces to false' => array( '0', false ),
|
|
'empty string coerces to false' => array( '', false ),
|
|
'real boolean true passes through' => array( true, true ),
|
|
'real boolean false passes through' => array( false, false ),
|
|
'integer 1 coerces to true' => array( 1, true ),
|
|
'integer 0 coerces to false' => array( 0, false ),
|
|
'missing legacy key is not migrated' => array( '__missing__', null ),
|
|
);
|
|
}
|
|
}
|