query( "DROP TABLE IF EXISTS `{$t}`" ); } } protected function setUp(): void { global $wpdb; $wpdb->query( 'TRUNCATE TABLE `' . self::MEM_TABLE . '`' ); $wpdb->query( 'TRUNCATE TABLE `' . self::STATUS_TABLE . '`' ); $wpdb->query( 'TRUNCATE TABLE `' . self::USERMETA . '`' ); } // ── Tests ───────────────────────────────────────────────────────────────── public function test_migrates_membership_level_and_points(): void { $this->seed_usermeta( array( array( 'user_id' => 1, 'meta_key' => 'membership_level', 'meta_value' => 'gold' ), array( 'user_id' => 1, 'meta_key' => 'points_balance', 'meta_value' => '500' ), array( 'user_id' => 2, 'meta_key' => 'membership_level', 'meta_value' => 'silver' ), array( 'user_id' => 2, 'meta_key' => 'points_balance', 'meta_value' => '200' ), ) ); $result = WPDO_Entity_Migration_Engine::migrate_group( 'user', 'membership', array( 'sleep_ms' => 0 ) ); $this->assertSame( 2, $result['migrated'], 'Expected 2 migrated rows' ); $this->assertSame( 0, $result['errors'] ); global $wpdb; $row1 = $wpdb->get_row( "SELECT * FROM `" . self::MEM_TABLE . "` WHERE user_id = 1", ARRAY_A ); $this->assertNotNull( $row1 ); $this->assertSame( 'gold', $row1['membership_level'] ); $this->assertSame( '500', $row1['points_balance'] ); $row2 = $wpdb->get_row( "SELECT * FROM `" . self::MEM_TABLE . "` WHERE user_id = 2", ARRAY_A ); $this->assertNotNull( $row2 ); $this->assertSame( 'silver', $row2['membership_level'] ); $this->assertSame( '200', $row2['points_balance'] ); } public function test_skips_users_with_no_managed_keys(): void { $this->seed_usermeta( array( array( 'user_id' => 3, 'meta_key' => 'some_other_meta', 'meta_value' => 'value' ), ) ); $result = WPDO_Entity_Migration_Engine::migrate_group( 'user', 'membership', array( 'sleep_ms' => 0 ) ); $this->assertSame( 0, $result['migrated'] ); $this->assertSame( 0, $result['errors'] ); } public function test_dry_run_does_not_write_to_flat_table(): void { $this->seed_usermeta( array( array( 'user_id' => 4, 'meta_key' => 'membership_level', 'meta_value' => 'platinum' ), ) ); $result = WPDO_Entity_Migration_Engine::migrate_group( 'user', 'membership', array( 'sleep_ms' => 0, 'dry_run' => true ) ); $this->assertTrue( $result['dry_run'] ); $this->assertSame( 1, $result['migrated'] ); global $wpdb; $count = (int) $wpdb->get_var( "SELECT COUNT(*) FROM `" . self::MEM_TABLE . "`" ); $this->assertSame( 0, $count, 'Dry run must not write to flat table' ); } public function test_row_count_matches_seeded_users(): void { $this->seed_usermeta( array( array( 'user_id' => 10, 'meta_key' => 'membership_level', 'meta_value' => 'bronze' ), array( 'user_id' => 11, 'meta_key' => 'membership_level', 'meta_value' => 'bronze' ), array( 'user_id' => 12, 'meta_key' => 'points_balance', 'meta_value' => '50' ), ) ); $result = WPDO_Entity_Migration_Engine::migrate_group( 'user', 'membership', array( 'sleep_ms' => 0 ) ); // user_id 10, 11 have membership_level; user_id 12 has points_balance. $this->assertSame( 3, $result['migrated'] ); global $wpdb; $count = (int) $wpdb->get_var( "SELECT COUNT(*) FROM `" . self::MEM_TABLE . "`" ); $this->assertSame( 3, $count ); } // ── Helpers ─────────────────────────────────────────────────────────────── private function seed_usermeta( array $rows ): void { global $wpdb; foreach ( $rows as $row ) { $wpdb->insert( self::USERMETA, $row ); } } private static function load_engine_classes(): void { $base = WPDO_PLUGIN_DIR; $files = array( 'includes/adapters/interface-entity-adapter.php', 'includes/engine/class-tmdo-type-caster.php', 'includes/engine/class-tmdo-schema-manager.php', 'includes/engine/class-tmdo-entity-registry.php', 'includes/adapters/class-tmdo-adapter-user.php', 'includes/engine/class-tmdo-entity-migration-engine.php', ); foreach ( $files as $file ) { if ( ! class_exists( self::class_for_file( $file ) ) ) { require_once $base . $file; } } } private static function class_for_file( string $file ): string { $map = array( 'interface-entity-adapter.php' => 'WPDO_Entity_Adapter_Interface', 'class-tmdo-type-caster.php' => 'WPDO_Type_Caster', 'class-tmdo-schema-manager.php' => 'WPDO_Schema_Manager', 'class-tmdo-entity-registry.php' => 'WPDO_Entity_Registry', 'class-tmdo-adapter-user.php' => 'WPDO_Adapter_User', 'class-tmdo-entity-migration-engine.php' => 'WPDO_Entity_Migration_Engine', ); return $map[ basename( $file ) ] ?? ''; } private static function create_tables(): void { global $wpdb; $wpdb->query( 'CREATE TABLE IF NOT EXISTS `' . self::USERMETA . '` ( `umeta_id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, `user_id` bigint(20) unsigned NOT NULL DEFAULT 0, `meta_key` varchar(255) DEFAULT NULL, `meta_value` longtext DEFAULT NULL, PRIMARY KEY (`umeta_id`), KEY `user_id` (`user_id`), KEY `meta_key` (`meta_key`(191)) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4' ); $wpdb->query( 'CREATE TABLE IF NOT EXISTS `' . self::MEM_TABLE . '` ( `user_id` bigint(20) NOT NULL, `membership_level` varchar(100) DEFAULT NULL, `points_balance` bigint(20) DEFAULT 0, `expires_at` datetime DEFAULT NULL, `activated_at` datetime DEFAULT NULL, `tier_source` varchar(255) DEFAULT NULL, `custom_tier` varchar(255) DEFAULT NULL, PRIMARY KEY (`user_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4' ); $wpdb->query( 'CREATE TABLE IF NOT EXISTS `' . self::STATUS_TABLE . '` ( `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, `entity_type` varchar(50) NOT NULL, `group_name` varchar(50) NOT NULL, `last_id` bigint(20) unsigned NOT NULL DEFAULT 0, `total_migrated` bigint(20) unsigned NOT NULL DEFAULT 0, `status` varchar(20) NOT NULL DEFAULT \'pending\', `started_at` datetime DEFAULT NULL, `completed_at` datetime DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `entity_group` (`entity_type`, `group_name`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4' ); } private static function register_user_entity(): void { if ( ! class_exists( 'WPDO_Entity_Registry' ) ) { return; } WPDO_Entity_Registry::register_adapter( 'user', new WPDO_Adapter_User() ); WPDO_Entity_Registry::register_group( 'user', 'membership', array( array( 'key' => 'membership_level', 'type' => 'enum', 'searchable' => true, 'options' => array( 'bronze', 'silver', 'gold', 'platinum', 'custom' ), ), array( 'key' => 'points_balance', 'type' => 'integer', 'searchable' => true, 'default' => 0 ), array( 'key' => 'expires_at', 'type' => 'datetime', 'searchable' => true ), array( 'key' => 'activated_at', 'type' => 'datetime' ), array( 'key' => 'tier_source', 'type' => 'text' ), array( 'key' => 'custom_tier', 'type' => 'text' ), ) ); } }