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 ); } }