prefix` so changing the prefix scopes drops to our tables only. private const TEST_PREFIX = 'wp_clnup_'; public static function setUpBeforeClass(): void { global $wpdb; if ( ! class_exists( 'WPDO_Installer' ) ) { require_once WPDO_PLUGIN_DIR . 'includes/class-tmdo-installer.php'; } $wpdb->prefix = self::TEST_PREFIX; $wpdb->options = self::TEST_PREFIX . 'options'; // Ensure options table exists (needed for delete_option fallback path). $wpdb->query( 'CREATE TABLE IF NOT EXISTS `' . self::TEST_PREFIX . 'options` ( option_id bigint(20) unsigned NOT NULL AUTO_INCREMENT, option_name varchar(191) NOT NULL DEFAULT "", option_value longtext NOT NULL, autoload varchar(20) NOT NULL DEFAULT "yes", PRIMARY KEY (option_id), UNIQUE KEY option_name (option_name) ) DEFAULT CHARACTER SET utf8mb4' ); } public static function tearDownAfterClass(): void { global $wpdb; // Drop the dedicated options table + any residual prefix tables. $wpdb->query( 'DROP TABLE IF EXISTS `' . self::TEST_PREFIX . 'options`' ); foreach ( array( self::TEST_PREFIX . 'wpdo_archive', self::TEST_PREFIX . 'wpdo_warm', self::TEST_PREFIX . 'wpdo_errors', self::TEST_PREFIX . 'wpdo_hot_test_type', self::TEST_PREFIX . 'wpdo_user_profile', self::TEST_PREFIX . 'wpdo_post_attachment', self::TEST_PREFIX . 'wpdo_term_hp_taxonomy', self::TEST_PREFIX . 'wpdo_comment_hp_review', self::TEST_PREFIX . 'unrelated_table', ) as $tbl ) { $wpdb->query( 'DROP TABLE IF EXISTS `' . $tbl . '`' ); } // Restore the shared integration test prefix so any teardown elsewhere // that depends on `$wpdb->prefix === 'wp_itest_'` still works. $wpdb->prefix = 'wp_itest_'; } protected function setUp(): void { global $wpdb; // Reset options table state. $wpdb->query( 'TRUNCATE TABLE `' . self::TEST_PREFIX . 'options`' ); $GLOBALS['_wp_options'] = array(); } /** * Events scheduled with args (tmdo_drift_resync( 'post' ), backfill * batches) are not cleared by wp_clear_scheduled_hook( $hook ); the user * stress hook was misnamed and three hooks were missing (2.4.2 PR-138, F-4). */ private const ARG_AND_MISSED_HOOKS = array( 'tmdo_drift_resync', 'wpdo_entity_backfill_batch', 'wpdo_stress_test_batch', 'wpdo_migration_tick', 'wpdo_auto_promote_check' ); public function test_deactivate_unschedules_every_hook_whatever_its_args(): void { $GLOBALS['_wp_unscheduled'] = array(); WPDO_Installer::deactivate(); foreach ( self::ARG_AND_MISSED_HOOKS as $hook ) { $this->assertContains( $hook, $GLOBALS['_wp_unscheduled'], $hook ); } } public function test_uninstall_unschedules_every_hook_whatever_its_args(): void { $GLOBALS['_wp_unscheduled'] = array(); WPDO_Installer::drop_all_tables_for_current_blog(); foreach ( self::ARG_AND_MISSED_HOOKS as $hook ) { $this->assertContains( $hook, $GLOBALS['_wp_unscheduled'], $hook ); } } /** tmdo_* options and transients were left behind; a reinstall then saw stale deferrals and indexes (F-5). */ public function test_uninstall_removes_tmdo_options_and_transients(): void { global $wpdb; foreach ( array( 'tmdo_schema_deferred', 'tmdo_entity_indexes', '_transient_tmdo_cov_x', '_transient_timeout_tmdo_cov_x', 'hivepress_settings', 'tmdo_latepoint_db_version' ) as $name ) { $wpdb->insert( $wpdb->options, array( 'option_name' => $name, 'option_value' => '1' ) ); $GLOBALS['_wp_options'][ $name ] = '1'; } WPDO_Installer::drop_all_tables_for_current_blog(); foreach ( array( 'tmdo_schema_deferred', 'tmdo_entity_indexes', '_transient_tmdo_cov_x', '_transient_timeout_tmdo_cov_x' ) as $name ) { $this->assertArrayNotHasKey( $name, $GLOBALS['_wp_options'], $name ); } $this->assertArrayHasKey( 'hivepress_settings', $GLOBALS['_wp_options'], 'other plugins untouched' ); $this->assertArrayHasKey( 'tmdo_latepoint_db_version', $GLOBALS['_wp_options'], 'the LatePoint add-on owns it' ); } public function test_drops_static_tables(): void { global $wpdb; // Create a few WPDO-prefixed tables that should be dropped. $wpdb->query( 'CREATE TABLE `' . self::TEST_PREFIX . 'wpdo_archive` (id INT)' ); $wpdb->query( 'CREATE TABLE `' . self::TEST_PREFIX . 'wpdo_warm` (id INT)' ); $counts = WPDO_Installer::drop_all_tables_for_current_blog(); $this->assertGreaterThanOrEqual( 2, $counts['tables_dropped'] ); $exists = (int) $wpdb->get_var( $wpdb->prepare( 'SELECT COUNT(*) FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = %s', self::TEST_PREFIX . 'wpdo_archive' ) ); $this->assertSame( 0, $exists, 'wpdo_archive should be dropped' ); } public function test_drops_dynamic_zone_tables(): void { global $wpdb; // Dynamic hot/cold zone tables should be discovered via LIKE pattern. $wpdb->query( 'CREATE TABLE `' . self::TEST_PREFIX . 'wpdo_hot_test_type` (id INT)' ); $wpdb->query( 'CREATE TABLE `' . self::TEST_PREFIX . 'wpdo_user_profile` (id INT)' ); $wpdb->query( 'CREATE TABLE `' . self::TEST_PREFIX . 'wpdo_post_attachment` (id INT)' ); $wpdb->query( 'CREATE TABLE `' . self::TEST_PREFIX . 'wpdo_term_hp_taxonomy` (id INT)' ); $wpdb->query( 'CREATE TABLE `' . self::TEST_PREFIX . 'wpdo_comment_hp_review` (id INT)' ); WPDO_Installer::drop_all_tables_for_current_blog(); foreach ( array( 'wpdo_hot_test_type', 'wpdo_user_profile', 'wpdo_post_attachment', 'wpdo_term_hp_taxonomy', 'wpdo_comment_hp_review', ) as $tbl_suffix ) { $exists = (int) $wpdb->get_var( $wpdb->prepare( 'SELECT COUNT(*) FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = %s', self::TEST_PREFIX . $tbl_suffix ) ); $this->assertSame( 0, $exists, $tbl_suffix . ' should be dropped' ); } } public function test_does_not_drop_unrelated_tables(): void { global $wpdb; // Defensive: a table named like wpdo_X should be dropped, but a table // with a non-wpdo prefix MUST NEVER be dropped even if name pattern // would match. $unrelated = self::TEST_PREFIX . 'unrelated_table'; $wpdb->query( 'CREATE TABLE IF NOT EXISTS `' . $unrelated . '` (id INT)' ); WPDO_Installer::drop_all_tables_for_current_blog(); $exists = (int) $wpdb->get_var( $wpdb->prepare( 'SELECT COUNT(*) FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = %s', $unrelated ) ); $this->assertSame( 1, $exists, 'Non-wpdo table must not be dropped' ); $wpdb->query( 'DROP TABLE IF EXISTS `' . $unrelated . '`' ); } public function test_returns_counts_structure(): void { $counts = WPDO_Installer::drop_all_tables_for_current_blog(); $this->assertIsArray( $counts ); $this->assertArrayHasKey( 'tables_dropped', $counts ); $this->assertArrayHasKey( 'options_deleted', $counts ); $this->assertArrayHasKey( 'crons_cleared', $counts ); } public function test_idempotent_on_empty_state(): void { // Run twice — second run should be a no-op for tables (we already // dropped them all in the first run). Options may not be 0 because // other test classes share the same wp_itest_options table and may // continually re-create wpdo_* rows; just verify that running cleanup // twice in a row does not throw. WPDO_Installer::drop_all_tables_for_current_blog(); $second = WPDO_Installer::drop_all_tables_for_current_blog(); $this->assertSame( 0, $second['tables_dropped'] ); $this->assertIsInt( $second['options_deleted'] ); $this->assertIsInt( $second['crons_cleared'] ); } public function test_drops_known_options(): void { // Stub `delete_option` does not interact with DB layer in our test stubs; // it modifies `$GLOBALS['_wp_options']`. Verify counts work via the // known-options list. $GLOBALS['_wp_options']['wpdo_db_version'] = '2.14.0'; $GLOBALS['_wp_options']['wpdo_features'] = array(); $GLOBALS['_wp_options']['wpdo_health_alert'] = '1'; $counts = WPDO_Installer::drop_all_tables_for_current_blog(); // Note: the actual count depends on $wpdb->options interaction in // the residual sweep. Just verify the structure works without errors. $this->assertIsInt( $counts['options_deleted'] ); } }