Files
wpdev d36bb954d1 chore: initial snapshot of 2meet-data-optimizer v0.1.0
Baseline before backporting wp-data-optimizer v3.0.1-v3.4.6.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TbG1keQQ7XBa7qMQY16KCY
2026-07-31 05:06:36 +08:00

260 lines
9.0 KiB
PHP

<?php
declare(strict_types=1);
use PHPUnit\Framework\TestCase;
// Bootstrap missing WP filesystem helpers used by Snapshot_Manager (idempotent across tests).
if ( ! function_exists( 'wp_upload_dir' ) ) {
function wp_upload_dir(): array {
return array(
'basedir' => sys_get_temp_dir() . '/wpdo-test-uploads',
'baseurl' => 'http://localhost/uploads',
);
}
}
if ( ! function_exists( 'wp_mkdir_p' ) ) {
function wp_mkdir_p( string $dir ): bool {
if ( is_dir( $dir ) ) {
return true;
}
return mkdir( $dir, 0777, true );
}
}
if ( ! function_exists( 'esc_sql' ) ) {
function esc_sql( $s ): string {
return addslashes( (string) $s );
}
}
if ( ! function_exists( 'size_format' ) ) {
function size_format( int $bytes, int $decimals = 0 ): string {
return $bytes . 'B';
}
}
require_once dirname( __DIR__, 3 ) . '/includes/class-tmdo-logger.php';
require_once dirname( __DIR__, 3 ) . '/includes/class-tmdo-feature-flags.php';
require_once dirname( __DIR__, 3 ) . '/includes/snapshots/class-tmdo-snapshot-manager.php';
require_once dirname( __DIR__, 3 ) . '/includes/snapshots/class-tmdo-snapshot-writer.php';
require_once dirname( __DIR__, 3 ) . '/includes/snapshots/class-tmdo-snapshot-reader.php';
require_once dirname( __DIR__, 3 ) . '/includes/snapshots/class-tmdo-snapshot-pruner.php';
/**
* Unit tests for WPDO_Snapshot_Manager + Writer + Reader (v2.2.0 M1).
*
* These tests exercise pure logic + filesystem-isolated paths in sys_get_temp_dir().
* Heavy integration cases (real DB dump + restore) are covered by the
* integration suite and the e2e/wp-data-optimizer/ Playwright tests.
*/
class SnapshotManagerTest extends TestCase {
protected function setUp(): void {
// Clean tmp upload dir.
$dir = sys_get_temp_dir() . '/wpdo-test-uploads/wpdo-backups';
if ( is_dir( $dir ) ) {
foreach ( glob( $dir . '/*' ) as $f ) {
if ( is_file( $f ) ) {
@unlink( $f ); // phpcs:ignore WordPress.PHP.NoSilencedErrors
}
}
}
}
public function test_generate_id_format_and_uniqueness(): void {
$ref = new ReflectionClass( WPDO_Snapshot_Manager::class );
$method = $ref->getMethod( 'generate_id' );
$method->setAccessible( true );
$ids = array();
for ( $i = 0; $i < 50; $i++ ) {
$id = $method->invoke( null );
$this->assertMatchesRegularExpression( '/^wpdo_[a-z0-9]+_[a-f0-9]+$/', $id );
$ids[] = $id;
}
$this->assertCount( 50, array_unique( $ids ), '50 generated IDs should all be distinct' );
}
public function test_ensure_backup_dir_creates_dir_and_htaccess(): void {
$ok = WPDO_Snapshot_Manager::ensure_backup_dir();
$this->assertTrue( $ok );
$dir = WPDO_Snapshot_Manager::backup_dir();
$this->assertDirectoryExists( $dir );
$this->assertFileExists( $dir . '/.htaccess' );
$this->assertStringContainsString( 'Deny from all', file_get_contents( $dir . '/.htaccess' ) );
$this->assertFileExists( $dir . '/index.php' );
}
public function test_create_with_invalid_trigger_returns_error(): void {
$result = WPDO_Snapshot_Manager::create( 'totally_made_up_trigger', array() );
$this->assertFalse( $result['ok'] );
$this->assertSame( 'invalid_trigger', $result['error'] );
}
public function test_writer_escape_sql_value_handles_all_types(): void {
$w = new WPDO_Snapshot_Writer( 'wpdo_test_id', array() );
$ref = new ReflectionClass( WPDO_Snapshot_Writer::class );
$m = $ref->getMethod( 'escape_sql_value' );
$m->setAccessible( true );
$this->assertSame( 'NULL', $m->invoke( $w, null ) );
$this->assertSame( '0', $m->invoke( $w, false ) );
$this->assertSame( '1', $m->invoke( $w, true ) );
$this->assertSame( '42', $m->invoke( $w, 42 ) );
$this->assertSame( '3.14', $m->invoke( $w, 3.14 ) );
$this->assertSame( "'hello'", $m->invoke( $w, 'hello' ) );
$this->assertSame( "'don\\'t'", $m->invoke( $w, "don't" ) );
// Binary (non-utf8) should hex-encode.
$bin = "\x00\x01\xff\xfe";
$out = $m->invoke( $w, $bin );
$this->assertSame( '0x0001fffe', $out );
}
public function test_writer_is_safe_name_validates_table(): void {
global $wpdb;
$saved_prefix = $wpdb->prefix ?? 'wp_';
$wpdb->prefix = 'wp_';
$w = new WPDO_Snapshot_Writer( 'wpdo_test_id', array() );
$ref = new ReflectionClass( WPDO_Snapshot_Writer::class );
$m = $ref->getMethod( 'is_safe_name' );
$m->setAccessible( true );
$this->assertTrue( $m->invoke( $w, 'wp_postmeta' ) );
$this->assertTrue( $m->invoke( $w, 'wp_wpdo_warm' ) );
$this->assertFalse( $m->invoke( $w, 'foo_postmeta' ), 'wrong prefix should be rejected' );
$this->assertFalse( $m->invoke( $w, 'wp_post; DROP TABLE' ), 'sql injection should be rejected' );
$this->assertFalse( $m->invoke( $w, 'wp_post-bad' ), 'dash should be rejected' );
$wpdb->prefix = $saved_prefix;
}
public function test_reader_parse_summary_extracts_table_row_counts(): void {
$catalog_row = array(
'snapshot_id' => 'wpdo_dummy',
'storage' => 'inline',
'size_bytes' => 100,
'inline_blob' => '',
);
$reader = new WPDO_Snapshot_Reader( $catalog_row );
$ref = new ReflectionClass( WPDO_Snapshot_Reader::class );
$m = $ref->getMethod( 'parse_summary' );
$m->setAccessible( true );
$sql = "-- header
INSERT INTO `wp_wpdo_warm` (`a`,`b`) VALUES (1,'x'),
(2,'y'),
(3,'z');
INSERT INTO `wp_wpdo_archive` (`a`) VALUES (10);
";
$summary = $m->invoke( $reader, $sql );
$this->assertSame( 4, $summary['total_rows'] );
$this->assertSame( 2, $summary['statements'] );
$this->assertSame( 3, $summary['tables']['wp_wpdo_warm'] );
$this->assertSame( 1, $summary['tables']['wp_wpdo_archive'] );
}
public function test_reader_verify_inline_size_match(): void {
$blob = "INSERT INTO `wp_wpdo_warm` VALUES (1);\n";
$reader = new WPDO_Snapshot_Reader( array(
'snapshot_id' => 'wpdo_dummy',
'storage' => 'inline',
'size_bytes' => strlen( $blob ),
'inline_blob' => $blob,
) );
$result = $reader->verify();
$this->assertTrue( $result['ok'] );
$this->assertTrue( $result['size_match'] );
$this->assertSame( 'inline', $result['storage'] );
}
public function test_reader_verify_inline_size_mismatch(): void {
$blob = "AAA";
$reader = new WPDO_Snapshot_Reader( array(
'snapshot_id' => 'wpdo_dummy',
'storage' => 'inline',
'size_bytes' => 999, // claim size that doesn't match.
'inline_blob' => $blob,
) );
$result = $reader->verify();
$this->assertFalse( $result['ok'] );
$this->assertFalse( $result['size_match'] );
}
public function test_reader_verify_file_missing(): void {
$reader = new WPDO_Snapshot_Reader( array(
'snapshot_id' => 'wpdo_dummy',
'storage' => 'file',
'size_bytes' => 100,
'file_path' => '/non/existent/path.sql.gz',
'file_sha256' => str_repeat( '0', 64 ),
) );
$result = $reader->verify();
$this->assertFalse( $result['ok'] );
$this->assertSame( 'file_missing', $result['error'] );
}
public function test_reader_maybe_gunzip_decompresses_real_gzip(): void {
$plaintext = "INSERT INTO `wp_wpdo_warm` VALUES (1);\n";
$gzipped = gzencode( $plaintext );
$catalog_row = array(
'snapshot_id' => 'wpdo_dummy',
'storage' => 'inline',
'size_bytes' => strlen( $gzipped ),
'inline_blob' => $gzipped,
);
$reader = new WPDO_Snapshot_Reader( $catalog_row );
$ref = new ReflectionClass( WPDO_Snapshot_Reader::class );
$m = $ref->getMethod( 'maybe_gunzip' );
$m->setAccessible( true );
$out = $m->invoke( $reader, $gzipped );
$this->assertSame( $plaintext, $out );
// Plain text should pass through untouched.
$out_plain = $m->invoke( $reader, $plaintext );
$this->assertSame( $plaintext, $out_plain );
}
public function test_reader_load_sql_inline_decompresses(): void {
$plaintext = "INSERT INTO `wp_test` VALUES (1);\n";
$gzipped = gzencode( $plaintext );
$reader = new WPDO_Snapshot_Reader( array(
'snapshot_id' => 'wpdo_dummy',
'storage' => 'inline',
'size_bytes' => strlen( $gzipped ),
'inline_blob' => $gzipped,
) );
$ref = new ReflectionClass( WPDO_Snapshot_Reader::class );
$m = $ref->getMethod( 'load_sql' );
$m->setAccessible( true );
$out = $m->invoke( $reader );
$this->assertSame( $plaintext, $out );
}
public function test_reader_throws_on_missing_required_keys(): void {
$this->expectException( InvalidArgumentException::class );
new WPDO_Snapshot_Reader( array() );
}
public function test_pruner_protected_triggers_listed(): void {
$ref = new ReflectionClass( WPDO_Snapshot_Pruner::class );
$prop = $ref->getReflectionConstant( 'PROTECTED_TRIGGERS' );
$this->assertNotNull( $prop );
$value = $prop->getValue();
$this->assertContains( 'pre_uninstall', $value );
$this->assertContains( 'pre_v2_upgrade', $value );
}
public function test_manager_valid_triggers_constant(): void {
$this->assertContains( 'manual', WPDO_Snapshot_Manager::VALID_TRIGGERS );
$this->assertContains( 'pre_fsm_transition', WPDO_Snapshot_Manager::VALID_TRIGGERS );
$this->assertContains( 'pre_v2_upgrade', WPDO_Snapshot_Manager::VALID_TRIGGERS );
$this->assertContains( 'scheduled', WPDO_Snapshot_Manager::VALID_TRIGGERS );
$this->assertContains( 'pre_uninstall', WPDO_Snapshot_Manager::VALID_TRIGGERS );
}
}