59c9b698b7
從 wp-data-optimizer v3.4.6 移植 5 個 integration 測試檔,並新增 tests/integration/bootstrap.php(複用核心的 integration bootstrap,不重複 與 WP stub)+ phpunit-integration.xml + CI integration job。
253 lines
12 KiB
PHP
253 lines
12 KiB
PHP
<?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() );
|
|
}
|
|
}
|