query( "CREATE TABLE `{$wpdb->usermeta}` ( umeta_id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, user_id BIGINT UNSIGNED NOT NULL DEFAULT 0, meta_key VARCHAR(255) DEFAULT NULL, meta_value LONGTEXT, PRIMARY KEY (umeta_id), KEY user_id (user_id), KEY meta_key (meta_key(191)) ) ENGINE=InnoDB" ); $wpdb->query( 'CREATE TABLE `' . self::A . '` ( id BIGINT NOT NULL AUTO_INCREMENT, user_id BIGINT NOT NULL, token VARCHAR(255) DEFAULT NULL, ips LONGTEXT 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_entity (user_id) ) ENGINE=InnoDB' ); $wpdb->query( 'CREATE TABLE `' . self::B . '` ( id BIGINT NOT NULL AUTO_INCREMENT, user_id BIGINT NOT NULL, other VARCHAR(255) 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_entity (user_id) ) ENGINE=InnoDB' ); $wpdb->query( 'INSERT INTO `' . self::A . "` (user_id, token, ips) VALUES (1, 't1', '[\"1.1.1.1\"]'), (2, 't2', NULL)" ); $wpdb->insert( self::B, array( 'user_id' => 1, 'other' => 'o1' ) ); $wpdb->query( 'UPDATE `' . self::A . "` SET updated_at = '2020-01-01 00:00:00'" ); $wpdb->query( 'UPDATE `' . self::B . "` SET updated_at = '2020-01-01 00:00:00'" ); $GLOBALS['_wp_options'] = array( 'wpdo_bridge_modes' => array( 'user' => 'aeav_only' ) ); $GLOBALS['_wp_cache'] = array(); $GLOBALS['_wp_transients'] = array(); TMDO_Mode_Manager::reset_cache(); self::register( true ); $wpdb->delete( self::META, array( 'entity_type' => 'user', 'group_name' => 'rt_a' ) ); $wpdb->insert( self::META, array( 'entity_type' => 'user', 'group_name' => 'rt_a', 'schema_hash' => 'itest', 'field_definitions' => wp_json_encode( WPDO_Entity_Registry::get_group_fields( 'user', 'rt_a' ) ), ) ); $ref = new ReflectionProperty( WPDO_Schema_Manager::class, 'table_exists_cache' ); $ref->setAccessible( true ); $ref->setValue( null, array() ); } protected function tearDown(): void { self::drop(); WPDO_Entity_Registry::init(); TMDO_Mode_Manager::reset_cache(); } /** Fresh registry; rt_b always registered, rt_a only while its provider is active. */ private static function register( bool $with_a ): void { WPDO_Entity_Registry::init(); WPDO_Entity_Registry::register_adapter( 'user', new WPDO_Adapter_User() ); if ( $with_a ) { WPDO_Entity_Registry::register_group( 'user', 'rt_a', array( array( 'key' => 'token', 'type' => 'text' ), array( 'key' => 'ips', 'type' => 'textarea' ), ) ); } WPDO_Entity_Registry::register_group( 'user', 'rt_b', array( array( 'key' => 'other', 'type' => 'text' ) ) ); } private static function drop(): void { global $wpdb; foreach ( (array) $wpdb->get_col( "SHOW TABLES LIKE 'wp\\_itest\\_wpdo\\_user\\_rt\\_%'" ) as $table ) { $wpdb->query( "DROP TABLE IF EXISTS `{$table}`" ); } $wpdb->query( "DROP TABLE IF EXISTS `{$wpdb->usermeta}`" ); $wpdb->delete( self::META, array( 'entity_type' => 'user', 'group_name' => 'rt_a' ) ); } private function native( int $user_id, string $key ): array { global $wpdb; return $wpdb->get_col( $wpdb->prepare( "SELECT meta_value FROM `{$wpdb->usermeta}` WHERE user_id = %d AND meta_key = %s", $user_id, $key ) ); } public function test_group_option_writes_back_only_that_group(): void { $report = TMDO_Native_Restorer::run( 'user', false, 500, false, 'rt_a' ); $this->assertSame( array( 'rt_a' ), array_keys( $report['groups'] ) ); $this->assertSame( array( 't1' ), $this->native( 1, 'token' ) ); $this->assertSame( array( '["1.1.1.1"]' ), $this->native( 1, 'ips' ) ); $this->assertSame( array(), $this->native( 1, 'other' ), 'rt_b untouched' ); } public function test_retire_refuses_a_group_that_is_still_registered(): void { TMDO_Native_Restorer::run( 'user', false, 500, false, 'rt_a' ); $this->assertSame( 'registered', TMDO_Native_Restorer::retire( 'user', 'rt_a' )['status'] ); global $wpdb; $this->assertSame( self::A, $wpdb->get_var( "SHOW TABLES LIKE '" . self::A . "'" ) ); } public function test_retire_refuses_when_native_meta_differs_from_the_flat_table(): void { self::register( false ); $result = TMDO_Native_Restorer::retire( 'user', 'rt_a' ); $this->assertSame( 'mismatch', $result['status'] ); $this->assertSame( 3, $result['mismatches'], 't1, ["1.1.1.1"], t2 are not native yet' ); } public function test_empty_string_values_count_as_written_back(): void { global $wpdb; $wpdb->query( 'UPDATE `' . self::A . "` SET ips = '' WHERE user_id = 2" ); TMDO_Native_Restorer::run( 'user', false, 500, false, 'rt_a' ); self::register( false ); $this->assertSame( 'retired', TMDO_Native_Restorer::retire( 'user', 'rt_a' )['status'], "wpdb::get_col() reads '' as null" ); } public function test_retire_forgets_the_group_and_keeps_its_table_as_a_backup(): void { global $wpdb; TMDO_Native_Restorer::run( 'user', false, 500, false, 'rt_a' ); self::register( false ); $result = TMDO_Native_Restorer::retire( 'user', 'rt_a' ); $this->assertSame( 'retired', $result['status'] ); $this->assertMatchesRegularExpression( '/^' . self::A . '_retired_\d{8}$/', $result['archived_table'] ); $this->assertSame( $result['archived_table'], $wpdb->get_var( "SHOW TABLES LIKE '" . $result['archived_table'] . "'" ) ); $this->assertNull( $wpdb->get_var( "SHOW TABLES LIKE '" . self::A . "'" ) ); $this->assertSame( '0', $wpdb->get_var( 'SELECT COUNT(*) FROM `' . self::META . "` WHERE entity_type = 'user' AND group_name = 'rt_a'" ) ); $this->assertSame( 0, WPDO_Schema_Manager::restore_orphaned_groups(), 'orphan restore no longer routes the keys to the flat table' ); } }