test: 補 HookBus 與 audit 寫入的 integration 回歸網(PR-I)
HookBusIntegrationTest(14 tests,自 A 移植) integration bootstrap 原本只載入 hook-bus-bridge、沒載入 hook-bus 本體, 所以連 WPDO_Hook_Bus alias 都建不出來 — 一併補 auto-promoter 與 hook-bus。 AuditLoggerIntegrationTest(4 tests,新寫) 直接鎖住上一個 commit 修掉的 fatal: - after_write 必須寫出一列 audit(覆蓋 Logger::trace_id() 缺失) - group_name / action 兩欄必須有值(SCHEMA_VERSION 2.1.0 新增) - trace_id 必須是 UUIDv4 且同一 request 內共用 - 未註冊的 key 不得寫入 tearDownAfterClass 卸掉 listener 而非 drop 表,否則後續測試類的受管寫入 會打到不存在的表;integration bootstrap 連帶補 remove_action() stub。 註:TermCommentBackfillTest 未移植 — 它 require HP AddOn 的 class-*-hivepress-term-comment-fields.php,屬 HP AddOn 測試套件而非核心。 unit 451 / integration 416 GREEN、PHPCS 0/0 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TbG1keQQ7XBa7qMQY16KCY
This commit is contained in:
@@ -0,0 +1,169 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Integration coverage for the audit log write path.
|
||||||
|
*
|
||||||
|
* Regression guard for a fatal that survived 451 unit + 398 integration tests
|
||||||
|
* and only surfaced on a live site: TMDO_Audit_Logger::write_row() calls
|
||||||
|
* TMDO_Logger::trace_id(), which was missing from the extracted core. The
|
||||||
|
* listener had never been registered (TMDO_Core did not call init()), so
|
||||||
|
* nothing exercised this path until that registration was restored.
|
||||||
|
*
|
||||||
|
* @package TMDO
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exercises wpdo_after_write → TMDO_Audit_Logger::on_write() → audit row.
|
||||||
|
*/
|
||||||
|
class AuditLoggerIntegrationTest extends TestCase {
|
||||||
|
|
||||||
|
private const TYPE = 'user';
|
||||||
|
private const GROUP = 'core_profile';
|
||||||
|
private const AUDIT_TABLE = 'wp_itest_wpdo_audit';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create the audit table and register the listener under test.
|
||||||
|
*/
|
||||||
|
public static function setUpBeforeClass(): void {
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
$base = WPDO_PLUGIN_DIR;
|
||||||
|
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-mode-manager.php',
|
||||||
|
'includes/engine/class-tmdo-cache-orchestrator.php',
|
||||||
|
'includes/adapters/class-tmdo-adapter-user.php',
|
||||||
|
'includes/integrations/class-tmdo-member-fields.php',
|
||||||
|
'includes/engine/class-tmdo-audit-logger.php',
|
||||||
|
'includes/engine/class-tmdo-hook-bus.php',
|
||||||
|
) as $file ) {
|
||||||
|
require_once $base . $file;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mirrors the wpdo_audit DDL in TMDO_Installer (SCHEMA_VERSION 2.1.0),
|
||||||
|
// including the group_name / action columns write_row() depends on.
|
||||||
|
$wpdb->query( 'DROP TABLE IF EXISTS `' . self::AUDIT_TABLE . '`' );
|
||||||
|
$wpdb->query(
|
||||||
|
'CREATE TABLE `' . self::AUDIT_TABLE . '` (
|
||||||
|
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
|
||||||
|
`ts` datetime NOT NULL DEFAULT \'0000-00-00 00:00:00\',
|
||||||
|
`user_id` bigint(20) unsigned NOT NULL DEFAULT 0,
|
||||||
|
`entity_type` varchar(20) NOT NULL DEFAULT \'\',
|
||||||
|
`entity_id` bigint(20) unsigned NOT NULL DEFAULT 0,
|
||||||
|
`group_name` varchar(50) NOT NULL DEFAULT \'\',
|
||||||
|
`meta_key` varchar(255) NOT NULL DEFAULT \'\',
|
||||||
|
`action` varchar(20) NOT NULL DEFAULT \'\',
|
||||||
|
`op` varchar(20) NOT NULL DEFAULT \'\',
|
||||||
|
`value_before` longtext,
|
||||||
|
`value_after` longtext,
|
||||||
|
`source` varchar(20) NOT NULL DEFAULT \'\',
|
||||||
|
`trace_id` varchar(36) NOT NULL DEFAULT \'\',
|
||||||
|
PRIMARY KEY (`id`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Groups only register once an adapter exists for the entity type.
|
||||||
|
WPDO_Entity_Registry::register_adapter( self::TYPE, new WPDO_Adapter_User() );
|
||||||
|
WPDO_Member_Fields::register_entity_fields();
|
||||||
|
WPDO_Audit_Logger::init();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detach the listener so later test classes' writes are not audited.
|
||||||
|
*
|
||||||
|
* The table is deliberately left in place: Audit_Logger::init() may already
|
||||||
|
* have been called elsewhere, and a dropped table would turn every later
|
||||||
|
* managed write into a DB error.
|
||||||
|
*/
|
||||||
|
public static function tearDownAfterClass(): void {
|
||||||
|
global $wpdb;
|
||||||
|
remove_action( 'wpdo_after_write', array( 'TMDO_Audit_Logger', 'on_write' ), 10 );
|
||||||
|
remove_action( 'wpdo_after_delete', array( 'TMDO_Audit_Logger', 'on_delete' ), 10 );
|
||||||
|
$wpdb->query( 'TRUNCATE TABLE `' . self::AUDIT_TABLE . '`' );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Empty the audit table before each case.
|
||||||
|
*/
|
||||||
|
protected function setUp(): void {
|
||||||
|
global $wpdb;
|
||||||
|
$wpdb->query( 'TRUNCATE TABLE `' . self::AUDIT_TABLE . '`' );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fire the action the Hook Bus emits after a managed write.
|
||||||
|
*
|
||||||
|
* @param string $meta_key Meta key.
|
||||||
|
* @param mixed $after New value.
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
private function fire_write( string $meta_key, $after ): void {
|
||||||
|
// Same argument order the Hook Bus uses (class-tmdo-hook-bus.php:114):
|
||||||
|
// entity_type, entity_id, meta_key, meta_value, upsert_result, op, before_value.
|
||||||
|
do_action( 'wpdo_after_write', self::TYPE, 42, $meta_key, $after, 1, 'update', null );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The listener must write a row without fataling — this is the regression
|
||||||
|
* guard for the missing TMDO_Logger::trace_id().
|
||||||
|
*/
|
||||||
|
public function test_after_write_inserts_audit_row(): void {
|
||||||
|
global $wpdb;
|
||||||
|
$this->fire_write( 'nickname', 'audit-test' );
|
||||||
|
|
||||||
|
$rows = (int) $wpdb->get_var( 'SELECT COUNT(*) FROM `' . self::AUDIT_TABLE . '`' );
|
||||||
|
$this->assertSame( 1, $rows, 'wpdo_after_write must produce exactly one audit row' );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* group_name / action are the two columns added in SCHEMA_VERSION 2.1.0;
|
||||||
|
* write_row() has always written them, so a missing column is a silent
|
||||||
|
* "Unknown column" failure.
|
||||||
|
*/
|
||||||
|
public function test_audit_row_populates_group_name_and_action(): void {
|
||||||
|
global $wpdb;
|
||||||
|
$this->fire_write( 'nickname', 'audit-test' );
|
||||||
|
|
||||||
|
$row = $wpdb->get_row( 'SELECT * FROM `' . self::AUDIT_TABLE . '` ORDER BY id DESC LIMIT 1', ARRAY_A );
|
||||||
|
$this->assertSame( self::TYPE, $row['entity_type'] );
|
||||||
|
$this->assertSame( 'nickname', $row['meta_key'] );
|
||||||
|
$this->assertSame( self::GROUP, $row['group_name'], 'group_name column must be populated' );
|
||||||
|
$this->assertSame( 'write', $row['action'], 'action column must be populated' );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* trace_id must be a UUIDv4 and stable within one request, so every row
|
||||||
|
* written by the same request can be correlated.
|
||||||
|
*/
|
||||||
|
public function test_trace_id_is_uuid_v4_and_shared_within_request(): void {
|
||||||
|
global $wpdb;
|
||||||
|
$this->fire_write( 'nickname', 'first' );
|
||||||
|
$this->fire_write( 'first_name', 'second' );
|
||||||
|
|
||||||
|
$ids = $wpdb->get_col( 'SELECT trace_id FROM `' . self::AUDIT_TABLE . '`' );
|
||||||
|
$this->assertCount( 2, $ids );
|
||||||
|
$this->assertMatchesRegularExpression(
|
||||||
|
'/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/',
|
||||||
|
$ids[0],
|
||||||
|
'trace_id must be a UUIDv4'
|
||||||
|
);
|
||||||
|
$this->assertSame( $ids[0], $ids[1], 'all rows in one request share a trace_id' );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unregistered keys carry no field definition and must not be audited.
|
||||||
|
*/
|
||||||
|
public function test_unregistered_key_is_not_audited(): void {
|
||||||
|
global $wpdb;
|
||||||
|
do_action( 'wpdo_after_write', self::TYPE, 42, 'not_registered_key', 'x', 1, 'update', null );
|
||||||
|
|
||||||
|
$rows = (int) $wpdb->get_var( 'SELECT COUNT(*) FROM `' . self::AUDIT_TABLE . '`' );
|
||||||
|
$this->assertSame( 0, $rows );
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,367 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
if ( ! defined( 'WPDO_CACHE_GROUP' ) ) {
|
||||||
|
define( 'WPDO_CACHE_GROUP', 'wpdo' );
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( ! function_exists( 'current_filter' ) ) {
|
||||||
|
/**
|
||||||
|
* Stub: returns $GLOBALS['_wp_current_filter'] so tests can set the simulated
|
||||||
|
* filter context before calling WPDO_Hook_Bus::intercept_update/delete.
|
||||||
|
*/
|
||||||
|
function current_filter(): string {
|
||||||
|
return $GLOBALS['_wp_current_filter'] ?? '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Integration test: WPDO_Hook_Bus intercept paths (P1-8 coverage).
|
||||||
|
*
|
||||||
|
* Verifies the three core interception methods (intercept_update, intercept_get,
|
||||||
|
* intercept_delete) against real MariaDB, covering:
|
||||||
|
* - aeav_only: flat write + EAV short-circuit (returns true)
|
||||||
|
* - dual_write: flat write + EAV passthrough (returns null)
|
||||||
|
* - disabled: full passthrough, no flat write
|
||||||
|
* - unregistered key: passthrough
|
||||||
|
* - direct_read: bypass-filter read path
|
||||||
|
* - cleanup_entity: row deletion on entity removal
|
||||||
|
*
|
||||||
|
* current_filter() is stubbed via $GLOBALS['_wp_current_filter'] — tests set
|
||||||
|
* this before calling intercept_update/delete to simulate the WP filter context
|
||||||
|
* that would normally be provided by the metadata filter pipeline.
|
||||||
|
*/
|
||||||
|
class HookBusIntegrationTest extends TestCase {
|
||||||
|
|
||||||
|
private const TYPE = 'user';
|
||||||
|
private const GROUP = 'core_profile';
|
||||||
|
private const FLAT_TABLE = 'wp_itest_wpdo_user_core_profile';
|
||||||
|
private const ERRORS_TABLE = 'wp_itest_wpdo_errors';
|
||||||
|
|
||||||
|
// ── Fixture lifecycle ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
public static function setUpBeforeClass(): void {
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
$base = WPDO_PLUGIN_DIR;
|
||||||
|
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-mode-manager.php',
|
||||||
|
'includes/engine/class-tmdo-cache-orchestrator.php',
|
||||||
|
'includes/adapters/class-tmdo-adapter-user.php',
|
||||||
|
'includes/integrations/class-tmdo-member-fields.php',
|
||||||
|
'includes/engine/class-tmdo-hook-bus.php',
|
||||||
|
) as $file ) {
|
||||||
|
require_once $base . $file;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear stale Schema_Manager cache entries for tables we manage.
|
||||||
|
// We only remove the specific entry rather than replacing the entire cache
|
||||||
|
// to avoid corrupting cached state that other test classes depend on.
|
||||||
|
$ref = new ReflectionClass( WPDO_Schema_Manager::class );
|
||||||
|
$cache = $ref->getProperty( 'table_exists_cache' );
|
||||||
|
$cache->setAccessible( true );
|
||||||
|
$existing = $cache->getValue( null );
|
||||||
|
unset( $existing[ self::FLAT_TABLE ] );
|
||||||
|
$cache->setValue( null, $existing );
|
||||||
|
|
||||||
|
// Errors table (required by WPDO_Logger).
|
||||||
|
$wpdb->query( 'DROP TABLE IF EXISTS `' . self::ERRORS_TABLE . '`' );
|
||||||
|
$wpdb->query(
|
||||||
|
'CREATE TABLE `' . self::ERRORS_TABLE . '` (
|
||||||
|
`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'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Flat table for user core_profile.
|
||||||
|
$wpdb->query( 'DROP TABLE IF EXISTS `' . self::FLAT_TABLE . '`' );
|
||||||
|
$wpdb->query(
|
||||||
|
'CREATE TABLE `' . self::FLAT_TABLE . '` (
|
||||||
|
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
|
||||||
|
`user_id` bigint(20) NOT NULL,
|
||||||
|
`nickname` varchar(255) DEFAULT NULL,
|
||||||
|
`first_name` varchar(255) DEFAULT NULL,
|
||||||
|
`last_name` varchar(255) DEFAULT NULL,
|
||||||
|
`description` text 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_user` (`user_id`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Mark table as known-good in Schema_Manager cache (add, don't replace).
|
||||||
|
$existing2 = $cache->getValue( null ) ?? array();
|
||||||
|
$existing2[ self::FLAT_TABLE ] = true;
|
||||||
|
$cache->setValue( null, $existing2 );
|
||||||
|
|
||||||
|
// Register adapter + all user field groups (direct call — add_action is a no-op
|
||||||
|
// in the integration test environment, so ::register() would not fire).
|
||||||
|
WPDO_Entity_Registry::init();
|
||||||
|
WPDO_Entity_Registry::register_adapter( self::TYPE, new WPDO_Adapter_User() );
|
||||||
|
WPDO_Member_Fields::register_entity_fields();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function tearDownAfterClass(): void {
|
||||||
|
global $wpdb;
|
||||||
|
$wpdb->query( 'DROP TABLE IF EXISTS `' . self::FLAT_TABLE . '`' );
|
||||||
|
$wpdb->query( 'DROP TABLE IF EXISTS `' . self::ERRORS_TABLE . '`' );
|
||||||
|
|
||||||
|
// Reset Mode_Manager + Entity_Registry so this test class doesn't leak
|
||||||
|
// user mode state into subsequent integration test classes.
|
||||||
|
$mref = new ReflectionClass( WPDO_Mode_Manager::class );
|
||||||
|
$mcache = $mref->getProperty( 'cache' );
|
||||||
|
$mcache->setAccessible( true );
|
||||||
|
$mcache->setValue( null, null );
|
||||||
|
|
||||||
|
WPDO_Entity_Registry::init();
|
||||||
|
|
||||||
|
// Clear Schema_Manager table_exists_cache entirely so that the `false`
|
||||||
|
// entries written by cleanup_entity (for non-existent group tables) don't
|
||||||
|
// trick subsequent test classes into thinking those tables don't exist after
|
||||||
|
// they create them.
|
||||||
|
$sref = new ReflectionClass( WPDO_Schema_Manager::class );
|
||||||
|
$scache = $sref->getProperty( 'table_exists_cache' );
|
||||||
|
$scache->setAccessible( true );
|
||||||
|
$scache->setValue( null, array() );
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function setUp(): void {
|
||||||
|
global $wpdb;
|
||||||
|
$wpdb->query( 'TRUNCATE TABLE `' . self::FLAT_TABLE . '`' );
|
||||||
|
|
||||||
|
$GLOBALS['_wp_options'] = array();
|
||||||
|
$GLOBALS['_wp_cache'] = array();
|
||||||
|
$GLOBALS['_wp_current_filter'] = '';
|
||||||
|
|
||||||
|
// Default: user mode = aeav_only (most tests use this).
|
||||||
|
self::set_user_mode( WPDO_Mode_Manager::MODE_AEAV_ONLY );
|
||||||
|
|
||||||
|
// Clear Cache_Orchestrator L1.
|
||||||
|
$ref = new ReflectionClass( WPDO_Cache_Orchestrator::class );
|
||||||
|
$l1 = $ref->getProperty( 'l1_cache' );
|
||||||
|
$l1->setAccessible( true );
|
||||||
|
$l1->setValue( null, array() );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inject user mode into Mode_Manager's static cache (bypasses wp_options
|
||||||
|
* and Cache_Orchestrator dependency of Mode_Manager::set()).
|
||||||
|
*/
|
||||||
|
private static function set_user_mode( string $mode ): void {
|
||||||
|
$ref = new ReflectionClass( WPDO_Mode_Manager::class );
|
||||||
|
$cache = $ref->getProperty( 'cache' );
|
||||||
|
$cache->setAccessible( true );
|
||||||
|
$cache->setValue( null, array(
|
||||||
|
'user' => $mode,
|
||||||
|
'post' => 'disabled',
|
||||||
|
'term' => 'dual_write',
|
||||||
|
'comment' => 'dual_write',
|
||||||
|
) );
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Helpers ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private function read_flat_first_name( int $user_id ): ?string {
|
||||||
|
global $wpdb;
|
||||||
|
return $wpdb->get_var(
|
||||||
|
$wpdb->prepare(
|
||||||
|
'SELECT `first_name` FROM `' . self::FLAT_TABLE . '` WHERE `user_id` = %d',
|
||||||
|
$user_id
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Tests: intercept_update ───────────────────────────────────────────────
|
||||||
|
|
||||||
|
public function test_update_writes_to_flat_in_aeav_only_mode(): void {
|
||||||
|
$GLOBALS['_wp_current_filter'] = 'update_user_metadata';
|
||||||
|
|
||||||
|
WPDO_Hook_Bus::intercept_update( null, 1, 'first_name', 'Alice', '' );
|
||||||
|
|
||||||
|
$this->assertSame( 'Alice', $this->read_flat_first_name( 1 ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_update_returns_true_in_aeav_only_mode(): void {
|
||||||
|
$GLOBALS['_wp_current_filter'] = 'update_user_metadata';
|
||||||
|
|
||||||
|
$result = WPDO_Hook_Bus::intercept_update( null, 2, 'first_name', 'Bob', '' );
|
||||||
|
|
||||||
|
$this->assertTrue( $result, 'aeav_only must return true to short-circuit EAV write' );
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_update_writes_flat_and_returns_null_in_dual_write_mode(): void {
|
||||||
|
self::set_user_mode( WPDO_Mode_Manager::MODE_DUAL_WRITE );
|
||||||
|
$GLOBALS['_wp_current_filter'] = 'update_user_metadata';
|
||||||
|
|
||||||
|
$result = WPDO_Hook_Bus::intercept_update( null, 3, 'first_name', 'Carol', '' );
|
||||||
|
|
||||||
|
$this->assertSame( 'Carol', $this->read_flat_first_name( 3 ) );
|
||||||
|
$this->assertNull( $result, 'dual_write must return null so WP continues to write EAV' );
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_update_passthrough_for_unregistered_key(): void {
|
||||||
|
$GLOBALS['_wp_current_filter'] = 'update_user_metadata';
|
||||||
|
|
||||||
|
$sentinel = new stdClass();
|
||||||
|
$result = WPDO_Hook_Bus::intercept_update( $sentinel, 4, '_nonexistent_key_xyz', 'x', '' );
|
||||||
|
|
||||||
|
$this->assertSame( $sentinel, $result, 'unregistered key must return $check unchanged' );
|
||||||
|
$this->assertNull( $this->read_flat_first_name( 4 ), 'unregistered key must not write flat table' );
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_update_passthrough_in_disabled_mode(): void {
|
||||||
|
self::set_user_mode( WPDO_Mode_Manager::MODE_DISABLED );
|
||||||
|
$GLOBALS['_wp_current_filter'] = 'update_user_metadata';
|
||||||
|
|
||||||
|
$result = WPDO_Hook_Bus::intercept_update( null, 5, 'first_name', 'Dave', '' );
|
||||||
|
|
||||||
|
$this->assertNull( $result, 'disabled mode must return $check unchanged' );
|
||||||
|
$this->assertNull( $this->read_flat_first_name( 5 ), 'disabled mode must not write flat table' );
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_update_passthrough_when_filter_context_unresolvable(): void {
|
||||||
|
$GLOBALS['_wp_current_filter'] = ''; // no active filter
|
||||||
|
|
||||||
|
$sentinel = new stdClass();
|
||||||
|
$result = WPDO_Hook_Bus::intercept_update( $sentinel, 6, 'first_name', 'Eve', '' );
|
||||||
|
|
||||||
|
$this->assertSame( $sentinel, $result, 'unknown filter context must return $check unchanged' );
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Tests: intercept_get ─────────────────────────────────────────────────
|
||||||
|
|
||||||
|
public function test_get_reads_from_flat_in_aeav_only_mode(): void {
|
||||||
|
// Seed the flat table directly.
|
||||||
|
global $wpdb;
|
||||||
|
$wpdb->query(
|
||||||
|
$wpdb->prepare(
|
||||||
|
'INSERT INTO `' . self::FLAT_TABLE . '` (`user_id`, `first_name`) VALUES (%d, %s)',
|
||||||
|
10,
|
||||||
|
'Frank'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// intercept_get accepts $meta_type as 5th arg — bypasses current_filter().
|
||||||
|
$result = WPDO_Hook_Bus::intercept_get( null, 10, 'first_name', true, 'user' );
|
||||||
|
|
||||||
|
$this->assertSame( array( 'Frank' ), $result, 'intercept_get must return [value] array in aeav_only mode' );
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_get_passthrough_in_dual_write_mode(): void {
|
||||||
|
self::set_user_mode( WPDO_Mode_Manager::MODE_DUAL_WRITE );
|
||||||
|
|
||||||
|
$result = WPDO_Hook_Bus::intercept_get( null, 11, 'first_name', true, 'user' );
|
||||||
|
|
||||||
|
$this->assertNull( $result, 'dual_write does not read from flat — must return $check' );
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_get_returns_empty_string_when_column_is_null(): void {
|
||||||
|
global $wpdb;
|
||||||
|
$wpdb->query(
|
||||||
|
$wpdb->prepare(
|
||||||
|
'INSERT INTO `' . self::FLAT_TABLE . '` (`user_id`, `first_name`) VALUES (%d, NULL)',
|
||||||
|
12
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
$result = WPDO_Hook_Bus::intercept_get( null, 12, 'first_name', true, 'user' );
|
||||||
|
|
||||||
|
$this->assertSame( '', $result );
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Tests: intercept_delete ───────────────────────────────────────────────
|
||||||
|
|
||||||
|
public function test_delete_clears_column_to_null_in_aeav_only_mode(): void {
|
||||||
|
global $wpdb;
|
||||||
|
$wpdb->query(
|
||||||
|
$wpdb->prepare(
|
||||||
|
'INSERT INTO `' . self::FLAT_TABLE . '` (`user_id`, `first_name`) VALUES (%d, %s)',
|
||||||
|
20,
|
||||||
|
'Grace'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
$GLOBALS['_wp_current_filter'] = 'delete_user_metadata';
|
||||||
|
|
||||||
|
WPDO_Hook_Bus::intercept_delete( null, 20, 'first_name', '', false );
|
||||||
|
|
||||||
|
$this->assertNull( $this->read_flat_first_name( 20 ), 'delete must set column to NULL (not drop row)' );
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_delete_returns_true_in_aeav_only_mode(): void {
|
||||||
|
global $wpdb;
|
||||||
|
$wpdb->query(
|
||||||
|
$wpdb->prepare(
|
||||||
|
'INSERT INTO `' . self::FLAT_TABLE . '` (`user_id`, `first_name`) VALUES (%d, %s)',
|
||||||
|
21,
|
||||||
|
'Hank'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
$GLOBALS['_wp_current_filter'] = 'delete_user_metadata';
|
||||||
|
|
||||||
|
$result = WPDO_Hook_Bus::intercept_delete( null, 21, 'first_name', '', false );
|
||||||
|
|
||||||
|
$this->assertTrue( $result, 'aeav_only delete must return true to short-circuit EAV delete' );
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Tests: direct_read ───────────────────────────────────────────────────
|
||||||
|
|
||||||
|
public function test_direct_read_returns_written_value(): void {
|
||||||
|
$GLOBALS['_wp_current_filter'] = 'update_user_metadata';
|
||||||
|
WPDO_Hook_Bus::intercept_update( null, 30, 'first_name', 'Ivan', '' );
|
||||||
|
|
||||||
|
// Flush cache so direct_read goes to DB.
|
||||||
|
$ref = new ReflectionClass( WPDO_Cache_Orchestrator::class );
|
||||||
|
$l1 = $ref->getProperty( 'l1_cache' );
|
||||||
|
$l1->setAccessible( true );
|
||||||
|
$l1->setValue( null, array() );
|
||||||
|
$GLOBALS['_wp_cache'] = array();
|
||||||
|
|
||||||
|
$value = WPDO_Hook_Bus::direct_read( 'user', 30, 'first_name' );
|
||||||
|
|
||||||
|
$this->assertSame( 'Ivan', $value );
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_direct_read_returns_null_for_unknown_key(): void {
|
||||||
|
$value = WPDO_Hook_Bus::direct_read( 'user', 31, '_nonexistent_key_xyz' );
|
||||||
|
|
||||||
|
$this->assertNull( $value );
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Tests: cleanup_entity ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
public function test_cleanup_entity_removes_flat_rows(): void {
|
||||||
|
global $wpdb;
|
||||||
|
$wpdb->query(
|
||||||
|
$wpdb->prepare(
|
||||||
|
'INSERT INTO `' . self::FLAT_TABLE . '` (`user_id`, `first_name`) VALUES (%d, %s)',
|
||||||
|
40,
|
||||||
|
'Judy'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
WPDO_Hook_Bus::cleanup_entity( 'user', 40 );
|
||||||
|
|
||||||
|
$count = (int) $wpdb->get_var(
|
||||||
|
$wpdb->prepare(
|
||||||
|
'SELECT COUNT(*) FROM `' . self::FLAT_TABLE . '` WHERE `user_id` = %d',
|
||||||
|
40
|
||||||
|
)
|
||||||
|
);
|
||||||
|
$this->assertSame( 0, $count, 'cleanup_entity must delete all flat rows for the entity' );
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -257,6 +257,11 @@ if ( ! function_exists( 'remove_filter' ) ) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if ( ! function_exists( 'remove_action' ) ) {
|
||||||
|
function remove_action( string $hook, $cb, int $p = 10 ): bool {
|
||||||
|
return remove_filter( $hook, $cb, $p );
|
||||||
|
}
|
||||||
|
}
|
||||||
if ( ! function_exists( 'apply_filters' ) ) {
|
if ( ! function_exists( 'apply_filters' ) ) {
|
||||||
function apply_filters( string $hook, $value, ...$args ) {
|
function apply_filters( string $hook, $value, ...$args ) {
|
||||||
foreach ( $GLOBALS['_wp_filter_callbacks'][ $hook ] ?? [] as $cb ) {
|
foreach ( $GLOBALS['_wp_filter_callbacks'][ $hook ] ?? [] as $cb ) {
|
||||||
@@ -570,6 +575,8 @@ require_once TMDO_PATH . 'includes/engine/class-tmdo-schema-manager.php';
|
|||||||
require_once TMDO_PATH . 'includes/engine/class-tmdo-entity-registry.php';
|
require_once TMDO_PATH . 'includes/engine/class-tmdo-entity-registry.php';
|
||||||
require_once TMDO_PATH . 'includes/engine/class-tmdo-entity-migration-engine.php';
|
require_once TMDO_PATH . 'includes/engine/class-tmdo-entity-migration-engine.php';
|
||||||
require_once TMDO_PATH . 'includes/engine/class-tmdo-entity-health.php';
|
require_once TMDO_PATH . 'includes/engine/class-tmdo-entity-health.php';
|
||||||
|
require_once TMDO_PATH . 'includes/engine/class-tmdo-auto-promoter.php';
|
||||||
|
require_once TMDO_PATH . 'includes/engine/class-tmdo-hook-bus.php';
|
||||||
require_once TMDO_PATH . 'includes/adapters/interface-entity-adapter.php';
|
require_once TMDO_PATH . 'includes/adapters/interface-entity-adapter.php';
|
||||||
require_once TMDO_PATH . 'includes/adapters/class-tmdo-adapter-post.php';
|
require_once TMDO_PATH . 'includes/adapters/class-tmdo-adapter-post.php';
|
||||||
require_once TMDO_PATH . 'includes/adapters/class-tmdo-adapter-user.php';
|
require_once TMDO_PATH . 'includes/adapters/class-tmdo-adapter-user.php';
|
||||||
|
|||||||
Reference in New Issue
Block a user