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 );
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user