Files
2meet-data-optimizer/tests/integration/ArrayLiteralRepairTest.php
T
wpdev 08f9e313e0 fix(2meet-data-optimizer): serialize arrays in text fields instead of storing "Array"
- TMDO_Type_Caster: text / textarea values that are arrays or objects are
  stored with maybe_serialize() (was (string) → the literal "Array", which
  bridge-restore-native then wrote back over native meta); from_db()
  unserializes them with TMDO_Safe_Unserialize. text columns are
  VARCHAR(255): longer values are stored NULL (native keeps them) and
  logged as text_overflow.
- Add `wp tmdo repair-array-literals [--entity] [--confirm]`
  (TMDO_Array_Literal_Repair): rewrites flat "Array" values from native
  meta and lists native rows that are "Array" themselves (backup needed).
- Verify: TypeCasterSerializeTest 13 OK, ArrayLiteralRepairTest 3 OK;
  unit 685 OK, integration 483 OK, phpcs (phpcs.xml) 0, stan OK;
  live `serialized` 3 PASS; dev36 repair dry-run lists the value corrupted
  by the 2026-09-26 round trip (user #1 community-events-location)
- PR: PR-055
2026-09-26 18:45:59 +08:00

120 lines
4.9 KiB
PHP

<?php
declare(strict_types=1);
use PHPUnit\Framework\TestCase;
/**
* Repair of text / textarea flat columns holding the literal "Array" (stored
* by the pre-1.1.0 type caster when the meta value was an array).
*
* Fixture (user entity, group al_prefs):
* user 1 prefs 'Array' native prefs serialized array → repairable
* user 2 loc 'Array' native loc 'Array' too → unrepairable, native polluted
* user 3 name 'Array' no native row → unrepairable
* user 4 prefs serialized array, loc 'Taipei' → untouched
*/
class ArrayLiteralRepairTest extends TestCase {
private const TABLE = 'wp_itest_wpdo_user_al_prefs';
public static function setUpBeforeClass(): void {
foreach ( 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/engine/class-tmdo-cache-orchestrator.php',
'includes/adapters/class-tmdo-adapter-user.php',
'includes/migration/class-tmdo-array-literal-repair.php',
) as $file ) {
require_once WPDO_PLUGIN_DIR . $file;
}
}
protected function setUp(): void {
global $wpdb;
self::drop();
$wpdb->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) ) ENGINE=InnoDB" );
$wpdb->query( 'CREATE TABLE `' . self::TABLE . '` ( id BIGINT NOT NULL AUTO_INCREMENT, user_id BIGINT NOT NULL, prefs TEXT DEFAULT NULL, loc VARCHAR(255) DEFAULT NULL, name VARCHAR(255) DEFAULT NULL, PRIMARY KEY (id), UNIQUE KEY uk_entity (user_id) ) ENGINE=InnoDB' );
$prefs = serialize( array( 'core' => array( 'welcomeGuide' => false ) ) );
$wpdb->insert( self::TABLE, array( 'user_id' => 1, 'prefs' => 'Array' ) );
$wpdb->insert( self::TABLE, array( 'user_id' => 2, 'loc' => 'Array' ) );
$wpdb->insert( self::TABLE, array( 'user_id' => 3, 'name' => 'Array' ) );
$wpdb->insert( self::TABLE, array( 'user_id' => 4, 'prefs' => $prefs, 'loc' => 'Taipei' ) );
$wpdb->insert( $wpdb->usermeta, array( 'user_id' => 1, 'meta_key' => 'prefs', 'meta_value' => $prefs ) );
$wpdb->insert( $wpdb->usermeta, array( 'user_id' => 2, 'meta_key' => 'loc', 'meta_value' => 'Array' ) );
$wpdb->insert( $wpdb->usermeta, array( 'user_id' => 4, 'meta_key' => 'prefs', 'meta_value' => $prefs ) );
$GLOBALS['_wp_cache'] = array();
WPDO_Entity_Registry::init();
WPDO_Entity_Registry::register_adapter( 'user', new WPDO_Adapter_User() );
WPDO_Entity_Registry::register_group(
'user',
'al_prefs',
array(
array( 'key' => 'prefs', 'type' => 'textarea' ),
array( 'key' => 'loc', 'type' => 'text' ),
array( 'key' => 'name', 'type' => 'text' ),
)
);
WPDO_Schema_Manager::flush_table_exists_cache();
}
protected function tearDown(): void {
self::drop();
WPDO_Entity_Registry::init();
}
private static function drop(): void {
global $wpdb;
foreach ( array( $wpdb->usermeta, self::TABLE ) as $table ) {
$wpdb->query( "DROP TABLE IF EXISTS `{$table}`" );
}
}
private function flat( int $user_id, string $col ): ?string {
global $wpdb;
return $wpdb->get_var( $wpdb->prepare( "SELECT `{$col}` FROM `" . self::TABLE . '` WHERE user_id = %d', $user_id ) );
}
public function test_dry_run_reports_without_writing(): void {
$report = TMDO_Array_Literal_Repair::run( 'user', false );
$this->assertSame( 3, $report['found'] );
$this->assertSame( 1, $report['repairable'] );
$this->assertSame( 0, $report['repaired'] );
$this->assertSame( 'Array', $this->flat( 1, 'prefs' ) );
}
public function test_apply_repairs_from_native_and_lists_the_rest(): void {
$report = TMDO_Array_Literal_Repair::run( 'user', true );
$this->assertSame( 1, $report['repaired'] );
$this->assertSame( serialize( array( 'core' => array( 'welcomeGuide' => false ) ) ), $this->flat( 1, 'prefs' ) );
$this->assertSame( 'Array', $this->flat( 2, 'loc' ), 'native is "Array" too — nothing to repair from' );
$this->assertSame( 'Array', $this->flat( 3, 'name' ), 'no native row' );
$this->assertSame( 'Taipei', $this->flat( 4, 'loc' ) );
$unrepairable = array_map(
static function ( $r ) {
return $r['id'] . ':' . $r['key'] . ':' . $r['reason'];
},
$report['unrepairable']
);
sort( $unrepairable );
$this->assertSame( array( '2:loc:native_is_array_literal', '3:name:no_native_row' ), $unrepairable );
$this->assertSame( array( array( 'group' => 'al_prefs', 'key' => 'loc', 'id' => 2 ) ), $report['native_polluted'] );
}
public function test_second_run_finds_only_the_unrepairable_rows(): void {
TMDO_Array_Literal_Repair::run( 'user', true );
$report = TMDO_Array_Literal_Repair::run( 'user', true );
$this->assertSame( 2, $report['found'] );
$this->assertSame( 0, $report['repaired'] );
}
}