getProperty( 'table_exists_cache' ); $cache->setAccessible( true ); $existing = $cache->getValue( null ); unset( $existing[ self::FLAT_TABLE ] ); $cache->setValue( null, $existing ); // Errors table (required by WPDO_Logger). $wpdb->query( 'DROP TABLE IF EXISTS `' . self::ERRORS_TABLE . '`' ); $wpdb->query( 'CREATE TABLE `' . self::ERRORS_TABLE . '` ( `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, `module` varchar(100) NOT NULL DEFAULT \'\', `zone` varchar(20) NOT NULL DEFAULT \'\', `hook` varchar(255) NOT NULL DEFAULT \'\', `message` text NOT NULL, `context` longtext, `created_at` datetime NOT NULL DEFAULT \'0000-00-00 00:00:00\', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4' ); // Flat table for user core_profile. $wpdb->query( 'DROP TABLE IF EXISTS `' . self::FLAT_TABLE . '`' ); $wpdb->query( 'CREATE TABLE `' . self::FLAT_TABLE . '` ( `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, `user_id` bigint(20) NOT NULL, `nickname` varchar(255) DEFAULT NULL, `first_name` varchar(255) DEFAULT NULL, `last_name` varchar(255) DEFAULT NULL, `description` text DEFAULT NULL, `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, `updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_user` (`user_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4' ); // Mark table as known-good in Schema_Manager cache (add, don't replace). $existing2 = $cache->getValue( null ) ?? array(); $existing2[ self::FLAT_TABLE ] = true; $cache->setValue( null, $existing2 ); // Register adapter + all user field groups (direct call — add_action is a no-op // in the integration test environment, so ::register() would not fire). WPDO_Entity_Registry::init(); WPDO_Entity_Registry::register_adapter( self::TYPE, new WPDO_Adapter_User() ); WPDO_Member_Fields::register_entity_fields(); } public static function tearDownAfterClass(): void { global $wpdb; $wpdb->query( 'DROP TABLE IF EXISTS `' . self::FLAT_TABLE . '`' ); $wpdb->query( 'DROP TABLE IF EXISTS `' . self::ERRORS_TABLE . '`' ); // Reset Mode_Manager + Entity_Registry so this test class doesn't leak // user mode state into subsequent integration test classes. $mref = new ReflectionClass( WPDO_Mode_Manager::class ); $mcache = $mref->getProperty( 'cache' ); $mcache->setAccessible( true ); $mcache->setValue( null, null ); WPDO_Entity_Registry::init(); // Clear Schema_Manager table_exists_cache entirely so that the `false` // entries written by cleanup_entity (for non-existent group tables) don't // trick subsequent test classes into thinking those tables don't exist after // they create them. $sref = new ReflectionClass( WPDO_Schema_Manager::class ); $scache = $sref->getProperty( 'table_exists_cache' ); $scache->setAccessible( true ); $scache->setValue( null, array() ); } protected function setUp(): void { global $wpdb; $wpdb->query( 'TRUNCATE TABLE `' . self::FLAT_TABLE . '`' ); $GLOBALS['_wp_options'] = array(); $GLOBALS['_wp_cache'] = array(); $GLOBALS['_wp_current_filter'] = ''; // Default: user mode = aeav_only (most tests use this). self::set_user_mode( WPDO_Mode_Manager::MODE_AEAV_ONLY ); // Clear Cache_Orchestrator L1. $ref = new ReflectionClass( WPDO_Cache_Orchestrator::class ); $l1 = $ref->getProperty( 'l1_cache' ); $l1->setAccessible( true ); $l1->setValue( null, array() ); } /** * Inject user mode into Mode_Manager's static cache (bypasses wp_options * and Cache_Orchestrator dependency of Mode_Manager::set()). */ private static function set_user_mode( string $mode ): void { $ref = new ReflectionClass( WPDO_Mode_Manager::class ); $cache = $ref->getProperty( 'cache' ); $cache->setAccessible( true ); $cache->setValue( null, array( 'user' => $mode, 'post' => 'disabled', 'term' => 'dual_write', 'comment' => 'dual_write', ) ); } // ── Helpers ─────────────────────────────────────────────────────────────── private function read_flat_first_name( int $user_id ): ?string { global $wpdb; return $wpdb->get_var( $wpdb->prepare( 'SELECT `first_name` FROM `' . self::FLAT_TABLE . '` WHERE `user_id` = %d', $user_id ) ); } // ── Tests: intercept_update ─────────────────────────────────────────────── public function test_update_writes_to_flat_in_aeav_only_mode(): void { $GLOBALS['_wp_current_filter'] = 'update_user_metadata'; WPDO_Hook_Bus::intercept_update( null, 1, 'first_name', 'Alice', '' ); $this->assertSame( 'Alice', $this->read_flat_first_name( 1 ) ); } public function test_update_returns_true_in_aeav_only_mode(): void { $GLOBALS['_wp_current_filter'] = 'update_user_metadata'; $result = WPDO_Hook_Bus::intercept_update( null, 2, 'first_name', 'Bob', '' ); $this->assertTrue( $result, 'aeav_only must return true to short-circuit EAV write' ); } public function test_update_writes_flat_and_returns_null_in_dual_write_mode(): void { self::set_user_mode( WPDO_Mode_Manager::MODE_DUAL_WRITE ); $GLOBALS['_wp_current_filter'] = 'update_user_metadata'; $result = WPDO_Hook_Bus::intercept_update( null, 3, 'first_name', 'Carol', '' ); $this->assertSame( 'Carol', $this->read_flat_first_name( 3 ) ); $this->assertNull( $result, 'dual_write must return null so WP continues to write EAV' ); } public function test_update_passthrough_for_unregistered_key(): void { $GLOBALS['_wp_current_filter'] = 'update_user_metadata'; $sentinel = new stdClass(); $result = WPDO_Hook_Bus::intercept_update( $sentinel, 4, '_nonexistent_key_xyz', 'x', '' ); $this->assertSame( $sentinel, $result, 'unregistered key must return $check unchanged' ); $this->assertNull( $this->read_flat_first_name( 4 ), 'unregistered key must not write flat table' ); } public function test_update_passthrough_in_disabled_mode(): void { self::set_user_mode( WPDO_Mode_Manager::MODE_DISABLED ); $GLOBALS['_wp_current_filter'] = 'update_user_metadata'; $result = WPDO_Hook_Bus::intercept_update( null, 5, 'first_name', 'Dave', '' ); $this->assertNull( $result, 'disabled mode must return $check unchanged' ); $this->assertNull( $this->read_flat_first_name( 5 ), 'disabled mode must not write flat table' ); } public function test_update_passthrough_when_filter_context_unresolvable(): void { $GLOBALS['_wp_current_filter'] = ''; // no active filter $sentinel = new stdClass(); $result = WPDO_Hook_Bus::intercept_update( $sentinel, 6, 'first_name', 'Eve', '' ); $this->assertSame( $sentinel, $result, 'unknown filter context must return $check unchanged' ); } // ── Tests: intercept_get ───────────────────────────────────────────────── public function test_get_reads_from_flat_in_aeav_only_mode(): void { // Seed the flat table directly. global $wpdb; $wpdb->query( $wpdb->prepare( 'INSERT INTO `' . self::FLAT_TABLE . '` (`user_id`, `first_name`) VALUES (%d, %s)', 10, 'Frank' ) ); // intercept_get accepts $meta_type as 5th arg — bypasses current_filter(). $result = WPDO_Hook_Bus::intercept_get( null, 10, 'first_name', true, 'user' ); $this->assertSame( array( 'Frank' ), $result, 'intercept_get must return [value] array in aeav_only mode' ); } public function test_get_passthrough_in_dual_write_mode(): void { self::set_user_mode( WPDO_Mode_Manager::MODE_DUAL_WRITE ); $result = WPDO_Hook_Bus::intercept_get( null, 11, 'first_name', true, 'user' ); $this->assertNull( $result, 'dual_write does not read from flat — must return $check' ); } public function test_get_returns_empty_string_when_column_is_null(): void { global $wpdb; $wpdb->query( $wpdb->prepare( 'INSERT INTO `' . self::FLAT_TABLE . '` (`user_id`, `first_name`) VALUES (%d, NULL)', 12 ) ); $result = WPDO_Hook_Bus::intercept_get( null, 12, 'first_name', true, 'user' ); $this->assertSame( '', $result ); } // ── Tests: intercept_delete ─────────────────────────────────────────────── public function test_delete_clears_column_to_null_in_aeav_only_mode(): void { global $wpdb; $wpdb->query( $wpdb->prepare( 'INSERT INTO `' . self::FLAT_TABLE . '` (`user_id`, `first_name`) VALUES (%d, %s)', 20, 'Grace' ) ); $GLOBALS['_wp_current_filter'] = 'delete_user_metadata'; WPDO_Hook_Bus::intercept_delete( null, 20, 'first_name', '', false ); $this->assertNull( $this->read_flat_first_name( 20 ), 'delete must set column to NULL (not drop row)' ); } public function test_delete_returns_true_in_aeav_only_mode(): void { global $wpdb; $wpdb->query( $wpdb->prepare( 'INSERT INTO `' . self::FLAT_TABLE . '` (`user_id`, `first_name`) VALUES (%d, %s)', 21, 'Hank' ) ); $GLOBALS['_wp_current_filter'] = 'delete_user_metadata'; $result = WPDO_Hook_Bus::intercept_delete( null, 21, 'first_name', '', false ); $this->assertTrue( $result, 'aeav_only delete must return true to short-circuit EAV delete' ); } // ── Tests: direct_read ─────────────────────────────────────────────────── public function test_direct_read_returns_written_value(): void { $GLOBALS['_wp_current_filter'] = 'update_user_metadata'; WPDO_Hook_Bus::intercept_update( null, 30, 'first_name', 'Ivan', '' ); // Flush cache so direct_read goes to DB. $ref = new ReflectionClass( WPDO_Cache_Orchestrator::class ); $l1 = $ref->getProperty( 'l1_cache' ); $l1->setAccessible( true ); $l1->setValue( null, array() ); $GLOBALS['_wp_cache'] = array(); $value = WPDO_Hook_Bus::direct_read( 'user', 30, 'first_name' ); $this->assertSame( 'Ivan', $value ); } public function test_direct_read_returns_null_for_unknown_key(): void { $value = WPDO_Hook_Bus::direct_read( 'user', 31, '_nonexistent_key_xyz' ); $this->assertNull( $value ); } // ── Tests: cleanup_entity ──────────────────────────────────────────────── public function test_cleanup_entity_removes_flat_rows(): void { global $wpdb; $wpdb->query( $wpdb->prepare( 'INSERT INTO `' . self::FLAT_TABLE . '` (`user_id`, `first_name`) VALUES (%d, %s)', 40, 'Judy' ) ); WPDO_Hook_Bus::cleanup_entity( 'user', 40 ); $count = (int) $wpdb->get_var( $wpdb->prepare( 'SELECT COUNT(*) FROM `' . self::FLAT_TABLE . '` WHERE `user_id` = %d', 40 ) ); $this->assertSame( 0, $count, 'cleanup_entity must delete all flat rows for the entity' ); } }