Files
2meet-data-optimizer/tests/integration/CryptoMigrationTest.php
T
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

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