6f117f4e9b
Anti-EAV Lint + Quality Gate / anti-eav-lint (pull_request) Successful in 21s
Tests / PHP Lint (pull_request) Successful in 18s
Tests / Unit Tests (pull_request) Successful in 41s
Tests / Integration Tests (pull_request) Successful in 1m25s
Tests / PHPCS (pull_request) Successful in 44s
Tests / PHPStan (pull_request) Successful in 47s
- Archive sweep waits until idx_original_meta exists (a plugin update does not run dbDelta; without the index NOT EXISTS scans the archive per row) - Duplicate archive cleanup runs from WP-CLI only, never in a web activation - Uninstall keeps the LatePoint add-on's tmdo_latepoint_db_version - AJAX save errors stay on screen instead of fading after 2.5 s - Verify: 2 new integration tests; unit 711, integration 612, phpcs 0, stan OK - PR: PR-141 (2.4.2 correctness review)
268 lines
11 KiB
PHP
268 lines
11 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
|
|
use PHPUnit\Framework\TestCase;
|
|
|
|
/**
|
|
* Integration tests for WPDO_Zone_Archive against real MariaDB.
|
|
*
|
|
* Covers archive/get (with gzip decompression), archive_batch (transaction),
|
|
* delete, stats(), and restore().
|
|
*/
|
|
class ZoneArchiveIntegrationTest extends TestCase {
|
|
|
|
private const TABLE = 'wp_itest_wpdo_archive';
|
|
|
|
// ── Fixture lifecycle ─────────────────────────────────────────────────
|
|
|
|
public static function setUpBeforeClass(): void {
|
|
global $wpdb;
|
|
$wpdb->query(
|
|
'CREATE TABLE IF NOT EXISTS `' . self::TABLE . '` (
|
|
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
|
|
`post_id` bigint(20) unsigned NOT NULL DEFAULT 0,
|
|
`post_type` varchar(20) NOT NULL DEFAULT \'\',
|
|
`meta_key` varchar(255) NOT NULL DEFAULT \'\',
|
|
`meta_value` longtext DEFAULT NULL,
|
|
`compressed` tinyint(1) NOT NULL DEFAULT 0,
|
|
`archived_at` datetime NOT NULL DEFAULT \'0000-00-00 00:00:00\',
|
|
`original_meta_id` bigint(20) unsigned NOT NULL DEFAULT 0,
|
|
PRIMARY KEY (`id`),
|
|
KEY `post_id` (`post_id`),
|
|
KEY `archived_at` (`archived_at`),
|
|
KEY `idx_original_meta` (`original_meta_id`)
|
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4'
|
|
);
|
|
}
|
|
|
|
public static function tearDownAfterClass(): void {
|
|
global $wpdb;
|
|
$wpdb->query( 'DROP TABLE IF EXISTS `' . self::TABLE . '`' );
|
|
}
|
|
|
|
protected function setUp(): void {
|
|
global $wpdb;
|
|
$wpdb->query( 'TRUNCATE TABLE `' . self::TABLE . '`' );
|
|
$GLOBALS['_wp_postmeta'] = [];
|
|
}
|
|
|
|
// ── sweep / duplicates (2.4.2 PR-136, manual F-6) ─────────────────────
|
|
|
|
/** Posts and postmeta for sweep(): post 10 trashed 200 days ago, post 11 published. */
|
|
private function seed_trashed_post(): void {
|
|
global $wpdb;
|
|
$wpdb->query( "DROP TABLE IF EXISTS `{$wpdb->posts}`" );
|
|
$wpdb->query( "DROP TABLE IF EXISTS `{$wpdb->postmeta}`" );
|
|
$wpdb->query( "CREATE TABLE `{$wpdb->posts}` ( ID BIGINT UNSIGNED NOT NULL, post_type VARCHAR(20) NOT NULL DEFAULT 'post', post_status VARCHAR(20) NOT NULL DEFAULT 'publish', post_modified_gmt DATETIME NOT NULL DEFAULT '2000-01-01 00:00:00', PRIMARY KEY (ID) ) ENGINE=InnoDB" );
|
|
$wpdb->query( "CREATE TABLE `{$wpdb->postmeta}` ( meta_id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, post_id BIGINT UNSIGNED NOT NULL DEFAULT 0, meta_key VARCHAR(255) DEFAULT NULL, meta_value LONGTEXT, PRIMARY KEY (meta_id) ) ENGINE=InnoDB" );
|
|
$old = gmdate( 'Y-m-d H:i:s', time() - 200 * DAY_IN_SECONDS );
|
|
$wpdb->query( "INSERT INTO `{$wpdb->posts}` VALUES (10, 'hp_listing', 'trash', '{$old}'), (11, 'hp_listing', 'publish', '{$old}')" );
|
|
$wpdb->query( "INSERT INTO `{$wpdb->postmeta}` (post_id, meta_key, meta_value) VALUES (10, 'hp_price', '5'), (10, 'hp_rating', '4'), (11, 'hp_price', '9')" );
|
|
}
|
|
|
|
private function drop_posts(): void {
|
|
global $wpdb;
|
|
$wpdb->query( "DROP TABLE IF EXISTS `{$wpdb->posts}`" );
|
|
$wpdb->query( "DROP TABLE IF EXISTS `{$wpdb->postmeta}`" );
|
|
}
|
|
|
|
private function archive_rows(): int {
|
|
global $wpdb;
|
|
return (int) $wpdb->get_var( 'SELECT COUNT(*) FROM `' . self::TABLE . '`' );
|
|
}
|
|
|
|
public function test_sweep_copies_each_trashed_meta_row_once(): void {
|
|
$this->seed_trashed_post();
|
|
try {
|
|
$this->assertSame( 2, WPDO_Zone_Archive::sweep( 90, false ) );
|
|
$this->assertSame( 0, WPDO_Zone_Archive::sweep( 90, false ), 'already archived' );
|
|
$this->assertSame( 2, $this->archive_rows() );
|
|
} finally {
|
|
$this->drop_posts();
|
|
}
|
|
}
|
|
|
|
/** Until `wp tmdo install` adds the index, NOT EXISTS would scan the archive per candidate. */
|
|
public function test_sweep_waits_for_the_original_meta_index(): void {
|
|
global $wpdb;
|
|
$this->seed_trashed_post();
|
|
$wpdb->query( 'ALTER TABLE `' . self::TABLE . '` DROP INDEX `idx_original_meta`' );
|
|
try {
|
|
$this->assertSame( 0, WPDO_Zone_Archive::sweep( 90, false ) );
|
|
$this->assertSame( 0, $this->archive_rows() );
|
|
} finally {
|
|
$wpdb->query( 'ALTER TABLE `' . self::TABLE . '` ADD KEY `idx_original_meta` (`original_meta_id`)' );
|
|
$this->drop_posts();
|
|
}
|
|
}
|
|
|
|
public function test_remove_duplicates_keeps_the_first_copy_of_each_meta_row(): void {
|
|
WPDO_Zone_Archive::archive( 10, 'hp_listing', 'hp_price', '5', 101 );
|
|
WPDO_Zone_Archive::archive( 10, 'hp_listing', 'hp_price', '5', 101 );
|
|
WPDO_Zone_Archive::archive( 10, 'hp_listing', 'hp_rating', '4', 102 );
|
|
WPDO_Zone_Archive::archive( 10, 'hp_listing', 'hp_rating', '4', 102 );
|
|
WPDO_Zone_Archive::archive( 12, 'hp_listing', 'hp_price', '7' ); // original_meta_id 0: no identity, kept.
|
|
WPDO_Zone_Archive::archive( 12, 'hp_listing', 'hp_price', '7' );
|
|
|
|
$this->assertSame( 2, WPDO_Zone_Archive::remove_duplicates() );
|
|
$this->assertSame( 4, $this->archive_rows() );
|
|
$this->assertSame( 0, WPDO_Zone_Archive::remove_duplicates() );
|
|
}
|
|
|
|
// ── archive / get ─────────────────────────────────────────────────────
|
|
|
|
public function test_archive_stores_plain_entry(): void {
|
|
WPDO_Zone_Archive::archive( 1, 'hp_listing', 'hp_price', '199.99' );
|
|
|
|
$rows = WPDO_Zone_Archive::get( 1 );
|
|
$this->assertCount( 1, $rows );
|
|
$this->assertSame( 'hp_price', $rows[0]['meta_key'] );
|
|
$this->assertSame( '199.99', $rows[0]['meta_value'] );
|
|
}
|
|
|
|
public function test_archive_with_compression_stores_and_decompresses(): void {
|
|
$original = 'large content that compresses well: ' . str_repeat( 'abcdef', 50 );
|
|
WPDO_Zone_Archive::archive( 2, 'hp_listing', 'hp_desc', $original, 0, true );
|
|
|
|
$rows = WPDO_Zone_Archive::get( 2, 'hp_desc' );
|
|
$this->assertCount( 1, $rows );
|
|
// get() decompresses automatically — value must match original.
|
|
$this->assertSame( $original, $rows[0]['meta_value'] );
|
|
}
|
|
|
|
public function test_get_with_meta_key_filter_returns_only_that_key(): void {
|
|
WPDO_Zone_Archive::archive( 3, 'hp_listing', 'hp_price', '50.00' );
|
|
WPDO_Zone_Archive::archive( 3, 'hp_listing', 'hp_featured', '1' );
|
|
WPDO_Zone_Archive::archive( 3, 'hp_listing', 'hp_verified', '1' );
|
|
|
|
$rows = WPDO_Zone_Archive::get( 3, 'hp_price' );
|
|
$this->assertCount( 1, $rows );
|
|
$this->assertSame( 'hp_price', $rows[0]['meta_key'] );
|
|
}
|
|
|
|
public function test_get_without_filter_returns_all_keys(): void {
|
|
WPDO_Zone_Archive::archive( 4, 'hp_listing', 'hp_price', '75.00' );
|
|
WPDO_Zone_Archive::archive( 4, 'hp_listing', 'hp_featured', '0' );
|
|
|
|
$rows = WPDO_Zone_Archive::get( 4 );
|
|
$this->assertCount( 2, $rows );
|
|
}
|
|
|
|
public function test_get_returns_empty_for_missing_post(): void {
|
|
$rows = WPDO_Zone_Archive::get( 9999 );
|
|
$this->assertSame( [], $rows );
|
|
}
|
|
|
|
// ── archive_batch ────────────────────────────────────────────────────
|
|
|
|
public function test_archive_batch_stores_all_entries_in_transaction(): void {
|
|
$entries = [
|
|
[ 'post_id' => 5, 'post_type' => 'hp_listing', 'meta_key' => 'hp_price', 'meta_value' => '100.00', 'meta_id' => 0 ],
|
|
[ 'post_id' => 5, 'post_type' => 'hp_listing', 'meta_key' => 'hp_featured', 'meta_value' => '1', 'meta_id' => 0 ],
|
|
[ 'post_id' => 6, 'post_type' => 'hp_vendor', 'meta_key' => 'hp_rate', 'meta_value' => '50.00', 'meta_id' => 0 ],
|
|
];
|
|
|
|
WPDO_Zone_Archive::archive_batch( $entries );
|
|
|
|
$this->assertCount( 2, WPDO_Zone_Archive::get( 5 ) );
|
|
$this->assertCount( 1, WPDO_Zone_Archive::get( 6 ) );
|
|
}
|
|
|
|
public function test_archive_batch_with_compression(): void {
|
|
$original = str_repeat( 'x', 200 );
|
|
WPDO_Zone_Archive::archive_batch(
|
|
[ [ 'post_id' => 7, 'post_type' => 'hp_listing', 'meta_key' => 'hp_desc', 'meta_value' => $original, 'meta_id' => 0 ] ],
|
|
true
|
|
);
|
|
|
|
$rows = WPDO_Zone_Archive::get( 7, 'hp_desc' );
|
|
$this->assertSame( $original, $rows[0]['meta_value'] );
|
|
}
|
|
|
|
// ── delete ───────────────────────────────────────────────────────────
|
|
|
|
public function test_delete_removes_all_entries_for_post(): void {
|
|
WPDO_Zone_Archive::archive( 8, 'hp_listing', 'hp_price', '30.00' );
|
|
WPDO_Zone_Archive::archive( 8, 'hp_listing', 'hp_featured', '1' );
|
|
$this->assertCount( 2, WPDO_Zone_Archive::get( 8 ) );
|
|
|
|
WPDO_Zone_Archive::delete( 8 );
|
|
$this->assertSame( [], WPDO_Zone_Archive::get( 8 ) );
|
|
}
|
|
|
|
public function test_delete_does_not_affect_other_posts(): void {
|
|
WPDO_Zone_Archive::archive( 9, 'hp_listing', 'hp_price', '10.00' );
|
|
WPDO_Zone_Archive::archive( 10, 'hp_listing', 'hp_price', '20.00' );
|
|
|
|
WPDO_Zone_Archive::delete( 9 );
|
|
|
|
$this->assertSame( [], WPDO_Zone_Archive::get( 9 ) );
|
|
$this->assertCount( 1, WPDO_Zone_Archive::get( 10 ) );
|
|
}
|
|
|
|
// ── stats ─────────────────────────────────────────────────────────────
|
|
|
|
public function test_stats_counts_total_and_compressed_rows(): void {
|
|
WPDO_Zone_Archive::archive( 11, 'hp_listing', 'hp_price', '1.00', 0, false );
|
|
WPDO_Zone_Archive::archive( 12, 'hp_listing', 'hp_price', '2.00', 0, true );
|
|
WPDO_Zone_Archive::archive( 13, 'hp_listing', 'hp_price', '3.00', 0, true );
|
|
|
|
$stats = WPDO_Zone_Archive::stats();
|
|
$this->assertSame( 3, $stats['total_rows'] );
|
|
$this->assertSame( 2, $stats['compressed_rows'] );
|
|
}
|
|
|
|
public function test_stats_groups_by_post_type(): void {
|
|
WPDO_Zone_Archive::archive( 14, 'hp_listing', 'hp_price', '1.00' );
|
|
WPDO_Zone_Archive::archive( 15, 'hp_listing', 'hp_price', '2.00' );
|
|
WPDO_Zone_Archive::archive( 16, 'hp_vendor', 'hp_rate', '3.00' );
|
|
|
|
$stats = WPDO_Zone_Archive::stats();
|
|
$type_map = array_column( $stats['post_types'], 'cnt', 'post_type' );
|
|
|
|
$this->assertSame( '2', $type_map['hp_listing'] );
|
|
$this->assertSame( '1', $type_map['hp_vendor'] );
|
|
}
|
|
|
|
public function test_stats_returns_zeros_on_empty_table(): void {
|
|
$stats = WPDO_Zone_Archive::stats();
|
|
$this->assertSame( 0, $stats['total_rows'] );
|
|
$this->assertSame( 0, $stats['compressed_rows'] );
|
|
$this->assertSame( [], $stats['post_types'] );
|
|
}
|
|
|
|
// ── restore ───────────────────────────────────────────────────────────
|
|
|
|
public function test_restore_writes_to_postmeta_and_removes_from_archive(): void {
|
|
WPDO_Zone_Archive::archive( 17, 'hp_listing', 'hp_price', '99.00' );
|
|
WPDO_Zone_Archive::archive( 17, 'hp_listing', 'hp_featured', '1' );
|
|
|
|
$count = WPDO_Zone_Archive::restore( 17 );
|
|
|
|
// Two entries restored.
|
|
$this->assertSame( 2, $count );
|
|
|
|
// Postmeta updated via stub.
|
|
$this->assertSame( '99.00', $GLOBALS['_wp_postmeta'][17]['hp_price'] );
|
|
$this->assertSame( '1', $GLOBALS['_wp_postmeta'][17]['hp_featured'] );
|
|
|
|
// Archive cleared.
|
|
$this->assertSame( [], WPDO_Zone_Archive::get( 17 ) );
|
|
}
|
|
|
|
public function test_restore_with_meta_key_filter_only_restores_that_key(): void {
|
|
WPDO_Zone_Archive::archive( 18, 'hp_listing', 'hp_price', '55.00' );
|
|
WPDO_Zone_Archive::archive( 18, 'hp_listing', 'hp_featured', '0' );
|
|
|
|
$count = WPDO_Zone_Archive::restore( 18, 'hp_price' );
|
|
|
|
$this->assertSame( 1, $count );
|
|
$this->assertSame( '55.00', $GLOBALS['_wp_postmeta'][18]['hp_price'] );
|
|
|
|
// hp_featured should still be in archive.
|
|
$remaining = WPDO_Zone_Archive::get( 18 );
|
|
$this->assertCount( 1, $remaining );
|
|
$this->assertSame( 'hp_featured', $remaining[0]['meta_key'] );
|
|
}
|
|
}
|