d36bb954d1
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
273 lines
10 KiB
PHP
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 );
|
|
}
|
|
}
|