Files
wpdev d36bb954d1 chore: initial snapshot of 2meet-data-optimizer v0.1.0
Baseline before backporting wp-data-optimizer v3.0.1-v3.4.6.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TbG1keQQ7XBa7qMQY16KCY
2026-07-31 05:06:36 +08:00

273 lines
10 KiB
PHP

<?php
declare(strict_types=1);
use PHPUnit\Framework\TestCase;
/**
* Integration tests for WPDO_Sync_Bridge against real MariaDB.
*
* Validates the full dual-write path:
* intercept_update / intercept_add → Zone Hot table written
* intercept_get → Zone Hot table read when module in read-custom state
* intercept_delete → Zone Hot column zeroed out
*
* Uses a dedicated test post type `test_post` and table `wp_itest_wpdo_hot_test_post`.
* get_post_type() is driven by $GLOBALS['_wp_post_types'] set in each test.
*/
class SyncBridgeIntegrationTest extends TestCase {
private const POST_TYPE = 'test_post';
private const TABLE = 'wp_itest_wpdo_hot_test_post';
private const MODULE = 'hot_test_post'; // WPDO_Sync_Bridge::get_zone_module('hot', 'test_post')
private const FIELD = 'hp_price';
private WPDO_Sync_Bridge $bridge;
// ── Fixture lifecycle ─────────────────────────────────────────────────────
public static function setUpBeforeClass(): void {
global $wpdb;
// DROP + CREATE ensures clean schema even after interrupted prior runs.
$wpdb->query( 'DROP TABLE IF EXISTS `' . self::TABLE . '`' );
$wpdb->query(
'CREATE TABLE `' . self::TABLE . '` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`post_id` bigint(20) unsigned NOT NULL DEFAULT 0,
`hp_price` decimal(10,2) DEFAULT NULL,
`updated_at` datetime NOT NULL DEFAULT \'0000-00-00 00:00:00\',
PRIMARY KEY (`id`),
UNIQUE KEY `post_id` (`post_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4'
);
// Create the errors log table so WPDO_Logger::error() can write to it.
$wpdb->query( 'DROP TABLE IF EXISTS `wp_itest_wpdo_errors`' );
$wpdb->query(
'CREATE TABLE `wp_itest_wpdo_errors` (
`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'
);
// Reset and populate the Schema Registry singleton.
$ref = new ReflectionClass( WPDO_Schema_Registry::class );
$inst = $ref->getProperty( 'instance' );
$inst->setAccessible( true );
$inst->setValue( null, null );
WPDO_Schema_Registry::instance()->register( 'test_provider', [
'post_type' => self::POST_TYPE,
'meta_key' => self::FIELD,
'zone' => 'hot',
'data_type' => 'decimal(10,2) NOT NULL DEFAULT 0',
'column' => self::FIELD,
'indexed' => false,
] );
}
public static function tearDownAfterClass(): void {
global $wpdb;
$wpdb->query( 'DROP TABLE IF EXISTS `' . self::TABLE . '`' );
$wpdb->query( 'DROP TABLE IF EXISTS `wp_itest_wpdo_errors`' );
}
protected function setUp(): void {
global $wpdb;
// Wipe data before each test.
$wpdb->query( 'TRUNCATE TABLE `' . self::TABLE . '`' );
// Reset FeatureFlags (clears option + static cache).
$GLOBALS['_wp_options'] = [];
WPDO_Feature_Flags::set( self::MODULE, 'idle' );
// Reset SyncBridge request-level field cache.
$ref = new ReflectionClass( WPDO_Sync_Bridge::class );
$cache = $ref->getProperty( 'field_cache' );
$cache->setAccessible( true );
$cache->setValue( null, [] );
// Reset $bypassing flag.
$bypass = $ref->getProperty( 'bypassing' );
$bypass->setAccessible( true );
$bypass->setValue( null, false );
// Seed post-type lookup.
$GLOBALS['_wp_post_types'] = [];
for ( $i = 1; $i <= 20; $i++ ) {
$GLOBALS['_wp_post_types'][ $i ] = self::POST_TYPE;
}
// Object cache reset.
$GLOBALS['_wp_cache'] = [];
$this->bridge = new WPDO_Sync_Bridge();
}
// ── intercept_update ─────────────────────────────────────────────────────
public function test_update_dual_write_writes_value_to_hot_table(): void {
WPDO_Feature_Flags::set( self::MODULE, 'dual_write' );
$this->bridge->intercept_update( null, 1, self::FIELD, '199.99', '' );
$val = WPDO_Zone_Hot::get( 1, self::POST_TYPE, self::FIELD );
$this->assertSame( '199.99', $val );
}
public function test_update_idle_does_not_write_to_hot_table(): void {
// Module stays in 'idle' — is_write_active() returns false.
$this->bridge->intercept_update( null, 2, self::FIELD, '50.00', '' );
$val = WPDO_Zone_Hot::get( 2, self::POST_TYPE, self::FIELD );
$this->assertNull( $val );
}
public function test_update_always_returns_null_to_allow_native_write(): void {
WPDO_Feature_Flags::set( self::MODULE, 'dual_write' );
$result = $this->bridge->intercept_update( null, 3, self::FIELD, '100.00', '' );
// Must return null (not short-circuit) so WordPress still writes postmeta.
$this->assertNull( $result );
}
public function test_update_skips_unregistered_meta_key(): void {
WPDO_Feature_Flags::set( self::MODULE, 'dual_write' );
// 'hp_unregistered' is not in Schema Registry.
$this->bridge->intercept_update( null, 4, 'hp_unregistered', '42.00', '' );
// Hot table for test_post should still be empty.
$val = WPDO_Zone_Hot::get( 4, self::POST_TYPE, self::FIELD );
$this->assertNull( $val );
}
public function test_update_skips_when_post_type_unknown(): void {
WPDO_Feature_Flags::set( self::MODULE, 'dual_write' );
// post_id 999 not seeded in _wp_post_types → get_post_type() returns false.
$this->bridge->intercept_update( null, 999, self::FIELD, '77.00', '' );
// Nothing should have been written (table doesn't have post 999).
$val = WPDO_Zone_Hot::get( 999, self::POST_TYPE, self::FIELD );
$this->assertNull( $val );
}
// ── intercept_add ────────────────────────────────────────────────────────
public function test_add_dual_write_writes_value_to_hot_table(): void {
WPDO_Feature_Flags::set( self::MODULE, 'dual_write' );
$this->bridge->intercept_add( null, 5, self::FIELD, '299.00', true );
$val = WPDO_Zone_Hot::get( 5, self::POST_TYPE, self::FIELD );
$this->assertSame( '299.00', $val );
}
public function test_add_returns_null_to_allow_native_write(): void {
WPDO_Feature_Flags::set( self::MODULE, 'dual_write' );
$result = $this->bridge->intercept_add( null, 6, self::FIELD, '10.00', false );
$this->assertNull( $result );
}
// ── intercept_get ────────────────────────────────────────────────────────
public function test_get_cutover_returns_zone_value_wrapped_in_array(): void {
// Write directly to hot table, then verify intercept_get reads it back.
WPDO_Zone_Hot::set( 7, self::POST_TYPE, self::FIELD, '500.00' );
WPDO_Feature_Flags::set( self::MODULE, 'cutover' );
$result = $this->bridge->intercept_get( null, 7, self::FIELD, true );
// SyncBridge wraps value in array so WP can unwrap correctly.
$this->assertIsArray( $result );
$this->assertSame( '500.00', $result[0] );
}
public function test_get_dual_write_returns_null_passthrough(): void {
WPDO_Zone_Hot::set( 8, self::POST_TYPE, self::FIELD, '123.00' );
// dual_write is NOT a read-custom state.
WPDO_Feature_Flags::set( self::MODULE, 'dual_write' );
$result = $this->bridge->intercept_get( null, 8, self::FIELD, true );
// Should pass through (return null) so WP reads from postmeta.
$this->assertNull( $result );
}
public function test_get_returns_null_when_no_zone_row(): void {
// cutover state but no row in hot table.
WPDO_Feature_Flags::set( self::MODULE, 'cutover' );
$result = $this->bridge->intercept_get( null, 9, self::FIELD, true );
$this->assertNull( $result );
}
public function test_get_returns_null_for_empty_meta_key(): void {
WPDO_Feature_Flags::set( self::MODULE, 'cutover' );
// Empty meta_key means "get all meta" — bridge should pass through.
$result = $this->bridge->intercept_get( null, 10, '', true );
$this->assertNull( $result );
}
// ── intercept_delete ─────────────────────────────────────────────────────
public function test_delete_zeros_out_hot_column(): void {
WPDO_Zone_Hot::set( 11, self::POST_TYPE, self::FIELD, '999.00' );
$this->assertSame( '999.00', WPDO_Zone_Hot::get( 11, self::POST_TYPE, self::FIELD ) );
WPDO_Feature_Flags::set( self::MODULE, 'dual_write' );
$this->bridge->intercept_delete( [ 1 ], 11, self::FIELD, '999.00' );
// delete_from_zone calls Zone_Hot::set(post_id, post_type, column, null).
$val = WPDO_Zone_Hot::get( 11, self::POST_TYPE, self::FIELD );
$this->assertNull( $val );
}
// ── $bypassing flag ───────────────────────────────────────────────────────
public function test_bypass_flag_prevents_intercept_get(): void {
WPDO_Zone_Hot::set( 12, self::POST_TYPE, self::FIELD, '777.00' );
WPDO_Feature_Flags::set( self::MODULE, 'cutover' );
// Simulate internal call (e.g. migration reading postmeta).
$ref = new ReflectionClass( WPDO_Sync_Bridge::class );
$bypass = $ref->getProperty( 'bypassing' );
$bypass->setAccessible( true );
$bypass->setValue( null, true );
$result = $this->bridge->intercept_get( null, 12, self::FIELD, true );
// Should pass through immediately, ignoring zone.
$this->assertNull( $result );
}
public function test_bypass_flag_prevents_intercept_update(): void {
WPDO_Feature_Flags::set( self::MODULE, 'dual_write' );
$ref = new ReflectionClass( WPDO_Sync_Bridge::class );
$bypass = $ref->getProperty( 'bypassing' );
$bypass->setAccessible( true );
$bypass->setValue( null, true );
$this->bridge->intercept_update( null, 13, self::FIELD, '888.00', '' );
// bypassing = true → no write to hot table.
$val = WPDO_Zone_Hot::get( 13, self::POST_TYPE, self::FIELD );
$this->assertNull( $val );
}
}