test: 加入 integration 測試套件(連真實 MariaDB)
Tests / PHP Lint (push) Successful in 5s
Tests / Unit Tests (push) Successful in 16s
Tests / Integration Tests (push) Successful in 20s

從 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:
2026-07-31 09:56:48 +08:00
parent 843a8e2156
commit 59c9b698b7
8 changed files with 1133 additions and 0 deletions
@@ -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() );
}
}