d36bb954d1
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
260 lines
9.0 KiB
PHP
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 );
|
|
}
|
|
}
|