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.
|
||||
Reference in New Issue
Block a user