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 ); } }