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
160 lines
5.8 KiB
PHP
160 lines
5.8 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
|
|
use PHPUnit\Framework\TestCase;
|
|
|
|
/**
|
|
* Integration test: WPDO_Crypto::migrate_v1_to_v2() (v2.15.0).
|
|
*
|
|
* Verifies the bulk migration path against a real wp_itest_options table:
|
|
* - Mixed format input (v1 / v2 / plaintext / empty) all classified correctly
|
|
* - Counts returned accurately
|
|
* - Idempotency: second run is no-op (all v2)
|
|
* - Non-wpdo prefix excluded from sweep
|
|
*/
|
|
class CryptoMigrationTest extends TestCase {
|
|
|
|
private const TEST_PREFIX = 'wp_itest_';
|
|
|
|
public static function setUpBeforeClass(): void {
|
|
global $wpdb;
|
|
|
|
$wpdb->prefix = self::TEST_PREFIX;
|
|
$wpdb->options = self::TEST_PREFIX . 'options';
|
|
|
|
$wpdb->query(
|
|
'CREATE TABLE IF NOT EXISTS `' . self::TEST_PREFIX . 'options` (
|
|
option_id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
|
|
option_name varchar(191) NOT NULL DEFAULT "",
|
|
option_value longtext NOT NULL,
|
|
autoload varchar(20) NOT NULL DEFAULT "yes",
|
|
PRIMARY KEY (option_id),
|
|
UNIQUE KEY option_name (option_name)
|
|
) DEFAULT CHARACTER SET utf8mb4'
|
|
);
|
|
|
|
// Define WP auth constants for stable key derivation.
|
|
if ( ! defined( 'AUTH_KEY' ) ) {
|
|
define( 'AUTH_KEY', 'integration_auth_key_long_enough_xxxxxxxxxxxxxxxxxxxxxx' );
|
|
}
|
|
if ( ! defined( 'SECURE_AUTH_SALT' ) ) {
|
|
define( 'SECURE_AUTH_SALT', 'integration_secure_auth_salt_long_xxxxxxxxxxxxxxxxxxxx' );
|
|
}
|
|
}
|
|
|
|
protected function setUp(): void {
|
|
global $wpdb;
|
|
// Clear all wpdo_* options before each test for isolation.
|
|
$wpdb->query( "DELETE FROM `" . self::TEST_PREFIX . "options` WHERE option_name LIKE 'wpdo_%' OR option_name LIKE 'unrelated_%'" );
|
|
}
|
|
|
|
public function test_migrate_mixed_format_inputs(): void {
|
|
// Set up: 2 v1 blobs, 1 v2 blob, 1 plaintext, 1 unrelated (non-wpdo).
|
|
$plain1 = 'https://hooks.slack.com/services/legacy1';
|
|
$plain2 = 'https://discord.com/api/webhooks/legacy2';
|
|
$this->insert_v1_option( 'wpdo_legacy_slack', $plain1 );
|
|
$this->insert_v1_option( 'wpdo_legacy_discord', $plain2 );
|
|
|
|
// Already v2.
|
|
$this->set_option_raw( 'wpdo_already_v2', WPDO_Crypto::encrypt( 'already encrypted' ) );
|
|
|
|
// Plaintext.
|
|
$this->set_option_raw( 'wpdo_plaintext_secret', 'just text' );
|
|
|
|
// Unrelated prefix — must NOT be touched.
|
|
$this->set_option_raw( 'unrelated_secret', 'should be ignored' );
|
|
|
|
$counts = WPDO_Crypto::migrate_v1_to_v2( 'wpdo_' );
|
|
|
|
// Scanned 4 wpdo_* options (unrelated_ excluded).
|
|
$this->assertSame( 4, $counts['scanned'] );
|
|
$this->assertSame( 2, $counts['migrated'] );
|
|
$this->assertSame( 1, $counts['already_v2'] );
|
|
$this->assertSame( 1, $counts['plaintext'] );
|
|
$this->assertSame( 0, $counts['failed'] );
|
|
|
|
// Verify v1 blobs were upgraded to v2 and decrypt correctly.
|
|
$this->assertSame( 'v2', WPDO_Crypto::format_version( 'wpdo_legacy_slack' ) );
|
|
$this->assertSame( 'v2', WPDO_Crypto::format_version( 'wpdo_legacy_discord' ) );
|
|
$this->assertSame( $plain1, WPDO_Crypto::get_option( 'wpdo_legacy_slack' ) );
|
|
$this->assertSame( $plain2, WPDO_Crypto::get_option( 'wpdo_legacy_discord' ) );
|
|
|
|
// Plaintext untouched.
|
|
$this->assertSame( 'plaintext', WPDO_Crypto::format_version( 'wpdo_plaintext_secret' ) );
|
|
|
|
// Unrelated option untouched.
|
|
global $wpdb;
|
|
$unrelated_value = $wpdb->get_var(
|
|
$wpdb->prepare(
|
|
"SELECT option_value FROM `" . self::TEST_PREFIX . "options` WHERE option_name = %s",
|
|
'unrelated_secret'
|
|
)
|
|
);
|
|
$this->assertSame( 'should be ignored', $unrelated_value );
|
|
}
|
|
|
|
public function test_migrate_idempotent_second_run_is_noop(): void {
|
|
$plain = 'a value';
|
|
$this->insert_v1_option( 'wpdo_test_idempotent', $plain );
|
|
|
|
$first = WPDO_Crypto::migrate_v1_to_v2( 'wpdo_' );
|
|
$second = WPDO_Crypto::migrate_v1_to_v2( 'wpdo_' );
|
|
|
|
// First run migrates 1, second run sees it as already_v2.
|
|
$this->assertSame( 1, $first['migrated'] );
|
|
$this->assertSame( 0, $second['migrated'] );
|
|
$this->assertSame( 1, $second['already_v2'] );
|
|
|
|
// Value still decrypts correctly after both runs.
|
|
$this->assertSame( $plain, WPDO_Crypto::get_option( 'wpdo_test_idempotent' ) );
|
|
}
|
|
|
|
public function test_migrate_empty_set(): void {
|
|
$counts = WPDO_Crypto::migrate_v1_to_v2( 'nonexistent_prefix_' );
|
|
|
|
$this->assertSame( 0, $counts['scanned'] );
|
|
$this->assertSame( 0, $counts['migrated'] );
|
|
$this->assertSame( 0, $counts['failed'] );
|
|
}
|
|
|
|
public function test_migrate_preserves_value_semantics(): void {
|
|
// Realistic test: write a webhook-shaped string that includes URL chars
|
|
// + special padding to make sure no encoding artifacts surface.
|
|
$plain = 'https://hooks.slack.com/services/T01/B02/=+&%/special?chars=true';
|
|
$this->insert_v1_option( 'wpdo_realistic_webhook', $plain );
|
|
|
|
WPDO_Crypto::migrate_v1_to_v2( 'wpdo_' );
|
|
|
|
$this->assertSame( $plain, WPDO_Crypto::get_option( 'wpdo_realistic_webhook' ) );
|
|
}
|
|
|
|
// ── Helpers ──────────────────────────────────────────────────────────────
|
|
|
|
/**
|
|
* Insert an option containing a hand-crafted v1 (CBC) ciphertext.
|
|
*/
|
|
private function insert_v1_option( string $name, string $plaintext ): void {
|
|
$key = substr( hash_hmac( 'sha256', 'wpdo_notifier_secrets_v1', AUTH_KEY . SECURE_AUTH_SALT, true ), 0, 32 );
|
|
$iv = random_bytes( 16 );
|
|
$ct = openssl_encrypt( $plaintext, 'AES-256-CBC', $key, OPENSSL_RAW_DATA, $iv );
|
|
$blob = WPDO_Crypto::PREFIX_V1 . base64_encode( $iv . $ct );
|
|
$this->set_option_raw( $name, $blob );
|
|
}
|
|
|
|
/**
|
|
* Write a raw option value directly (bypasses WPDO_Crypto::set_option).
|
|
*/
|
|
private function set_option_raw( string $name, string $value ): void {
|
|
global $wpdb;
|
|
$wpdb->query(
|
|
$wpdb->prepare(
|
|
'REPLACE INTO `' . self::TEST_PREFIX . 'options` (option_name, option_value, autoload) VALUES (%s, %s, %s)',
|
|
$name,
|
|
$value,
|
|
'no'
|
|
)
|
|
);
|
|
$GLOBALS['_wp_options'][ $name ] = $value;
|
|
}
|
|
}
|