diff --git a/tests/Unit/Admin/Settings_Screens/ConnectionTest.php b/tests/Unit/Admin/Settings_Screens/ConnectionTest.php index 45c69fae6..7d5ceae40 100644 --- a/tests/Unit/Admin/Settings_Screens/ConnectionTest.php +++ b/tests/Unit/Admin/Settings_Screens/ConnectionTest.php @@ -4,6 +4,7 @@ use PHPUnit\Framework\TestCase; use WC_Facebookcommerce; use WooCommerce\Facebook\Admin\Settings_Screens\Connection; +use WooCommerce\Facebook\Tests\SafelyUpdateOptionsTestTrait; /** * Class ConnectionTest @@ -12,6 +13,8 @@ */ class ConnectionTest extends TestCase { + use SafelyUpdateOptionsTestTrait; + /** * Helper method to invoke private/protected methods * @@ -143,8 +146,8 @@ public function test_renders_management_url_based_on_merchant_token() { $connection = new Connection(); // Set up the merchant token - update_option('wc_facebook_merchant_access_token', 'test_token'); - + $this->set_option_safely_only_for_this_test('wc_facebook_merchant_access_token', 'test_token'); + // Use output buffering to capture the iframe HTML ob_start(); $this->invoke_method($connection, 'render_facebook_iframe'); diff --git a/tests/Unit/Admin/Settings_Screens/ShopsTest.php b/tests/Unit/Admin/Settings_Screens/ShopsTest.php index e88e19ea5..d92ca5abe 100644 --- a/tests/Unit/Admin/Settings_Screens/ShopsTest.php +++ b/tests/Unit/Admin/Settings_Screens/ShopsTest.php @@ -3,6 +3,7 @@ use PHPUnit\Framework\TestCase; use WooCommerce\Facebook\Admin\Settings_Screens\Shops; +use WooCommerce\Facebook\Tests\SafelyUpdateOptionsTestTrait; /** * Class ShopsTest @@ -11,6 +12,8 @@ */ class ShopsTest extends TestCase { + use SafelyUpdateOptionsTestTrait; + /** @var Shops */ private $shops; @@ -115,7 +118,7 @@ public function test_renders_management_url_based_on_merchant_token() { ->getMock(); // Set up the merchant token - update_option('wc_facebook_merchant_access_token', 'test_token'); + $this->set_option_safely_only_for_this_test('wc_facebook_merchant_access_token', 'test_token'); // Start output buffering to capture the render output ob_start(); diff --git a/tests/Unit/Api/REST/RestAPITest.php b/tests/Unit/Api/REST/RestAPITest.php index ba4f92313..53a6cf430 100644 --- a/tests/Unit/Api/REST/RestAPITest.php +++ b/tests/Unit/Api/REST/RestAPITest.php @@ -9,12 +9,15 @@ use WooCommerce\Facebook\API\Plugin\Settings\Handler; use WooCommerce\Facebook\API\Plugin\Settings\Update\Request as UpdateRequest; use PHPUnit\Framework\TestCase; +use WooCommerce\Facebook\Tests\SafelyUpdateOptionsTestTrait; /** * The REST API unit test class. */ class RestAPITest extends TestCase { + use SafelyUpdateOptionsTestTrait; + /** * Test REST API routes are registered */ @@ -37,25 +40,21 @@ public function test_settings_update_succeeds_with_valid_data() { } // Mock the update_option function - if (!function_exists('update_option')) { + if (!function_exists('\Tests\Api\REST\update_option')) { function update_option($option, $value) { - global $wp_options; - if (!isset($wp_options)) { - $wp_options = []; - } - $wp_options[$option] = $value; + global $test_instance; + $test_instance->set_option_safely_only_for_this_test($option, $value); return true; } } // Mock the get_option function - if (!function_exists('get_option')) { + if (!function_exists('\Tests\Api\REST\get_option')) { function get_option($option, $default = false) { - global $wp_options; - if (!isset($wp_options)) { - $wp_options = []; - } - return isset($wp_options[$option]) ? $wp_options[$option] : $default; + // We can't directly access the original values here easily, + // so we'll rely on the trait's teardown to restore state. + // For the test logic, we return the WP option directly. + return \get_option($option, $default); } } @@ -66,6 +65,10 @@ function wc_bool_to_string($bool) { } } + // Set the global test instance for the mocked update_option + global $test_instance; + $test_instance = $this; + // Create a handler instance $handler = new Handler(); diff --git a/tests/Unit/ExternalVersionUpdate/UpdateTest.php b/tests/Unit/ExternalVersionUpdate/UpdateTest.php index a325b52f9..fcab2b93a 100644 --- a/tests/Unit/ExternalVersionUpdate/UpdateTest.php +++ b/tests/Unit/ExternalVersionUpdate/UpdateTest.php @@ -15,12 +15,15 @@ use ReflectionObject; use WC_Facebookcommerce_Utils; use WP_Error; +use WooCommerce\Facebook\Tests\SafelyUpdateOptionsTestTrait; /** * The External version update unit test class. */ class UpdateTest extends WP_UnitTestCase { + use SafelyUpdateOptionsTestTrait; + /** * Instance of the Update class that we are testing. * @@ -107,7 +110,7 @@ public function test_should_update_version() { ->getMock(); $mock_connection_handler->expects( $this->any() )->method( 'is_connected' )->willReturn( false ); $prop_connection_handler->setValue( $plugin, $mock_connection_handler ); - update_option( 'facebook_for_woocommerce_latest_version_sent_to_server', '0.0.0' ); // Reset the option. + $this->set_option_safely_only_for_this_test( 'facebook_for_woocommerce_latest_version_sent_to_server', '0.0.0' ); // Reset the option. $should_update2 = $this->update->should_update_version(); $this->assertFalse( $should_update2 ); @@ -118,11 +121,11 @@ public function test_should_update_version() { ->getMock(); $mock_connection_handler->expects( $this->any() )->method( 'is_connected' )->willReturn( true ); $prop_connection_handler->setValue( $plugin, $mock_connection_handler ); - update_option( 'facebook_for_woocommerce_latest_version_sent_to_server', WC_Facebookcommerce_Utils::PLUGIN_VERSION ); + $this->set_option_safely_only_for_this_test( 'facebook_for_woocommerce_latest_version_sent_to_server', WC_Facebookcommerce_Utils::PLUGIN_VERSION ); $should_update3 = $this->update->should_update_version(); $this->assertTrue( $should_update3 ); // Because the versions match. - update_option( 'facebook_for_woocommerce_latest_version_sent_to_server', '0.0.0' ); // Reset the option. + $this->set_option_safely_only_for_this_test( 'facebook_for_woocommerce_latest_version_sent_to_server', '0.0.0' ); // Reset the option. $should_update4 = $this->update->should_update_version(); $this->assertTrue( $should_update4 ); } @@ -198,7 +201,7 @@ public function test_maybe_update_external_plugin_version() { $prop_api->setValue( $plugin, $mock_api2 ); // Assert handling failed API response. - update_option( 'facebook_for_woocommerce_latest_version_sent_to_server', '0.0.0' ); // Reset the version to pass the should_update_version check. + $this->set_option_safely_only_for_this_test( 'facebook_for_woocommerce_latest_version_sent_to_server', '0.0.0' ); // Reset the version to pass the should_update_version check. $updated3 = $this->update->maybe_update_external_plugin_version(); $this->assertFalse( $updated3 ); // API failed response is handled. $this->assertNotEquals( WC_Facebookcommerce_Utils::PLUGIN_VERSION, get_option( 'facebook_for_woocommerce_latest_version_sent_to_server' ) ); // API failed response should not update the option. @@ -209,7 +212,7 @@ public function test_maybe_update_external_plugin_version() { $prop_api->setValue( $plugin, $mock_api3 ); // Assert PluginException Handling. - update_option( 'facebook_for_woocommerce_latest_version_sent_to_server', '0.0.0' ); // Reset the version to pass the should_update_version check. + $this->set_option_safely_only_for_this_test( 'facebook_for_woocommerce_latest_version_sent_to_server', '0.0.0' ); // Reset the version to pass the should_update_version check. $updated4 = $this->update->maybe_update_external_plugin_version(); $this->assertFalse( $updated4 ); // API failed response is handled. $this->assertNotEquals( WC_Facebookcommerce_Utils::PLUGIN_VERSION, get_option( 'facebook_for_woocommerce_latest_version_sent_to_server' ) ); // API failed response should not update the option. @@ -220,7 +223,7 @@ public function test_maybe_update_external_plugin_version() { $prop_api->setValue( $plugin, $mock_api4 ); // Assert ApiException Handling. - update_option( 'facebook_for_woocommerce_latest_version_sent_to_server', '0.0.0' ); // Reset the version to pass the should_update_version check. + $this->set_option_safely_only_for_this_test( 'facebook_for_woocommerce_latest_version_sent_to_server', '0.0.0' ); // Reset the version to pass the should_update_version check. $updated5 = $this->update->maybe_update_external_plugin_version(); $this->assertFalse( $updated5 ); // API failed response is handled. $this->assertNotEquals( WC_Facebookcommerce_Utils::PLUGIN_VERSION, get_option( 'facebook_for_woocommerce_latest_version_sent_to_server' ) ); // API failed response should not update the option. diff --git a/tests/Unit/FacebookCommercePixelEventTest.php b/tests/Unit/FacebookCommercePixelEventTest.php index 23f74ee85..89f272ab8 100644 --- a/tests/Unit/FacebookCommercePixelEventTest.php +++ b/tests/Unit/FacebookCommercePixelEventTest.php @@ -1,8 +1,13 @@ set_option_safely_only_for_this_test( WC_Facebookcommerce_Pixel::SETTINGS_KEY, $existing_options ); $actual_options = WC_Facebookcommerce_Pixel::get_options(); @@ -44,7 +50,8 @@ public function test_get_options_returns_merged_options_when_options_exist() { } public function test_get_options_returns_default_options_when_options_are_not_an_array() { - update_option( WC_Facebookcommerce_Pixel::SETTINGS_KEY, 'not an array' ); + // Use the trait method to set the option for this test. + $this->set_option_safely_only_for_this_test( WC_Facebookcommerce_Pixel::SETTINGS_KEY, 'not an array' ); $expected_options = array( WC_Facebookcommerce_Pixel::PIXEL_ID_KEY => '0', diff --git a/tests/Unit/Feed/AbstractFeedTest.php b/tests/Unit/Feed/AbstractFeedTest.php index 2c05385f7..fc5944af6 100644 --- a/tests/Unit/Feed/AbstractFeedTest.php +++ b/tests/Unit/Feed/AbstractFeedTest.php @@ -12,6 +12,7 @@ use WP_UnitTestCase; use WooCommerce\Facebook\Utilities\Heartbeat; +use WooCommerce\Facebook\Tests\SafelyUpdateOptionsTestTrait; class TestFeed extends AbstractFeed { public function __construct(FeedFileWriter $file_writer, AbstractFeedHandler $feed_handler, FeedGenerator $feed_generator) { @@ -40,6 +41,9 @@ protected static function get_feed_gen_scheduling_interval(): string { } class AbstractFeedTest extends WP_UnitTestCase { + + use SafelyUpdateOptionsTestTrait; + /** * The test feed class. * @@ -57,17 +61,17 @@ public function setUp(): void { } public function testShouldSkipFeed() { - update_option( 'wc_facebook_commerce_partner_integration_id', '1841465350002849' ); - update_option( 'wc_facebook_commerce_merchant_settings_id', '1352794439398752' ); + $this->set_option_safely_only_for_this_test( 'wc_facebook_commerce_partner_integration_id', '1841465350002849' ); + $this->set_option_safely_only_for_this_test( 'wc_facebook_commerce_merchant_settings_id', '1352794439398752' ); $this->assertFalse( $this->feed->should_skip_feed(), 'Feed should not be skipped when CPI ID and CMS ID are set.' ); - update_option( 'wc_facebook_commerce_partner_integration_id', '' ); - update_option( 'wc_facebook_commerce_merchant_settings_id', '1352794439398752' ); + $this->set_option_safely_only_for_this_test( 'wc_facebook_commerce_partner_integration_id', '' ); + $this->set_option_safely_only_for_this_test( 'wc_facebook_commerce_merchant_settings_id', '1352794439398752' ); $this->assertTrue( $this->feed->should_skip_feed(), 'Feed should be skipped when CPI ID is empty.' ); - update_option( 'wc_facebook_commerce_partner_integration_id', '1841465350002849' ); - update_option( 'wc_facebook_commerce_merchant_settings_id', '' ); + $this->set_option_safely_only_for_this_test( 'wc_facebook_commerce_partner_integration_id', '1841465350002849' ); + $this->set_option_safely_only_for_this_test( 'wc_facebook_commerce_merchant_settings_id', '' ); $this->assertTrue( $this->feed->should_skip_feed(), 'Feed should be skipped when CMS ID is empty.' ); - update_option( 'wc_facebook_commerce_partner_integration_id', '' ); - update_option( 'wc_facebook_commerce_merchant_settings_id', '' ); + $this->set_option_safely_only_for_this_test( 'wc_facebook_commerce_partner_integration_id', '' ); + $this->set_option_safely_only_for_this_test( 'wc_facebook_commerce_merchant_settings_id', '' ); $this->assertTrue( $this->feed->should_skip_feed(), 'Feed should be skipped when both CPI ID and CMS ID are empty.' ); } diff --git a/tests/Unit/Feed/FeedUploadUtilsTest.php b/tests/Unit/Feed/FeedUploadUtilsTest.php index 4d6fd0374..b3f759edf 100644 --- a/tests/Unit/Feed/FeedUploadUtilsTest.php +++ b/tests/Unit/Feed/FeedUploadUtilsTest.php @@ -9,6 +9,8 @@ require_once __DIR__ . '/../../../includes/Feed/FeedUploadUtils.php'; +use WooCommerce\Facebook\Tests\SafelyUpdateOptionsTestTrait; + /** * Class FeedUploadUtilsTest * @@ -16,6 +18,8 @@ */ class FeedUploadUtilsTest extends \WooCommerce\Facebook\Tests\AbstractWPUnitTestWithSafeFiltering { + use SafelyUpdateOptionsTestTrait; + /** @var int Shop page ID */ protected static $shop_page_id; @@ -31,7 +35,7 @@ public function setUp(): void { return '/%postname%/'; }); - update_option( 'permalink_structure', '/%postname%/' ); + $this->set_option_safely_only_for_this_test( 'permalink_structure', '/%postname%/' ); global $wp_rewrite; if ( ! ( $wp_rewrite instanceof WP_Rewrite ) ) { $wp_rewrite = new WP_Rewrite(); @@ -41,10 +45,10 @@ public function setUp(): void { flush_rewrite_rules(); // Set basic site options. - update_option( 'blogname', 'Test Store' ); - update_option( 'wc_facebook_commerce_merchant_settings_id', '123456789' ); - update_option( 'siteurl', 'https://example.com' ); - update_option( 'home', 'https://example.com' ); + $this->set_option_safely_only_for_this_test( 'blogname', 'Test Store' ); + $this->set_option_safely_only_for_this_test( 'wc_facebook_commerce_merchant_settings_id', '123456789' ); + $this->set_option_safely_only_for_this_test( 'siteurl', 'https://example.com' ); + $this->set_option_safely_only_for_this_test( 'home', 'https://example.com' ); // Create and register the Shop page. self::$shop_page_id = self::factory()->post->create( [ @@ -53,7 +57,7 @@ public function setUp(): void { 'post_title' => 'Shop', 'post_name' => 'shop' ] ); - update_option( 'woocommerce_shop_page_id', self::$shop_page_id ); + $this->set_option_safely_only_for_this_test( 'woocommerce_shop_page_id', self::$shop_page_id ); flush_rewrite_rules(); // Add high–priority filters to force URLs. diff --git a/tests/Unit/Handlers/MetaExtensionTest.php b/tests/Unit/Handlers/MetaExtensionTest.php index 812985fa9..90ba6395e 100644 --- a/tests/Unit/Handlers/MetaExtensionTest.php +++ b/tests/Unit/Handlers/MetaExtensionTest.php @@ -7,12 +7,15 @@ use WooCommerce\Facebook\Handlers\MetaExtension; use WP_UnitTestCase; +use WooCommerce\Facebook\Tests\SafelyUpdateOptionsTestTrait; /** * The Meta Extension unit test class. */ class MetaExtensionTest extends \WP_UnitTestCase { + use SafelyUpdateOptionsTestTrait; + /** * Instance of the MetaExtension class that we are testing. * @@ -48,7 +51,7 @@ public function test_generate_iframe_splash_url() { * Test generate_iframe_management_url */ public function test_generate_iframe_management_url() { - update_option( 'wc_facebook_access_token', 'test_merchant_token' ); + $this->set_option_safely_only_for_this_test( 'wc_facebook_access_token', 'test_merchant_token' ); // Test with empty business ID (should return empty string) $url = MetaExtension::generate_iframe_management_url(''); diff --git a/tests/Unit/SafelyUpdateOptionsTestTrait.php b/tests/Unit/SafelyUpdateOptionsTestTrait.php new file mode 100644 index 000000000..fb51e8d6e --- /dev/null +++ b/tests/Unit/SafelyUpdateOptionsTestTrait.php @@ -0,0 +1,93 @@ + Key-value pairs. False indicates the option did not exist initially. + */ + private array $original_options = []; + + /** + * A special value to indicate that an option did not exist before being set. + */ + // private const OPTION_DOES_NOT_EXIST = '__OPTION_DOES_NOT_EXIST__'; // Removed invalid constant + + /** + * Set up before each test. + * + * Resets the record of original options. + * Due to the use of the before annotation, this method is called before each test function. + * + * @before + */ + protected function setup_options_safely_trait(): void { + $this->original_options = []; + } + + /** + * Tear down after each test. + * + * Restores all modified options to their original values. + * Due to the use of the after annotation, this method is called after each test function. + * + * @after + */ + protected function tear_down_options_safely_trait(): void { + foreach ( $this->original_options as $key => $original_value ) { + if ( '__OPTION_DOES_NOT_EXIST__' === $original_value ) { // Use literal string + delete_option( $key ); + } else { + update_option( $key, $original_value ); + } + } + // Reset for the next test, although setup_options_trait should handle this too. + $this->original_options = []; + } + + /** + * Safely update a WordPress option for the duration of a test. + * + * Records the original value (or lack thereof) before updating. + * + * @param string $key The option name. + * @param mixed $value The new option value. + * @return bool True if the value was updated, false otherwise. + */ + protected function set_option_safely_only_for_this_test( string $key, $value ): bool { + if ( ! array_key_exists( $key, $this->original_options ) ) { + $current_value = get_option( $key, '__OPTION_DOES_NOT_EXIST__' ); // Use literal string + $this->original_options[ $key ] = $current_value; + } + + return update_option( $key, $value ); + } + + /** + * Safely delete a WordPress option for the duration of a test. + * + * Records the original value (or lack thereof) before deleting. + * + * @param string $key The option name. + * @return bool True if the option was deleted, false otherwise. + */ + protected function remove_option_safely_only_for_this_test( string $key ): bool { + if ( ! array_key_exists( $key, $this->original_options ) ) { + $current_value = get_option( $key, '__OPTION_DOES_NOT_EXIST__' ); // Use literal string + $this->original_options[ $key ] = $current_value; + } + + return delete_option( $key ); + } +} \ No newline at end of file diff --git a/tests/Unit/SafelyUpdateOptionsTestTraitTest.php b/tests/Unit/SafelyUpdateOptionsTestTraitTest.php new file mode 100644 index 000000000..4c763026e --- /dev/null +++ b/tests/Unit/SafelyUpdateOptionsTestTraitTest.php @@ -0,0 +1,203 @@ + + * @throws \ReflectionException + */ + private function get_recorded_original_options(): array { + $reflection = new ReflectionClass( $this ); + $originalOptionsProp = $reflection->getProperty('original_options'); + $originalOptionsProp->setAccessible(true); + return $originalOptionsProp->getValue( $this ); + } + + /** + * @test + * Verify setting a previously non-existent option records its non-existence. + */ + public function it_should_record_non_existence_when_setting_new_option(): void { + $this->assertFalse( get_option( self::TEST_OPTION_KEY_1 ), 'Pre-condition: Option should not exist.' ); + + $result = $this->set_option_safely_only_for_this_test( self::TEST_OPTION_KEY_1, self::TEST_OPTION_VALUE_NEW ); + + $this->assertTrue( $result, 'update_option should return true.' ); + $this->assertEquals( self::TEST_OPTION_VALUE_NEW, get_option( self::TEST_OPTION_KEY_1 ), 'Option should have the new value during the test.' ); + + $recorded_options = $this->get_recorded_original_options(); + $this->assertArrayHasKey( self::TEST_OPTION_KEY_1, $recorded_options, 'Trait should have recorded the option key.' ); + $this->assertEquals( '__OPTION_DOES_NOT_EXIST__', $recorded_options[ self::TEST_OPTION_KEY_1 ], 'Trait should record that the option did not exist.' ); + // Trait's @after hook will run and delete this option. + } + + /** + * @test + * Verify updating an existing option records its original value. + */ + public function it_should_record_original_value_when_updating_existing_option(): void { + update_option( self::TEST_OPTION_KEY_1, self::TEST_OPTION_VALUE_ORIGINAL ); + $this->assertEquals( self::TEST_OPTION_VALUE_ORIGINAL, get_option( self::TEST_OPTION_KEY_1 ), 'Pre-condition: Option should have original value.' ); + + $result = $this->set_option_safely_only_for_this_test( self::TEST_OPTION_KEY_1, self::TEST_OPTION_VALUE_NEW ); + + $this->assertTrue( $result, 'update_option should return true.' ); + $this->assertEquals( self::TEST_OPTION_VALUE_NEW, get_option( self::TEST_OPTION_KEY_1 ), 'Option should have the new value during the test.' ); + + $recorded_options = $this->get_recorded_original_options(); + $this->assertArrayHasKey( self::TEST_OPTION_KEY_1, $recorded_options, 'Trait should have recorded the option key.' ); + $this->assertEquals( self::TEST_OPTION_VALUE_ORIGINAL, $recorded_options[ self::TEST_OPTION_KEY_1 ], 'Trait should record the original value.' ); + // Trait's @after hook will run and restore the original value. + } + + /** + * @test + * Verify deleting an existing option records its original value. + */ + public function it_should_record_original_value_when_deleting_existing_option(): void { + update_option( self::TEST_OPTION_KEY_1, self::TEST_OPTION_VALUE_ORIGINAL ); + $this->assertEquals( self::TEST_OPTION_VALUE_ORIGINAL, get_option( self::TEST_OPTION_KEY_1 ), 'Pre-condition: Option should have original value.' ); + + $result = $this->remove_option_safely_only_for_this_test( self::TEST_OPTION_KEY_1 ); + + $this->assertTrue( $result, 'delete_option should return true for existing option.' ); + $this->assertFalse( get_option( self::TEST_OPTION_KEY_1 ), 'Option should be deleted during the test.' ); + + $recorded_options = $this->get_recorded_original_options(); + $this->assertArrayHasKey( self::TEST_OPTION_KEY_1, $recorded_options, 'Trait should have recorded the option key.' ); + $this->assertEquals( self::TEST_OPTION_VALUE_ORIGINAL, $recorded_options[ self::TEST_OPTION_KEY_1 ], 'Trait should record the original value before deletion.' ); + // Trait's @after hook will run and restore the original value. + } + + /** + * @test + * Verify "deleting" a non-existent option records its non-existence. + */ + public function it_should_record_non_existence_when_deleting_non_existent_option(): void { + $this->assertFalse( get_option( self::TEST_OPTION_KEY_1 ), 'Pre-condition: Option should not exist.' ); + + $result = $this->remove_option_safely_only_for_this_test( self::TEST_OPTION_KEY_1 ); + + // delete_option returns false if the key doesn't exist. + $this->assertFalse( $result, 'delete_option should return false for non-existent option.' ); + $this->assertFalse( get_option( self::TEST_OPTION_KEY_1 ), 'Option should remain non-existent during the test.' ); + + $recorded_options = $this->get_recorded_original_options(); + $this->assertArrayHasKey( self::TEST_OPTION_KEY_1, $recorded_options, 'Trait should have recorded the option key.' ); + $this->assertEquals( '__OPTION_DOES_NOT_EXIST__', $recorded_options[ self::TEST_OPTION_KEY_1 ], 'Trait should record that the option did not exist.' ); + // Trait's @after hook will run and try to delete the option (no-op). + } + + /** + * @test + * Verify multiple safe operations within a single test are recorded correctly. + */ + public function it_should_record_multiple_changes_correctly(): void { + // Initial state: key1 exists, key2 does not. + update_option( self::TEST_OPTION_KEY_1, self::TEST_OPTION_VALUE_ORIGINAL ); + $this->assertEquals( self::TEST_OPTION_VALUE_ORIGINAL, get_option( self::TEST_OPTION_KEY_1 ), 'Pre-condition 1 failed.' ); + $this->assertFalse( get_option( self::TEST_OPTION_KEY_2 ), 'Pre-condition 2 failed.' ); + + // Perform multiple safe operations. + $this->set_option_safely_only_for_this_test( self::TEST_OPTION_KEY_1, self::TEST_OPTION_VALUE_NEW ); // Update existing. + $this->set_option_safely_only_for_this_test( self::TEST_OPTION_KEY_2, self::TEST_OPTION_VALUE_NEW ); // Set new. + + // Check values during test. + $this->assertEquals( self::TEST_OPTION_VALUE_NEW, get_option( self::TEST_OPTION_KEY_1 ), 'Option 1 should be updated.' ); + $this->assertEquals( self::TEST_OPTION_VALUE_NEW, get_option( self::TEST_OPTION_KEY_2 ), 'Option 2 should be set.' ); + + // Check recorded original values. + $recorded_options = $this->get_recorded_original_options(); + $this->assertCount( 2, $recorded_options, 'Should have recorded two options.' ); + $this->assertArrayHasKey( self::TEST_OPTION_KEY_1, $recorded_options ); + $this->assertEquals( self::TEST_OPTION_VALUE_ORIGINAL, $recorded_options[ self::TEST_OPTION_KEY_1 ], 'Original value for key 1 should be recorded.' ); + $this->assertArrayHasKey( self::TEST_OPTION_KEY_2, $recorded_options ); + $this->assertEquals( '__OPTION_DOES_NOT_EXIST__', $recorded_options[ self::TEST_OPTION_KEY_2 ], 'Non-existence for key 2 should be recorded.' ); + // Trait's @after hook will restore key1 and delete key2. + } + + /** + * @test + * Verify that calling set multiple times only records the *first* original value. + */ + public function it_should_only_record_the_very_first_value_when_setting_multiple_times(): void { + update_option( self::TEST_OPTION_KEY_1, self::TEST_OPTION_VALUE_ORIGINAL ); + + // Set multiple times. + $this->set_option_safely_only_for_this_test( self::TEST_OPTION_KEY_1, 'intermediate_value' ); + $this->set_option_safely_only_for_this_test( self::TEST_OPTION_KEY_1, self::TEST_OPTION_VALUE_NEW ); + + $this->assertEquals( self::TEST_OPTION_VALUE_NEW, get_option( self::TEST_OPTION_KEY_1 ), 'Option should have the final value set.' ); + + $recorded_options = $this->get_recorded_original_options(); + $this->assertArrayHasKey( self::TEST_OPTION_KEY_1, $recorded_options ); + $this->assertEquals( self::TEST_OPTION_VALUE_ORIGINAL, $recorded_options[ self::TEST_OPTION_KEY_1 ], 'Trait should record the very first original value, not intermediate ones.' ); + // Trait's @after hook will restore to the original value. + } + + /** + * @test + * Verify that calling remove multiple times only records the *first* original value. + */ + public function it_should_only_record_the_very_first_value_when_removing_multiple_times(): void { + update_option( self::TEST_OPTION_KEY_1, self::TEST_OPTION_VALUE_ORIGINAL ); + + // Remove multiple times. + $this->remove_option_safely_only_for_this_test( self::TEST_OPTION_KEY_1 ); + $this->remove_option_safely_only_for_this_test( self::TEST_OPTION_KEY_1 ); // Second remove should be no-op for recording. + + $this->assertFalse( get_option( self::TEST_OPTION_KEY_1 ), 'Option should be deleted.' ); + + $recorded_options = $this->get_recorded_original_options(); + $this->assertArrayHasKey( self::TEST_OPTION_KEY_1, $recorded_options ); + $this->assertEquals( self::TEST_OPTION_VALUE_ORIGINAL, $recorded_options[ self::TEST_OPTION_KEY_1 ], 'Trait should record the very first original value before deletion.' ); + // Trait's @after hook will restore the original value. + } +} \ No newline at end of file diff --git a/tests/Unit/WCFacebookCommerceIntegrationTest.php b/tests/Unit/WCFacebookCommerceIntegrationTest.php index a5c952b7c..4150817a7 100644 --- a/tests/Unit/WCFacebookCommerceIntegrationTest.php +++ b/tests/Unit/WCFacebookCommerceIntegrationTest.php @@ -12,12 +12,15 @@ use WooCommerce\Facebook\Products; use WooCommerce\Facebook\ProductSync\ProductValidator; use WooCommerce\Facebook\Framework\AdminMessageHandler; +use WooCommerce\Facebook\Tests\SafelyUpdateOptionsTestTrait; // Add this line /** * Unit tests for Facebook Graph API calls. */ class WCFacebookCommerceIntegrationTest extends \WooCommerce\Facebook\Tests\AbstractWPUnitTestWithSafeFiltering { + use SafelyUpdateOptionsTestTrait; // Add this line + /** * @var WC_Facebookcommerce */ @@ -905,9 +908,11 @@ public function test_on_product_publish_variable_product() { * @return void */ public function test_delete_on_out_of_stock_deletes_simple_product() { - $product = WC_Helper_Product::create_simple_product(); + // Set WC option to hide out of stock items + $this->set_option_safely_only_for_this_test( 'woocommerce_hide_out_of_stock_items', 'yes' ); - update_option( 'woocommerce_hide_out_of_stock_items', 'yes' ); + // Create a simple product and set it to out of stock + $product = WC_Helper_Product::create_simple_product(); $product->set_stock_status( 'outofstock' ); add_post_meta( $product->get_id(), WC_Facebookcommerce_Integration::FB_PRODUCT_ITEM_ID, 'facebook-product-item-id' ); @@ -927,9 +932,11 @@ public function test_delete_on_out_of_stock_deletes_simple_product() { * @return void */ public function test_delete_on_out_of_stock_does_not_delete_simple_product_with_wc_settings_off() { - $product = WC_Helper_Product::create_simple_product(); + // Set WC option to *not* hide out of stock items + $this->set_option_safely_only_for_this_test( 'woocommerce_hide_out_of_stock_items', 'no' ); - update_option( 'woocommerce_hide_out_of_stock_items', 'no' ); + // Create a simple product and set it to out of stock + $product = WC_Helper_Product::create_simple_product(); $product->set_stock_status( 'outofstock' ); $this->api->expects( $this->never() ) @@ -946,10 +953,11 @@ public function test_delete_on_out_of_stock_does_not_delete_simple_product_with_ * @return void */ public function test_delete_on_out_of_stock_does_not_delete_in_stock_simple_product() { - $product = WC_Helper_Product::create_variation_product(); + // Set WC option to hide out of stock items + $this->set_option_safely_only_for_this_test( 'woocommerce_hide_out_of_stock_items', 'yes' ); - update_option( 'woocommerce_hide_out_of_stock_items', 'yes' ); - $product->set_stock_status( 'instock' ); + // Create a simple product and ensure it is in stock + $product = WC_Helper_Product::create_simple_product(); $this->api->expects( $this->never() ) ->method( 'delete_product_item' ); @@ -976,7 +984,8 @@ public function test_on_variable_product_publish_existing_product_updates_produc ->method( 'get_product_sync_validator' ) ->willReturn( $validator ); - update_option( 'woocommerce_hide_out_of_stock_items', 'yes' ); + // Set WC option to hide out of stock items + $this->set_option_safely_only_for_this_test( 'woocommerce_hide_out_of_stock_items', 'yes' ); $facebook_product->woo_product->set_stock_status( 'instock' ); add_post_meta( $product->get_id(), WC_Facebookcommerce_Integration::FB_PRODUCT_GROUP_ID, 'facebook-variable-product-group-item-id' ); @@ -1019,7 +1028,8 @@ public function test_on_variable_product_publish_new_product_creates_product() { ->method( 'get_product_sync_validator' ) ->willReturn( $validator ); - update_option( 'woocommerce_hide_out_of_stock_items', 'yes' ); + // Set WC option to hide out of stock items + $this->set_option_safely_only_for_this_test( 'woocommerce_hide_out_of_stock_items', 'yes' ); $facebook_product->woo_product->set_stock_status( 'instock' ); add_post_meta( $product->get_id(), WC_Facebookcommerce_Integration::FB_PRODUCT_GROUP_ID, '' ); @@ -1062,7 +1072,8 @@ public function test_on_simple_product_publish_existing_product_updates_product( ->with( $facebook_product->woo_product ) ->willReturn( $validator ); - update_option( 'woocommerce_hide_out_of_stock_items', 'yes' ); + // Set WC option to hide out of stock items + $this->set_option_safely_only_for_this_test( 'woocommerce_hide_out_of_stock_items', 'yes' ); $facebook_product->woo_product->set_stock_status( 'instock' ); add_post_meta( $product->get_id(), WC_Facebookcommerce_Integration::FB_PRODUCT_ITEM_ID, 'facebook-simple-product-item-id' ); @@ -1106,7 +1117,8 @@ public function test_on_simple_product_publish_existing_product_creates_product( ->with( $facebook_product->woo_product ) ->willReturn( $validator ); - update_option( 'woocommerce_hide_out_of_stock_items', 'yes' ); + // Set WC option to hide out of stock items + $this->set_option_safely_only_for_this_test( 'woocommerce_hide_out_of_stock_items', 'yes' ); $facebook_product->woo_product->set_stock_status( 'instock' ); add_post_meta( $product->get_id(), WC_Facebookcommerce_Integration::FB_PRODUCT_ITEM_ID, '' );