test: 加入 integration 測試套件(連真實 MariaDB)
從 wp-data-optimizer v3.4.6 移植 5 個 integration 測試檔,並新增 tests/integration/bootstrap.php(複用核心的 integration bootstrap,不重複 與 WP stub)+ phpunit-integration.xml + CI integration job。
This commit is contained in:
@@ -0,0 +1,197 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
// Class loading is handled by tests/integration/bootstrap.php, which requires
|
||||
// the core plugin's integration bootstrap and then this AddOn's own files.
|
||||
|
||||
/**
|
||||
* Integration tests for the HivePress family integration.
|
||||
*
|
||||
* Exercises against real MariaDB (via tests/integration/bootstrap.php) the
|
||||
* scenarios that unit tests can only stub:
|
||||
*
|
||||
* - Shadow table CREATE actually works with the declared expected_columns
|
||||
* - UNIQUE constraint on (user_id, listing_id) really prevents dup favorites
|
||||
* - mirror_insert() roundtrips through real $wpdb (SQL syntax valid)
|
||||
* - 7-state FSM lifecycle moves WPDO_Feature_Flags correctly
|
||||
*
|
||||
* @group integration
|
||||
*/
|
||||
class HivepressIntegrationTest extends TestCase {
|
||||
|
||||
/** Test-scope shadow table for favorites. */
|
||||
private const TABLE_FAVORITE = 'wp_itest_wpdo_comment_hp_favorite';
|
||||
|
||||
/** Test-scope shadow table for messages. */
|
||||
private const TABLE_MESSAGE = 'wp_itest_wpdo_comment_hp_message';
|
||||
|
||||
public static function setUpBeforeClass(): void {
|
||||
// HivePress integration files are loaded at file-top level so the
|
||||
// Testable* subclass definitions below can resolve their parents at
|
||||
// parse time. Schema setup happens here.
|
||||
global $wpdb;
|
||||
$wpdb->query( 'DROP TABLE IF EXISTS `' . self::TABLE_FAVORITE . '`' );
|
||||
$wpdb->query( "CREATE TABLE `" . self::TABLE_FAVORITE . "` (
|
||||
comment_id BIGINT UNSIGNED NOT NULL PRIMARY KEY,
|
||||
user_id BIGINT UNSIGNED NOT NULL,
|
||||
listing_id BIGINT UNSIGNED NOT NULL,
|
||||
created_at DATETIME NOT NULL,
|
||||
UNIQUE KEY unique_user_listing (user_id, listing_id),
|
||||
KEY idx_listing (listing_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4" );
|
||||
|
||||
$wpdb->query( 'DROP TABLE IF EXISTS `' . self::TABLE_MESSAGE . '`' );
|
||||
$wpdb->query( "CREATE TABLE `" . self::TABLE_MESSAGE . "` (
|
||||
comment_id BIGINT UNSIGNED NOT NULL PRIMARY KEY,
|
||||
sender_id BIGINT UNSIGNED NOT NULL,
|
||||
recipient_id BIGINT UNSIGNED NOT NULL,
|
||||
listing_id BIGINT UNSIGNED NOT NULL,
|
||||
is_read TINYINT(1) NOT NULL DEFAULT 0,
|
||||
sent_at DATETIME NOT NULL,
|
||||
KEY idx_recipient_unread (recipient_id, is_read),
|
||||
KEY idx_thread (listing_id, sent_at)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4" );
|
||||
}
|
||||
|
||||
public static function tearDownAfterClass(): void {
|
||||
global $wpdb;
|
||||
$wpdb->query( 'DROP TABLE IF EXISTS `' . self::TABLE_FAVORITE . '`' );
|
||||
$wpdb->query( 'DROP TABLE IF EXISTS `' . self::TABLE_MESSAGE . '`' );
|
||||
}
|
||||
|
||||
protected function setUp(): void {
|
||||
global $wpdb;
|
||||
$wpdb->query( 'TRUNCATE TABLE `' . self::TABLE_FAVORITE . '`' );
|
||||
$wpdb->query( 'TRUNCATE TABLE `' . self::TABLE_MESSAGE . '`' );
|
||||
// Clear FSM module state between tests.
|
||||
$GLOBALS['_wp_options']['wpdo_features'] = array();
|
||||
}
|
||||
|
||||
// ── Schema tests ────────────────────────────────────────────────────────
|
||||
|
||||
public function test_favorite_table_has_unique_user_listing_constraint(): void {
|
||||
global $wpdb;
|
||||
$row = $wpdb->get_row( 'SHOW CREATE TABLE `' . self::TABLE_FAVORITE . '`', 'ARRAY_A' );
|
||||
$this->assertNotNull( $row );
|
||||
$ddl = $row['Create Table'] ?? '';
|
||||
$this->assertStringContainsString( 'UNIQUE KEY', $ddl );
|
||||
$this->assertStringContainsString( 'unique_user_listing', $ddl );
|
||||
}
|
||||
|
||||
public function test_message_table_has_recipient_unread_index(): void {
|
||||
global $wpdb;
|
||||
$row = $wpdb->get_row( 'SHOW CREATE TABLE `' . self::TABLE_MESSAGE . '`', 'ARRAY_A' );
|
||||
$this->assertNotNull( $row );
|
||||
$ddl = $row['Create Table'] ?? '';
|
||||
$this->assertStringContainsString( 'idx_recipient_unread', $ddl );
|
||||
$this->assertStringContainsString( 'recipient_id', $ddl );
|
||||
}
|
||||
|
||||
// ── mirror_insert real roundtrip ────────────────────────────────────────
|
||||
|
||||
public function test_favorite_mirror_insert_writes_real_row(): void {
|
||||
global $wpdb;
|
||||
$adapter = new WPDO_HivePress_Favorites_Adapter();
|
||||
|
||||
$comment = new stdClass();
|
||||
$comment->comment_type = 'hp_favorite';
|
||||
$comment->user_id = 11;
|
||||
$comment->comment_post_ID = 222;
|
||||
$comment->comment_date = '2026-05-04 12:00:00';
|
||||
|
||||
$adapter->mirror_insert( 1001, $comment );
|
||||
|
||||
$rows = (int) $wpdb->get_var( 'SELECT COUNT(*) FROM `' . self::TABLE_FAVORITE . '`' );
|
||||
$this->assertSame( 1, $rows );
|
||||
|
||||
$saved = $wpdb->get_row( 'SELECT * FROM `' . self::TABLE_FAVORITE . '` WHERE comment_id = 1001', 'ARRAY_A' );
|
||||
$this->assertSame( '11', (string) $saved['user_id'] );
|
||||
$this->assertSame( '222', (string) $saved['listing_id'] );
|
||||
}
|
||||
|
||||
public function test_favorite_unique_constraint_blocks_double_tap(): void {
|
||||
global $wpdb;
|
||||
$adapter = new WPDO_HivePress_Favorites_Adapter();
|
||||
|
||||
// Two different comment_ids but same user+listing — UNIQUE should win.
|
||||
$comment = new stdClass();
|
||||
$comment->comment_type = 'hp_favorite';
|
||||
$comment->user_id = 7;
|
||||
$comment->comment_post_ID = 99;
|
||||
$comment->comment_date = '2026-05-04 12:00:00';
|
||||
|
||||
$adapter->mirror_insert( 2001, $comment );
|
||||
$adapter->mirror_insert( 2002, $comment ); // Race-style duplicate.
|
||||
|
||||
$rows = (int) $wpdb->get_var( 'SELECT COUNT(*) FROM `' . self::TABLE_FAVORITE . '` WHERE user_id = 7 AND listing_id = 99' );
|
||||
$this->assertSame( 1, $rows, 'UNIQUE(user_id, listing_id) must reject duplicate.' );
|
||||
}
|
||||
|
||||
public function test_message_mirror_insert_promotes_recipient_id(): void {
|
||||
global $wpdb;
|
||||
$adapter = new WPDO_HivePress_Messages_Adapter();
|
||||
|
||||
$comment = new stdClass();
|
||||
$comment->comment_type = 'hp_message';
|
||||
$comment->user_id = 5; // sender
|
||||
$comment->comment_karma = 42; // recipient (HP hack)
|
||||
$comment->comment_post_ID = 99;
|
||||
$comment->comment_approved = 0;
|
||||
$comment->comment_date = '2026-05-04 12:00:00';
|
||||
|
||||
$adapter->mirror_insert( 3001, $comment );
|
||||
|
||||
$saved = $wpdb->get_row( 'SELECT * FROM `' . self::TABLE_MESSAGE . '` WHERE comment_id = 3001', 'ARRAY_A' );
|
||||
$this->assertNotNull( $saved );
|
||||
$this->assertSame( '42', (string) $saved['recipient_id'] );
|
||||
$this->assertSame( '5', (string) $saved['sender_id'] );
|
||||
|
||||
// recipient_unread index supports the dashboard query.
|
||||
$unread_count = (int) $wpdb->get_var( 'SELECT COUNT(*) FROM `' . self::TABLE_MESSAGE . '` WHERE recipient_id = 42 AND is_read = 0' );
|
||||
$this->assertSame( 1, $unread_count );
|
||||
}
|
||||
|
||||
// ── FSM lifecycle ───────────────────────────────────────────────────────
|
||||
|
||||
public function test_fsm_lifecycle_progression(): void {
|
||||
// Replicates a typical migration: idle → dual_write → backfill → ...
|
||||
// → cleanup → complete. Each step verified via Feature_Flags state.
|
||||
WPDO_Feature_Flags::set( 'comment_hp_favorite', 'idle' );
|
||||
$this->assertSame( 'idle', WPDO_Feature_Flags::get( 'comment_hp_favorite' ) );
|
||||
$this->assertFalse( WPDO_Feature_Flags::is_query_active( 'comment_hp_favorite' ) );
|
||||
|
||||
foreach ( array( 'dual_write', 'backfill', 'verify' ) as $state ) {
|
||||
WPDO_Feature_Flags::set( 'comment_hp_favorite', $state );
|
||||
// Reads still go to legacy until cutover.
|
||||
$this->assertFalse(
|
||||
WPDO_Feature_Flags::is_query_active( 'comment_hp_favorite' ),
|
||||
"State $state should not be query-active yet"
|
||||
);
|
||||
}
|
||||
|
||||
WPDO_Feature_Flags::set( 'comment_hp_favorite', 'cutover' );
|
||||
$this->assertTrue( WPDO_Feature_Flags::is_query_active( 'comment_hp_favorite' ) );
|
||||
|
||||
WPDO_Feature_Flags::set( 'comment_hp_favorite', 'cleanup' );
|
||||
$this->assertTrue( WPDO_Feature_Flags::is_query_active( 'comment_hp_favorite' ) );
|
||||
|
||||
WPDO_Feature_Flags::set( 'comment_hp_favorite', 'complete' );
|
||||
$this->assertTrue( WPDO_Feature_Flags::is_query_active( 'comment_hp_favorite' ) );
|
||||
}
|
||||
|
||||
public function test_cron_optimizer_respects_module_state(): void {
|
||||
WPDO_HivePress_Cron_Optimizer::reset_for_tests();
|
||||
// Module idle → optimizer must not query.
|
||||
WPDO_Feature_Flags::set( 'hot_hp_listing', 'idle' );
|
||||
$count = WPDO_HivePress_Cron_Optimizer::maybe_run();
|
||||
$this->assertSame( 0, $count, 'Optimizer must short-circuit when module is idle.' );
|
||||
}
|
||||
}
|
||||
|
||||
// Production adapters use `$wpdb->prefix . self::TABLE` to compose the full
|
||||
// table name. Integration test bootstrap sets prefix to `wp_itest_`, so the
|
||||
// production mirror_insert() naturally writes to wp_itest_wpdo_comment_hp_*.
|
||||
// No subclassing or test-double needed — the production code path is exercised
|
||||
// directly against real MariaDB with isolated prefix.
|
||||
@@ -0,0 +1,131 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* Integration test: WPDO_Hivepress_Term_Comment_Fields (v2.12.2 Phase 2).
|
||||
*
|
||||
* Verifies that the field group registration:
|
||||
* - Hooks correctly into wpdo_register_entity_fields action
|
||||
* - Calls register_group() exactly once per group
|
||||
* - Registers expected keys with correct types/searchable flags
|
||||
* - is_managed() returns true for registered keys
|
||||
* - is_managed() returns false for unregistered keys (pass-through)
|
||||
*
|
||||
* Schema_Manager auto-creation of flat tables is exercised live in dev10
|
||||
* smoke tests (see CHANGELOG); these tests focus on registry contract.
|
||||
*/
|
||||
class HivepressTermCommentFieldsTest extends TestCase {
|
||||
|
||||
public static function setUpBeforeClass(): void {
|
||||
// Load adapter interface + term/comment adapters so register_group
|
||||
// passes the adapter-presence guard (line 102 of entity-registry.php).
|
||||
|
||||
// Register adapters once for the whole test class (idempotent —
|
||||
// register_adapter overwrites if entity_type already present).
|
||||
WPDO_Entity_Registry::register_adapter( 'term', new WPDO_Adapter_Term() );
|
||||
WPDO_Entity_Registry::register_adapter( 'comment', new WPDO_Adapter_Comment() );
|
||||
|
||||
// Register field groups once. register_group is guarded against
|
||||
// duplicate registration (line 108 of entity-registry.php), so
|
||||
// re-running register_entity_fields() in subsequent tests is no-op.
|
||||
WPDO_Hivepress_Term_Comment_Fields::register_entity_fields();
|
||||
}
|
||||
|
||||
protected function setUp(): void {
|
||||
// Re-run registration for idempotency tests; no-op for already-registered groups.
|
||||
WPDO_Hivepress_Term_Comment_Fields::register_entity_fields();
|
||||
}
|
||||
|
||||
// ── Registration ────────────────────────────────────────────────────────
|
||||
|
||||
public function test_term_hp_taxonomy_group_registered(): void {
|
||||
$keys = WPDO_Entity_Registry::get_group_keys( 'term', 'hp_taxonomy' );
|
||||
$this->assertContains( 'hp_sort_order', $keys );
|
||||
$this->assertContains( 'hp_default', $keys );
|
||||
$this->assertContains( 'hp_icon', $keys );
|
||||
$this->assertCount( 3, $keys );
|
||||
}
|
||||
|
||||
public function test_comment_hp_review_group_registered(): void {
|
||||
$keys = WPDO_Entity_Registry::get_group_keys( 'comment', 'hp_review' );
|
||||
$this->assertContains( 'hp_rating', $keys );
|
||||
$this->assertCount( 1, $keys );
|
||||
}
|
||||
|
||||
// ── is_managed contract ─────────────────────────────────────────────────
|
||||
|
||||
public function test_term_hp_keys_are_managed(): void {
|
||||
$this->assertTrue( WPDO_Entity_Registry::is_managed( 'term', 'hp_sort_order' ) );
|
||||
$this->assertTrue( WPDO_Entity_Registry::is_managed( 'term', 'hp_default' ) );
|
||||
$this->assertTrue( WPDO_Entity_Registry::is_managed( 'term', 'hp_icon' ) );
|
||||
}
|
||||
|
||||
public function test_comment_hp_keys_are_managed(): void {
|
||||
$this->assertTrue( WPDO_Entity_Registry::is_managed( 'comment', 'hp_rating' ) );
|
||||
}
|
||||
|
||||
public function test_unregistered_term_keys_pass_through(): void {
|
||||
$this->assertFalse( WPDO_Entity_Registry::is_managed( 'term', 'note_group' ) );
|
||||
$this->assertFalse( WPDO_Entity_Registry::is_managed( 'term', 'product_count_product_cat' ) );
|
||||
$this->assertFalse( WPDO_Entity_Registry::is_managed( 'term', '_wxr_import_user' ) );
|
||||
}
|
||||
|
||||
public function test_unregistered_comment_keys_pass_through(): void {
|
||||
$this->assertFalse( WPDO_Entity_Registry::is_managed( 'comment', 'note_group' ) );
|
||||
$this->assertFalse( WPDO_Entity_Registry::is_managed( 'comment', '_wxr_import_user' ) );
|
||||
}
|
||||
|
||||
public function test_term_keys_not_treated_as_comment_keys(): void {
|
||||
// hp_sort_order is term-only; comment side should not see it as managed.
|
||||
$this->assertFalse( WPDO_Entity_Registry::is_managed( 'comment', 'hp_sort_order' ) );
|
||||
}
|
||||
|
||||
public function test_comment_keys_not_treated_as_term_keys(): void {
|
||||
// hp_rating is comment-only; term side should not see it as managed.
|
||||
$this->assertFalse( WPDO_Entity_Registry::is_managed( 'term', 'hp_rating' ) );
|
||||
}
|
||||
|
||||
// ── Field metadata contract ─────────────────────────────────────────────
|
||||
|
||||
public function test_hp_sort_order_field_metadata(): void {
|
||||
$field = WPDO_Entity_Registry::get_field( 'term', 'hp_sort_order' );
|
||||
$this->assertNotNull( $field );
|
||||
$this->assertSame( 'hp_sort_order', $field['key'] );
|
||||
$this->assertSame( 'integer', $field['type'] );
|
||||
$this->assertTrue( ! empty( $field['searchable'] ) );
|
||||
}
|
||||
|
||||
public function test_hp_rating_field_metadata(): void {
|
||||
$field = WPDO_Entity_Registry::get_field( 'comment', 'hp_rating' );
|
||||
$this->assertNotNull( $field );
|
||||
$this->assertSame( 'hp_rating', $field['key'] );
|
||||
$this->assertSame( 'integer', $field['type'] );
|
||||
$this->assertTrue( ! empty( $field['searchable'] ) );
|
||||
}
|
||||
|
||||
public function test_hp_default_is_enum_with_options(): void {
|
||||
$field = WPDO_Entity_Registry::get_field( 'term', 'hp_default' );
|
||||
$this->assertSame( 'enum', $field['type'] );
|
||||
$this->assertSame( array( '0', '1' ), $field['options'] ?? array() );
|
||||
}
|
||||
|
||||
// ── get_field returns null for unregistered ─────────────────────────────
|
||||
|
||||
public function test_get_field_returns_null_for_unregistered(): void {
|
||||
$this->assertNull( WPDO_Entity_Registry::get_field( 'term', 'unregistered_key' ) );
|
||||
$this->assertNull( WPDO_Entity_Registry::get_field( 'comment', 'unregistered_key' ) );
|
||||
}
|
||||
|
||||
// ── Bootstrap idempotency ───────────────────────────────────────────────
|
||||
|
||||
public function test_register_entity_fields_is_idempotent(): void {
|
||||
// Re-running registration should not duplicate fields.
|
||||
WPDO_Hivepress_Term_Comment_Fields::register_entity_fields();
|
||||
WPDO_Hivepress_Term_Comment_Fields::register_entity_fields();
|
||||
|
||||
$keys = WPDO_Entity_Registry::get_group_keys( 'term', 'hp_taxonomy' );
|
||||
$this->assertCount( 3, $keys, 'Re-registration must not duplicate keys' );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,252 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* Integration test: WPDO_Hivepress_Transient_Filter (v2.11.5).
|
||||
*
|
||||
* Verifies the filter contract:
|
||||
* - is_target_key() correctly identifies _transient_hp_* / _transient_timeout_hp_*
|
||||
* - non-target keys (regular meta, foreign-prefix transients) are NOT touched
|
||||
* - translate_key() namespaces by post_id and md5-hashes the key
|
||||
* - count/purge legacy SQL helpers correctly target HivePress transient rows only
|
||||
* - register/load chain doesn't cause syntax/load failures
|
||||
*
|
||||
* Filter callbacks (on_read/on_add/on_update/on_delete) are called via the
|
||||
* WordPress metadata filter chain. Since the test bootstrap stubs
|
||||
* `update_post_meta` / `get_post_meta` to bypass the filter chain entirely
|
||||
* (they go directly to the test wp_itest_postmeta table or $GLOBALS), we
|
||||
* test the callback functions directly with synthetic filter args.
|
||||
*/
|
||||
class HivepressTransientFilterTest extends TestCase {
|
||||
|
||||
private const POSTMETA = 'wp_itest_postmeta';
|
||||
|
||||
public static function setUpBeforeClass(): void {
|
||||
global $wpdb;
|
||||
|
||||
// Ensure the postmeta table exists for purge SQL tests.
|
||||
$wpdb->query(
|
||||
'CREATE TABLE IF NOT EXISTS `' . self::POSTMETA . '` (
|
||||
meta_id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
|
||||
post_id bigint(20) unsigned NOT NULL DEFAULT 0,
|
||||
meta_key varchar(255) DEFAULT NULL,
|
||||
meta_value longtext,
|
||||
PRIMARY KEY (meta_id),
|
||||
KEY post_id (post_id),
|
||||
KEY meta_key (meta_key(191))
|
||||
) DEFAULT CHARACTER SET utf8mb4'
|
||||
);
|
||||
}
|
||||
|
||||
protected function setUp(): void {
|
||||
global $wpdb;
|
||||
$wpdb->query( 'TRUNCATE TABLE `' . self::POSTMETA . '`' );
|
||||
|
||||
// Reset wp_options state for filter tests
|
||||
$GLOBALS['_wp_options'] = array();
|
||||
|
||||
// Default the toggle to enabled
|
||||
update_option( WPDO_Hivepress_Transient_Filter::OPT_ENABLED, '1' );
|
||||
}
|
||||
|
||||
// ── is_target_key() ─────────────────────────────────────────────────────
|
||||
|
||||
public function test_is_target_key_matches_hp_transient_value(): void {
|
||||
$this->assertTrue( WPDO_Hivepress_Transient_Filter::is_target_key( '_transient_hp_models/listing_category/version' ) );
|
||||
}
|
||||
|
||||
public function test_is_target_key_matches_hp_transient_timeout(): void {
|
||||
$this->assertTrue( WPDO_Hivepress_Transient_Filter::is_target_key( '_transient_timeout_hp_models/term/listing_availability/version' ) );
|
||||
}
|
||||
|
||||
public function test_is_target_key_rejects_non_hp_transient(): void {
|
||||
$this->assertFalse( WPDO_Hivepress_Transient_Filter::is_target_key( '_transient_other_plugin_cache' ) );
|
||||
$this->assertFalse( WPDO_Hivepress_Transient_Filter::is_target_key( '_transient_timeout_other' ) );
|
||||
}
|
||||
|
||||
public function test_is_target_key_rejects_regular_meta(): void {
|
||||
$this->assertFalse( WPDO_Hivepress_Transient_Filter::is_target_key( 'hp_price' ) );
|
||||
$this->assertFalse( WPDO_Hivepress_Transient_Filter::is_target_key( '_thumbnail_id' ) );
|
||||
}
|
||||
|
||||
public function test_is_target_key_rejects_non_string(): void {
|
||||
$this->assertFalse( WPDO_Hivepress_Transient_Filter::is_target_key( null ) );
|
||||
$this->assertFalse( WPDO_Hivepress_Transient_Filter::is_target_key( 123 ) );
|
||||
$this->assertFalse( WPDO_Hivepress_Transient_Filter::is_target_key( array() ) );
|
||||
}
|
||||
|
||||
// ── translate_key() ─────────────────────────────────────────────────────
|
||||
|
||||
public function test_translate_key_namespaces_by_post_id(): void {
|
||||
$key1 = WPDO_Hivepress_Transient_Filter::translate_key( 100, '_transient_hp_models/cat/v1' );
|
||||
$key2 = WPDO_Hivepress_Transient_Filter::translate_key( 200, '_transient_hp_models/cat/v1' );
|
||||
|
||||
$this->assertNotSame( $key1, $key2, 'Same meta_key on different posts must yield different translated keys.' );
|
||||
$this->assertStringContainsString( 'wpdo_hp_pm_100_', $key1 );
|
||||
$this->assertStringContainsString( 'wpdo_hp_pm_200_', $key2 );
|
||||
}
|
||||
|
||||
public function test_translate_key_strips_transient_prefix(): void {
|
||||
$value_key = WPDO_Hivepress_Transient_Filter::translate_key( 100, '_transient_hp_models/cat' );
|
||||
$timeout_key = WPDO_Hivepress_Transient_Filter::translate_key( 100, '_transient_timeout_hp_models/cat' );
|
||||
|
||||
// Value and timeout SHOULD share the same translated suffix (md5 of stripped name)
|
||||
// since they refer to the same logical cache entry.
|
||||
$this->assertSame( $value_key, $timeout_key );
|
||||
}
|
||||
|
||||
public function test_translate_key_md5_handles_long_input(): void {
|
||||
$long_key = '_transient_hp_models/term/listing_availability/' . str_repeat( 'a', 200 );
|
||||
$translated = WPDO_Hivepress_Transient_Filter::translate_key( 1, $long_key );
|
||||
|
||||
// Result must fit within wp_options.option_name (172 chars used as proxy).
|
||||
$option_name = '_transient_timeout_' . $translated;
|
||||
$this->assertLessThanOrEqual( 191, strlen( $option_name ), 'Translated option_name must fit MySQL VARCHAR(191).' );
|
||||
}
|
||||
|
||||
// ── on_read() callback ──────────────────────────────────────────────────
|
||||
|
||||
public function test_on_read_returns_pre_for_non_target_key(): void {
|
||||
$result = WPDO_Hivepress_Transient_Filter::on_read( null, 100, '_thumbnail_id', true );
|
||||
$this->assertNull( $result, 'Non-target keys must pass through (return $pre).' );
|
||||
}
|
||||
|
||||
public function test_on_read_returns_value_from_wp_options_when_present(): void {
|
||||
// Seed a "translated" wp_options entry as if filter previously wrote it.
|
||||
$key = '_transient_hp_models/listing_category/version';
|
||||
$translated = WPDO_Hivepress_Transient_Filter::translate_key( 100, $key );
|
||||
update_option( '_transient_' . $translated, 'cached-value-xyz' );
|
||||
|
||||
$result = WPDO_Hivepress_Transient_Filter::on_read( null, 100, $key, true );
|
||||
$this->assertSame( array( 'cached-value-xyz' ), $result );
|
||||
}
|
||||
|
||||
public function test_on_read_returns_pre_on_cache_miss(): void {
|
||||
// No wp_options entry seeded.
|
||||
$result = WPDO_Hivepress_Transient_Filter::on_read( null, 100, '_transient_hp_models/cat/v1', true );
|
||||
$this->assertNull( $result, 'Cache miss should fall through (return $pre = null).' );
|
||||
}
|
||||
|
||||
// ── on_add() / on_update() callbacks ────────────────────────────────────
|
||||
|
||||
public function test_on_add_writes_to_wp_options_and_short_circuits(): void {
|
||||
$key = '_transient_hp_models/cat/v1';
|
||||
$result = WPDO_Hivepress_Transient_Filter::on_add( null, 100, $key, 'value-xyz', false );
|
||||
|
||||
$this->assertTrue( $result, 'Filter must short-circuit (return truthy).' );
|
||||
|
||||
$translated = WPDO_Hivepress_Transient_Filter::translate_key( 100, $key );
|
||||
$this->assertSame( 'value-xyz', get_option( '_transient_' . $translated ) );
|
||||
}
|
||||
|
||||
public function test_on_update_writes_to_wp_options(): void {
|
||||
$key = '_transient_hp_models/cat/v1';
|
||||
WPDO_Hivepress_Transient_Filter::on_update( null, 200, $key, 'updated-value', '' );
|
||||
|
||||
$translated = WPDO_Hivepress_Transient_Filter::translate_key( 200, $key );
|
||||
$this->assertSame( 'updated-value', get_option( '_transient_' . $translated ) );
|
||||
}
|
||||
|
||||
public function test_on_update_handles_timeout_separately(): void {
|
||||
$value_key = '_transient_hp_models/cat/v1';
|
||||
$timeout_key = '_transient_timeout_hp_models/cat/v1';
|
||||
|
||||
WPDO_Hivepress_Transient_Filter::on_update( null, 100, $value_key, 'val', '' );
|
||||
WPDO_Hivepress_Transient_Filter::on_update( null, 100, $timeout_key, '99999999', '' );
|
||||
|
||||
$translated = WPDO_Hivepress_Transient_Filter::translate_key( 100, $value_key );
|
||||
$this->assertSame( 'val', get_option( '_transient_' . $translated ) );
|
||||
$this->assertSame( '99999999', get_option( '_transient_timeout_' . $translated ) );
|
||||
}
|
||||
|
||||
public function test_on_add_passes_through_non_target_keys(): void {
|
||||
$result = WPDO_Hivepress_Transient_Filter::on_add( null, 100, '_thumbnail_id', '50', false );
|
||||
$this->assertNull( $result );
|
||||
// And nothing was written to wp_options
|
||||
$this->assertFalse( get_option( '_transient_wpdo_hp_pm_100_' . md5( '' ) ) );
|
||||
}
|
||||
|
||||
// ── on_delete() callback ────────────────────────────────────────────────
|
||||
|
||||
public function test_on_delete_clears_wp_options_entry(): void {
|
||||
$key = '_transient_hp_models/cat/v1';
|
||||
WPDO_Hivepress_Transient_Filter::on_update( null, 100, $key, 'value', '' );
|
||||
$translated = WPDO_Hivepress_Transient_Filter::translate_key( 100, $key );
|
||||
$this->assertSame( 'value', get_option( '_transient_' . $translated ) );
|
||||
|
||||
WPDO_Hivepress_Transient_Filter::on_delete( null, 100, $key, '', false );
|
||||
$this->assertFalse( get_option( '_transient_' . $translated ) );
|
||||
}
|
||||
|
||||
public function test_on_delete_passes_through_non_target_keys(): void {
|
||||
$result = WPDO_Hivepress_Transient_Filter::on_delete( null, 100, 'hp_price', '', false );
|
||||
$this->assertNull( $result );
|
||||
}
|
||||
|
||||
// ── Round-trip: write then read ─────────────────────────────────────────
|
||||
|
||||
public function test_full_round_trip_write_then_read(): void {
|
||||
$key = '_transient_hp_models/listing_category/abc123';
|
||||
WPDO_Hivepress_Transient_Filter::on_update( null, 500, $key, array( 'serialized', 'data' ), '' );
|
||||
|
||||
$value = WPDO_Hivepress_Transient_Filter::on_read( null, 500, $key, true );
|
||||
$this->assertSame( array( array( 'serialized', 'data' ) ), $value );
|
||||
}
|
||||
|
||||
// ── count/purge legacy ──────────────────────────────────────────────────
|
||||
|
||||
public function test_count_legacy_postmeta_rows_returns_zero_when_clean(): void {
|
||||
$this->assertSame( 0, WPDO_Hivepress_Transient_Filter::count_legacy_postmeta_rows() );
|
||||
}
|
||||
|
||||
public function test_count_legacy_postmeta_rows_counts_hp_transients_only(): void {
|
||||
global $wpdb;
|
||||
// Insert a mix of rows: target hp transients + non-target rows.
|
||||
$rows = array(
|
||||
array( 1, '_transient_hp_models/cat/v1', 'val1' ),
|
||||
array( 1, '_transient_timeout_hp_models/cat/v1', '99999' ),
|
||||
array( 2, '_transient_hp_models/tag/v2', 'val2' ),
|
||||
array( 1, 'hp_price', '100' ),
|
||||
array( 1, '_thumbnail_id', '50' ),
|
||||
array( 2, '_transient_other_plugin', 'foreign' ), // non-hp transient
|
||||
);
|
||||
foreach ( $rows as $r ) {
|
||||
$wpdb->insert( self::POSTMETA, array(
|
||||
'post_id' => $r[0],
|
||||
'meta_key' => $r[1],
|
||||
'meta_value' => $r[2],
|
||||
) );
|
||||
}
|
||||
|
||||
$count = WPDO_Hivepress_Transient_Filter::count_legacy_postmeta_rows();
|
||||
$this->assertSame( 3, $count, 'Must count only _transient_hp_* rows, not other-plugin transients or normal meta.' );
|
||||
}
|
||||
|
||||
public function test_purge_legacy_postmeta_rows_deletes_only_hp_transients(): void {
|
||||
global $wpdb;
|
||||
$wpdb->insert( self::POSTMETA, array( 'post_id' => 1, 'meta_key' => '_transient_hp_models/cat/v1', 'meta_value' => 'a' ) );
|
||||
$wpdb->insert( self::POSTMETA, array( 'post_id' => 1, 'meta_key' => '_transient_timeout_hp_models/cat/v1', 'meta_value' => '99' ) );
|
||||
$wpdb->insert( self::POSTMETA, array( 'post_id' => 1, 'meta_key' => 'hp_price', 'meta_value' => '50' ) );
|
||||
$wpdb->insert( self::POSTMETA, array( 'post_id' => 2, 'meta_key' => '_transient_woo_thing', 'meta_value' => 'foreign' ) );
|
||||
|
||||
$deleted = WPDO_Hivepress_Transient_Filter::purge_legacy_postmeta_rows();
|
||||
$this->assertSame( 2, $deleted );
|
||||
|
||||
$remaining = (int) $wpdb->get_var( 'SELECT COUNT(*) FROM `' . self::POSTMETA . '`' );
|
||||
$this->assertSame( 2, $remaining, 'hp_price + _transient_woo_thing must remain.' );
|
||||
}
|
||||
|
||||
// ── is_enabled() toggle ─────────────────────────────────────────────────
|
||||
|
||||
public function test_is_enabled_defaults_true(): void {
|
||||
delete_option( WPDO_Hivepress_Transient_Filter::OPT_ENABLED );
|
||||
$this->assertTrue( WPDO_Hivepress_Transient_Filter::is_enabled() );
|
||||
}
|
||||
|
||||
public function test_is_enabled_respects_zero_value(): void {
|
||||
update_option( WPDO_Hivepress_Transient_Filter::OPT_ENABLED, '0' );
|
||||
$this->assertFalse( WPDO_Hivepress_Transient_Filter::is_enabled() );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* Integration tests for WPDO_Listing_Meta_Interceptor — PR-3 R-2 + P-C1 fixes.
|
||||
*
|
||||
* Verifies:
|
||||
* 1. R-2: Interceptor SKIPS hook registration when hpct_listing_meta is missing
|
||||
* (prevents the "wp_hpct_listing_meta doesn't exist" cascading errors that
|
||||
* forced 2meet-infocards to bypass the interceptor in production).
|
||||
* 2. P-C1: upsert_meta() uses 1 SQL round-trip via WPDO_DB::upsert().
|
||||
*
|
||||
* @covers WPDO_Listing_Meta_Interceptor
|
||||
*/
|
||||
class ListingMetaInterceptorTest extends TestCase {
|
||||
|
||||
/**
|
||||
* R-2: When hpct_listing_meta table is missing, register_hooks() must skip
|
||||
* gracefully and write a single explanatory error log entry.
|
||||
*
|
||||
* @runInSeparateProcess
|
||||
*/
|
||||
public function test_register_hooks_skips_when_table_missing(): void {
|
||||
global $wpdb;
|
||||
|
||||
$wpdb->query( "DROP TABLE IF EXISTS `{$wpdb->prefix}hpct_listing_meta`" );
|
||||
|
||||
// Simulate HPCT_Core being present — register_hooks() only logs the skip
|
||||
// when HPCT is loaded but its table is missing (genuine inconsistency).
|
||||
if ( ! class_exists( 'HPCT_Core' ) ) {
|
||||
// phpcs:ignore Generic.Commenting.InlineComment.InvalidEndChar -- runtime stub
|
||||
eval( 'class HPCT_Core {}' ); // @phpcs:ignore
|
||||
}
|
||||
|
||||
// Ensure wpdo_errors exists for the skip log; harmless if pre-created.
|
||||
$wpdb->query(
|
||||
"CREATE TABLE IF NOT EXISTS `{$wpdb->prefix}wpdo_errors` (
|
||||
id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
|
||||
module varchar(50) NOT NULL DEFAULT '',
|
||||
zone varchar(10) NOT NULL DEFAULT '',
|
||||
hook varchar(255) NOT NULL DEFAULT '',
|
||||
message longtext NOT NULL,
|
||||
context longtext,
|
||||
created_at datetime NOT NULL DEFAULT '0000-00-00 00:00:00',
|
||||
PRIMARY KEY (id),
|
||||
KEY idx_module (module)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4"
|
||||
);
|
||||
|
||||
// Snapshot pre-call count of skip messages.
|
||||
$pre = (int) $wpdb->get_var(
|
||||
$wpdb->prepare(
|
||||
"SELECT COUNT(*) FROM `{$wpdb->prefix}wpdo_errors` WHERE module = %s AND hook = %s",
|
||||
'listing_meta',
|
||||
'register_hooks'
|
||||
)
|
||||
);
|
||||
|
||||
$interceptor = new WPDO_Listing_Meta_Interceptor();
|
||||
$interceptor->register_hooks();
|
||||
|
||||
$post = (int) $wpdb->get_var(
|
||||
$wpdb->prepare(
|
||||
"SELECT COUNT(*) FROM `{$wpdb->prefix}wpdo_errors` WHERE module = %s AND hook = %s",
|
||||
'listing_meta',
|
||||
'register_hooks'
|
||||
)
|
||||
);
|
||||
$this->assertSame( $pre + 1, $post, 'Expected exactly one new skip message after register_hooks()' );
|
||||
}
|
||||
|
||||
/**
|
||||
* P-C1: WPDO_DB::upsert() must produce an INSERT ... ON DUPLICATE KEY UPDATE
|
||||
* statement (single round-trip), and writes must succeed via the composite
|
||||
* unique key (listing_id, meta_key).
|
||||
*
|
||||
* @runInSeparateProcess
|
||||
*/
|
||||
public function test_upsert_uses_composite_unique_key(): void {
|
||||
global $wpdb;
|
||||
|
||||
$table = $wpdb->prefix . 'hpct_listing_meta';
|
||||
|
||||
// Clean slate.
|
||||
$wpdb->query( "DROP TABLE IF EXISTS `{$table}`" );
|
||||
$wpdb->query(
|
||||
"CREATE TABLE `{$table}` (
|
||||
id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
|
||||
listing_id bigint(20) unsigned NOT NULL DEFAULT 0,
|
||||
meta_key varchar(255) NOT NULL DEFAULT '',
|
||||
meta_value longtext,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY ui_listing_meta (listing_id, meta_key(191))
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4"
|
||||
);
|
||||
|
||||
// First upsert: INSERT.
|
||||
$result1 = WPDO_DB::upsert(
|
||||
$table,
|
||||
array(
|
||||
'listing_id' => 4242,
|
||||
'meta_key' => 'hp_price',
|
||||
'meta_value' => '100.00',
|
||||
),
|
||||
array( 'meta_value' ),
|
||||
array( 'listing_id', 'meta_key' ),
|
||||
array( '%d', '%s', '%s' )
|
||||
);
|
||||
$this->assertNotFalse( $result1 );
|
||||
|
||||
// Second upsert: UPDATE via the composite key.
|
||||
$result2 = WPDO_DB::upsert(
|
||||
$table,
|
||||
array(
|
||||
'listing_id' => 4242,
|
||||
'meta_key' => 'hp_price',
|
||||
'meta_value' => '250.50',
|
||||
),
|
||||
array( 'meta_value' ),
|
||||
array( 'listing_id', 'meta_key' ),
|
||||
array( '%d', '%s', '%s' )
|
||||
);
|
||||
$this->assertNotFalse( $result2 );
|
||||
|
||||
// Verify only one row exists for (4242, 'hp_price') with the latest value.
|
||||
$rows = $wpdb->get_results(
|
||||
$wpdb->prepare(
|
||||
"SELECT meta_value FROM `{$table}` WHERE listing_id = %d AND meta_key = %s",
|
||||
4242,
|
||||
'hp_price'
|
||||
),
|
||||
ARRAY_A
|
||||
);
|
||||
$this->assertCount( 1, $rows, 'Composite UNIQUE must collapse two upserts into a single row' );
|
||||
$this->assertSame( '250.50', $rows[0]['meta_value'] );
|
||||
|
||||
// Cleanup.
|
||||
$wpdb->query( "DROP TABLE IF EXISTS `{$table}`" );
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify table_exists() returns false when table missing, true after CREATE.
|
||||
*
|
||||
* @runInSeparateProcess
|
||||
*/
|
||||
public function test_table_exists_detection(): void {
|
||||
global $wpdb;
|
||||
|
||||
$wpdb->query( "DROP TABLE IF EXISTS `{$wpdb->prefix}hpct_listing_meta`" );
|
||||
|
||||
// Use reflection to invoke private static.
|
||||
$ref = new ReflectionClass( WPDO_Listing_Meta_Interceptor::class );
|
||||
$method = $ref->getMethod( 'table_exists' );
|
||||
$method->setAccessible( true );
|
||||
|
||||
$this->assertFalse( $method->invoke( null ), 'table_exists must return false when table missing' );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,247 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* Integration test: WPDO_Term_Comment_Backfill (v2.12.6 Phase 6).
|
||||
*
|
||||
* Verifies pivot-style backfill from wp_*meta to flat tables:
|
||||
* - dry_run reports candidate count without writing
|
||||
* - confirm path performs INSERT ... ON DUPLICATE KEY UPDATE
|
||||
* - multi-key pivot collapses N rows per term into 1 flat row
|
||||
* - idempotent re-run does not duplicate
|
||||
* - empty DB returns zero counts
|
||||
* - error returns for invalid group / missing registry
|
||||
*/
|
||||
class TermCommentBackfillTest extends TestCase {
|
||||
|
||||
private const TERMS = 'wp_itest_terms';
|
||||
private const TERMMETA = 'wp_itest_termmeta';
|
||||
private const COMMENTS = 'wp_itest_comments';
|
||||
private const COMMENTMETA = 'wp_itest_commentmeta';
|
||||
private const TERM_FLAT = 'wp_itest_wpdo_term_hp_taxonomy';
|
||||
private const COMMENT_FLAT = 'wp_itest_wpdo_comment_hp_review';
|
||||
|
||||
public static function setUpBeforeClass(): void {
|
||||
global $wpdb;
|
||||
|
||||
$wpdb->terms = self::TERMS;
|
||||
$wpdb->termmeta = self::TERMMETA;
|
||||
$wpdb->comments = self::COMMENTS;
|
||||
$wpdb->commentmeta = self::COMMENTMETA;
|
||||
|
||||
WPDO_Entity_Registry::register_adapter( 'term', new WPDO_Adapter_Term() );
|
||||
WPDO_Entity_Registry::register_adapter( 'comment', new WPDO_Adapter_Comment() );
|
||||
WPDO_Hivepress_Term_Comment_Fields::register_entity_fields();
|
||||
|
||||
// Test tables.
|
||||
$wpdb->query( 'DROP TABLE IF EXISTS `' . self::TERMS . '`' );
|
||||
$wpdb->query(
|
||||
'CREATE TABLE `' . self::TERMS . '` (
|
||||
term_id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
|
||||
name varchar(200) NOT NULL DEFAULT "",
|
||||
PRIMARY KEY (term_id)
|
||||
) DEFAULT CHARACTER SET utf8mb4'
|
||||
);
|
||||
|
||||
$wpdb->query( 'DROP TABLE IF EXISTS `' . self::COMMENTS . '`' );
|
||||
$wpdb->query(
|
||||
'CREATE TABLE `' . self::COMMENTS . '` (
|
||||
comment_ID bigint(20) unsigned NOT NULL AUTO_INCREMENT,
|
||||
comment_post_ID bigint(20) unsigned NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (comment_ID)
|
||||
) DEFAULT CHARACTER SET utf8mb4'
|
||||
);
|
||||
|
||||
$wpdb->query( 'CREATE TABLE IF NOT EXISTS `' . self::TERMMETA . '` (
|
||||
meta_id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
|
||||
term_id bigint(20) unsigned NOT NULL DEFAULT 0,
|
||||
meta_key varchar(255) DEFAULT NULL,
|
||||
meta_value longtext,
|
||||
PRIMARY KEY (meta_id),
|
||||
KEY term_id (term_id),
|
||||
KEY meta_key (meta_key(191))
|
||||
) DEFAULT CHARACTER SET utf8mb4' );
|
||||
|
||||
$wpdb->query( 'CREATE TABLE IF NOT EXISTS `' . self::COMMENTMETA . '` (
|
||||
meta_id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
|
||||
comment_id bigint(20) unsigned NOT NULL DEFAULT 0,
|
||||
meta_key varchar(255) DEFAULT NULL,
|
||||
meta_value longtext,
|
||||
PRIMARY KEY (meta_id),
|
||||
KEY comment_id (comment_id),
|
||||
KEY meta_key (meta_key(191))
|
||||
) DEFAULT CHARACTER SET utf8mb4' );
|
||||
|
||||
$wpdb->query( 'DROP TABLE IF EXISTS `' . self::TERM_FLAT . '`' );
|
||||
$wpdb->query(
|
||||
'CREATE TABLE `' . self::TERM_FLAT . '` (
|
||||
term_id bigint(20) unsigned NOT NULL,
|
||||
hp_sort_order int(11) DEFAULT NULL,
|
||||
hp_default tinyint(1) DEFAULT NULL,
|
||||
hp_icon varchar(64) DEFAULT NULL,
|
||||
PRIMARY KEY (term_id)
|
||||
) DEFAULT CHARACTER SET utf8mb4'
|
||||
);
|
||||
|
||||
$wpdb->query( 'DROP TABLE IF EXISTS `' . self::COMMENT_FLAT . '`' );
|
||||
$wpdb->query(
|
||||
'CREATE TABLE `' . self::COMMENT_FLAT . '` (
|
||||
comment_id bigint(20) unsigned NOT NULL,
|
||||
hp_rating tinyint(1) DEFAULT NULL,
|
||||
PRIMARY KEY (comment_id)
|
||||
) DEFAULT CHARACTER SET utf8mb4'
|
||||
);
|
||||
}
|
||||
|
||||
public static function tearDownAfterClass(): void {
|
||||
global $wpdb;
|
||||
foreach ( array( self::TERMS, self::COMMENTS, self::TERM_FLAT, self::COMMENT_FLAT ) as $tbl ) {
|
||||
$wpdb->query( 'DROP TABLE IF EXISTS `' . $tbl . '`' );
|
||||
}
|
||||
}
|
||||
|
||||
protected function setUp(): void {
|
||||
global $wpdb;
|
||||
$wpdb->query( 'TRUNCATE TABLE `' . self::TERMS . '`' );
|
||||
$wpdb->query( 'TRUNCATE TABLE `' . self::COMMENTS . '`' );
|
||||
$wpdb->query( 'TRUNCATE TABLE `' . self::TERMMETA . '`' );
|
||||
$wpdb->query( 'TRUNCATE TABLE `' . self::COMMENTMETA . '`' );
|
||||
$wpdb->query( 'TRUNCATE TABLE `' . self::TERM_FLAT . '`' );
|
||||
$wpdb->query( 'TRUNCATE TABLE `' . self::COMMENT_FLAT . '`' );
|
||||
}
|
||||
|
||||
// ── backfill_group ──────────────────────────────────────────────────────
|
||||
|
||||
public function test_backfill_returns_zero_counts_for_empty_db(): void {
|
||||
$result = WPDO_Term_Comment_Backfill::backfill_group( 'term', 'hp_taxonomy', false );
|
||||
$this->assertSame( 0, $result['candidates'] );
|
||||
$this->assertSame( 0, $result['written'] );
|
||||
}
|
||||
|
||||
public function test_dry_run_does_not_write(): void {
|
||||
global $wpdb;
|
||||
$wpdb->insert( self::TERMS, array( 'term_id' => 1, 'name' => 'a' ) );
|
||||
$wpdb->insert( self::TERMMETA, array( 'term_id' => 1, 'meta_key' => 'hp_sort_order', 'meta_value' => '5' ) );
|
||||
|
||||
$result = WPDO_Term_Comment_Backfill::backfill_group( 'term', 'hp_taxonomy', true );
|
||||
$this->assertSame( 1, $result['candidates'] );
|
||||
$this->assertSame( 0, $result['written'] );
|
||||
$this->assertTrue( $result['dry_run'] );
|
||||
|
||||
$flat_count = (int) $wpdb->get_var( 'SELECT COUNT(*) FROM `' . self::TERM_FLAT . '`' );
|
||||
$this->assertSame( 0, $flat_count, 'dry_run must not INSERT' );
|
||||
}
|
||||
|
||||
public function test_backfill_pivots_multi_key_into_single_row(): void {
|
||||
global $wpdb;
|
||||
$wpdb->insert( self::TERMS, array( 'term_id' => 1, 'name' => 'a' ) );
|
||||
$wpdb->insert( self::TERMMETA, array( 'term_id' => 1, 'meta_key' => 'hp_sort_order', 'meta_value' => '5' ) );
|
||||
$wpdb->insert( self::TERMMETA, array( 'term_id' => 1, 'meta_key' => 'hp_default', 'meta_value' => '1' ) );
|
||||
$wpdb->insert( self::TERMMETA, array( 'term_id' => 1, 'meta_key' => 'hp_icon', 'meta_value' => 'fa-star' ) );
|
||||
|
||||
$result = WPDO_Term_Comment_Backfill::backfill_group( 'term', 'hp_taxonomy', false );
|
||||
$this->assertSame( 1, $result['candidates'] );
|
||||
|
||||
$row = $wpdb->get_row( 'SELECT * FROM `' . self::TERM_FLAT . '` WHERE term_id = 1', ARRAY_A );
|
||||
$this->assertNotNull( $row );
|
||||
$this->assertSame( '5', (string) $row['hp_sort_order'] );
|
||||
$this->assertSame( '1', (string) $row['hp_default'] );
|
||||
$this->assertSame( 'fa-star', $row['hp_icon'] );
|
||||
}
|
||||
|
||||
public function test_backfill_handles_multiple_terms(): void {
|
||||
global $wpdb;
|
||||
for ( $i = 1; $i <= 5; $i++ ) {
|
||||
$wpdb->insert( self::TERMS, array( 'term_id' => $i, 'name' => 'term_' . $i ) );
|
||||
$wpdb->insert( self::TERMMETA, array( 'term_id' => $i, 'meta_key' => 'hp_sort_order', 'meta_value' => $i * 10 ) );
|
||||
}
|
||||
|
||||
$result = WPDO_Term_Comment_Backfill::backfill_group( 'term', 'hp_taxonomy', false );
|
||||
$this->assertSame( 5, $result['candidates'] );
|
||||
|
||||
$count = (int) $wpdb->get_var( 'SELECT COUNT(*) FROM `' . self::TERM_FLAT . '`' );
|
||||
$this->assertSame( 5, $count );
|
||||
}
|
||||
|
||||
public function test_backfill_idempotent_on_re_run(): void {
|
||||
global $wpdb;
|
||||
$wpdb->insert( self::TERMS, array( 'term_id' => 1, 'name' => 'a' ) );
|
||||
$wpdb->insert( self::TERMMETA, array( 'term_id' => 1, 'meta_key' => 'hp_sort_order', 'meta_value' => '5' ) );
|
||||
|
||||
WPDO_Term_Comment_Backfill::backfill_group( 'term', 'hp_taxonomy', false );
|
||||
WPDO_Term_Comment_Backfill::backfill_group( 'term', 'hp_taxonomy', false );
|
||||
|
||||
$count = (int) $wpdb->get_var( 'SELECT COUNT(*) FROM `' . self::TERM_FLAT . '`' );
|
||||
$this->assertSame( 1, $count, 'Re-running must not duplicate (PRIMARY KEY enforces)' );
|
||||
}
|
||||
|
||||
public function test_backfill_updates_existing_row_via_on_duplicate_key(): void {
|
||||
global $wpdb;
|
||||
$wpdb->insert( self::TERMS, array( 'term_id' => 1, 'name' => 'a' ) );
|
||||
$wpdb->insert( self::TERM_FLAT, array( 'term_id' => 1, 'hp_sort_order' => 99 ) );
|
||||
$wpdb->insert( self::TERMMETA, array( 'term_id' => 1, 'meta_key' => 'hp_sort_order', 'meta_value' => '5' ) );
|
||||
|
||||
WPDO_Term_Comment_Backfill::backfill_group( 'term', 'hp_taxonomy', false );
|
||||
|
||||
$value = $wpdb->get_var( 'SELECT hp_sort_order FROM `' . self::TERM_FLAT . '` WHERE term_id = 1' );
|
||||
$this->assertSame( '5', (string) $value, 'wp_termmeta value should overwrite stale flat value' );
|
||||
}
|
||||
|
||||
public function test_backfill_handles_comment_entity(): void {
|
||||
global $wpdb;
|
||||
$wpdb->insert( self::COMMENTS, array( 'comment_ID' => 1, 'comment_post_ID' => 100 ) );
|
||||
$wpdb->insert( self::COMMENTMETA, array( 'comment_id' => 1, 'meta_key' => 'hp_rating', 'meta_value' => '5' ) );
|
||||
|
||||
$result = WPDO_Term_Comment_Backfill::backfill_group( 'comment', 'hp_review', false );
|
||||
$this->assertSame( 1, $result['candidates'] );
|
||||
|
||||
$value = $wpdb->get_var( 'SELECT hp_rating FROM `' . self::COMMENT_FLAT . '` WHERE comment_id = 1' );
|
||||
$this->assertSame( '5', (string) $value );
|
||||
}
|
||||
|
||||
public function test_backfill_only_picks_registered_keys(): void {
|
||||
global $wpdb;
|
||||
$wpdb->insert( self::TERMS, array( 'term_id' => 1, 'name' => 'a' ) );
|
||||
$wpdb->insert( self::TERMMETA, array( 'term_id' => 1, 'meta_key' => 'hp_sort_order', 'meta_value' => '5' ) );
|
||||
$wpdb->insert( self::TERMMETA, array( 'term_id' => 1, 'meta_key' => 'unknown_key', 'meta_value' => 'x' ) );
|
||||
$wpdb->insert( self::TERMMETA, array( 'term_id' => 1, 'meta_key' => 'note_group', 'meta_value' => 'foo' ) );
|
||||
|
||||
WPDO_Term_Comment_Backfill::backfill_group( 'term', 'hp_taxonomy', false );
|
||||
|
||||
// hp_icon and hp_default should be NULL (not in wp_termmeta).
|
||||
$row = $wpdb->get_row( 'SELECT * FROM `' . self::TERM_FLAT . '` WHERE term_id = 1', ARRAY_A );
|
||||
$this->assertSame( '5', (string) $row['hp_sort_order'] );
|
||||
$this->assertNull( $row['hp_default'] );
|
||||
$this->assertNull( $row['hp_icon'] );
|
||||
}
|
||||
|
||||
public function test_backfill_unknown_group_returns_error(): void {
|
||||
$result = WPDO_Term_Comment_Backfill::backfill_group( 'term', 'bogus_group', false );
|
||||
$this->assertArrayHasKey( 'error', $result );
|
||||
$this->assertSame( 0, $result['candidates'] );
|
||||
}
|
||||
|
||||
public function test_backfill_unsupported_entity_returns_error(): void {
|
||||
$result = WPDO_Term_Comment_Backfill::backfill_group( 'user', 'hp_taxonomy', false );
|
||||
$this->assertArrayHasKey( 'error', $result );
|
||||
}
|
||||
|
||||
// ── backfill_all ────────────────────────────────────────────────────────
|
||||
|
||||
public function test_backfill_all_runs_every_group(): void {
|
||||
global $wpdb;
|
||||
$wpdb->insert( self::TERMS, array( 'term_id' => 1, 'name' => 'a' ) );
|
||||
$wpdb->insert( self::TERMMETA, array( 'term_id' => 1, 'meta_key' => 'hp_sort_order', 'meta_value' => '5' ) );
|
||||
$wpdb->insert( self::COMMENTS, array( 'comment_ID' => 1, 'comment_post_ID' => 100 ) );
|
||||
$wpdb->insert( self::COMMENTMETA, array( 'comment_id' => 1, 'meta_key' => 'hp_rating', 'meta_value' => '5' ) );
|
||||
|
||||
$results = WPDO_Term_Comment_Backfill::backfill_all( false );
|
||||
$this->assertCount( 2, $results );
|
||||
$this->assertArrayHasKey( 'hp_taxonomy', $results );
|
||||
$this->assertArrayHasKey( 'hp_review', $results );
|
||||
$this->assertSame( 1, $results['hp_taxonomy']['candidates'] );
|
||||
$this->assertSame( 1, $results['hp_review']['candidates'] );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
<?php
|
||||
/**
|
||||
* PHPUnit integration bootstrap for the HivePress AddOn.
|
||||
*
|
||||
* Reuses the core plugin's integration bootstrap (real MariaDB $wpdb, WP
|
||||
* function stubs, constants, WPDO_* aliases) instead of duplicating it, then
|
||||
* loads this AddOn's own classes on top.
|
||||
*
|
||||
* The core plugin must sit next to this one under wp-content/plugins/, and the
|
||||
* same TMDO_TEST_DB_* environment variables the core suite needs apply here.
|
||||
*
|
||||
* @package TMDO_HIVEPRESS
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
$tmdo_core_bootstrap = dirname( __DIR__, 3 ) . '/2meet-data-optimizer/tests/integration/bootstrap.php';
|
||||
if ( ! file_exists( $tmdo_core_bootstrap ) ) {
|
||||
throw new RuntimeException(
|
||||
'HivePress AddOn integration tests require the core plugin at '
|
||||
. dirname( __DIR__, 3 ) . '/2meet-data-optimizer'
|
||||
);
|
||||
}
|
||||
require_once $tmdo_core_bootstrap;
|
||||
|
||||
define( 'TMDO_HIVEPRESS_PATH', dirname( __DIR__, 2 ) . '/' );
|
||||
define( 'TMDO_HIVEPRESS_URL', 'http://localhost/wp-content/plugins/2meet-data-optimizer-hivepress-addon/' );
|
||||
define( 'TMDO_HIVEPRESS_FILE', TMDO_HIVEPRESS_PATH . '2meet-data-optimizer-hivepress-addon.php' );
|
||||
define( 'TMDO_HIVEPRESS_VERSION', '0.1.0' );
|
||||
|
||||
// ── AddOn classes ──────────────────────────────────────────────────────────
|
||||
// Superset of the unit bootstrap's list: the integration suite also exercises
|
||||
// the interceptors, which the unit suite does not load.
|
||||
|
||||
$tmdo_hp_files = array(
|
||||
'includes/class-tmdo-hivepress.php',
|
||||
'includes/hivepress/interface-tmdo-hp-adapter.php',
|
||||
'includes/hivepress/trait-tmdo-hp-adapter.php',
|
||||
'includes/hivepress/class-tmdo-hivepress-detector.php',
|
||||
'includes/hivepress/class-tmdo-hivepress-conflict-guard.php',
|
||||
'includes/hivepress/class-tmdo-hivepress-bootstrap.php',
|
||||
'includes/hivepress/class-tmdo-hivepress-comment-router.php',
|
||||
'includes/hivepress/class-tmdo-hivepress-cron-optimizer.php',
|
||||
'includes/hivepress/class-tmdo-hivepress-attribute-bridge.php',
|
||||
'includes/hivepress/class-tmdo-hivepress-suitability-scorer.php',
|
||||
'includes/hivepress/class-tmdo-hivepress-benchmark.php',
|
||||
'includes/hivepress/class-tmdo-hivepress-rest.php',
|
||||
'includes/class-tmdo-hivepress-transient-filter.php',
|
||||
'includes/class-tmdo-hivepress-term-comment-fields.php',
|
||||
'includes/class-tmdo-hpct-import.php',
|
||||
'admin/class-tmdo-admin-hivepress.php',
|
||||
'cli/class-tmdo-cli-hivepress.php',
|
||||
);
|
||||
|
||||
foreach ( glob( TMDO_HIVEPRESS_PATH . 'includes/hivepress/adapters/*.php' ) ?: array() as $tmdo_hp_glob ) {
|
||||
$tmdo_hp_files[] = 'includes/hivepress/adapters/' . basename( $tmdo_hp_glob );
|
||||
}
|
||||
foreach ( glob( TMDO_HIVEPRESS_PATH . 'includes/interceptors/*.php' ) ?: array() as $tmdo_hp_glob ) {
|
||||
$tmdo_hp_files[] = 'includes/interceptors/' . basename( $tmdo_hp_glob );
|
||||
}
|
||||
foreach ( glob( TMDO_HIVEPRESS_PATH . 'includes/query/*.php' ) ?: array() as $tmdo_hp_glob ) {
|
||||
$tmdo_hp_files[] = 'includes/query/' . basename( $tmdo_hp_glob );
|
||||
}
|
||||
|
||||
foreach ( $tmdo_hp_files as $tmdo_hp_file ) {
|
||||
$tmdo_hp_path = TMDO_HIVEPRESS_PATH . $tmdo_hp_file;
|
||||
if ( file_exists( $tmdo_hp_path ) ) {
|
||||
require_once $tmdo_hp_path;
|
||||
}
|
||||
}
|
||||
unset( $tmdo_hp_files, $tmdo_hp_file, $tmdo_hp_path, $tmdo_hp_glob, $tmdo_core_bootstrap );
|
||||
|
||||
// TMDO_Listing_Stats is stubbed by the core integration bootstrap (it lives in
|
||||
// this AddOn but core tests reference it). Loading the real one here would
|
||||
// collide with that stub, and no integration test exercises it — so it stays
|
||||
// out, matching the unit bootstrap.
|
||||
|
||||
// ── Back-compat aliases for this AddOn's classes ───────────────────────────
|
||||
// Mirrors includes/back-compat-aliases.php, which runs on plugins_loaded:7 and
|
||||
// therefore never fires under PHPUnit.
|
||||
foreach ( get_declared_classes() as $tmdo_declared ) {
|
||||
if ( str_starts_with( $tmdo_declared, 'TMDO_' ) ) {
|
||||
$tmdo_alias = 'WPDO_' . substr( $tmdo_declared, 5 );
|
||||
if ( ! class_exists( $tmdo_alias, false ) ) {
|
||||
class_alias( $tmdo_declared, $tmdo_alias );
|
||||
}
|
||||
}
|
||||
}
|
||||
unset( $tmdo_declared, $tmdo_alias );
|
||||
|
||||
// PHP cannot class_alias() traits — declare a thin wrapper so the suite can
|
||||
// keep referring to the WPDO_ name. The WPDO_HivePress_Adapter interface is
|
||||
// declared by interface-tmdo-hp-adapter.php itself (TMDO_ extends WPDO_).
|
||||
if ( trait_exists( 'TMDO_HivePress_Adapter_Trait' ) && ! trait_exists( 'WPDO_HivePress_Adapter_Trait', false ) ) {
|
||||
trait WPDO_HivePress_Adapter_Trait {
|
||||
use TMDO_HivePress_Adapter_Trait;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user