query( 'DROP TABLE IF EXISTS `' . self::POSTS . '`' ); $wpdb->query( 'CREATE TABLE `' . self::POSTS . '` ( ID bigint(20) unsigned NOT NULL AUTO_INCREMENT, post_type varchar(20) NOT NULL DEFAULT \'post\', post_status varchar(20) NOT NULL DEFAULT \'publish\', PRIMARY KEY (ID), KEY post_type (post_type) ) DEFAULT CHARACTER SET utf8mb4' ); $wpdb->query( 'DROP TABLE IF EXISTS `' . self::POSTMETA . '`' ); $wpdb->query( 'CREATE TABLE `' . self::POSTMETA . '` ( meta_id bigint(20) unsigned NOT NULL AUTO_INCREMENT, post_id bigint(20) unsigned NOT NULL DEFAULT 0, meta_key varchar(255) DEFAULT NULL, meta_value longtext, PRIMARY KEY (meta_id), KEY post_id (post_id), KEY meta_key (meta_key(191)) ) DEFAULT CHARACTER SET utf8mb4' ); $wpdb->query( 'DROP TABLE IF EXISTS `' . self::FLAT . '`' ); $wpdb->query( 'CREATE TABLE `' . self::FLAT . '` ( id bigint(20) unsigned NOT NULL AUTO_INCREMENT, post_id bigint(20) unsigned NOT NULL, _price decimal(18,6) DEFAULT NULL, _stock_status varchar(100) DEFAULT NULL, _sku longtext DEFAULT NULL, PRIMARY KEY (id), UNIQUE KEY uk_post_id (post_id) ) DEFAULT CHARACTER SET utf8mb4' ); WPDO_Entity_Registry::init(); WPDO_Entity_Registry::register_adapter( 'post', new WPDO_Adapter_Post() ); WPDO_Post_Fields::register_entity_fields(); } public static function tearDownAfterClass(): void { global $wpdb; $wpdb->query( 'DROP TABLE IF EXISTS `' . self::POSTS . '`' ); $wpdb->query( 'DROP TABLE IF EXISTS `' . self::POSTMETA . '`' ); $wpdb->query( 'DROP TABLE IF EXISTS `' . self::FLAT . '`' ); WPDO_Entity_Registry::init(); } protected function setUp(): void { global $wpdb; $wpdb->query( 'TRUNCATE TABLE `' . self::POSTS . '`' ); $wpdb->query( 'TRUNCATE TABLE `' . self::POSTMETA . '`' ); $wpdb->query( 'TRUNCATE TABLE `' . self::FLAT . '`' ); } private function seed_consistent( int $post_id, string $price, string $stock ): void { global $wpdb; $wpdb->insert( self::POSTS, array( 'ID' => $post_id, 'post_type' => 'product', 'post_status' => 'publish' ) ); $wpdb->insert( self::POSTMETA, array( 'post_id' => $post_id, 'meta_key' => '_price', 'meta_value' => $price ) ); $wpdb->insert( self::POSTMETA, array( 'post_id' => $post_id, 'meta_key' => '_stock_status', 'meta_value' => $stock ) ); $wpdb->insert( self::FLAT, array( 'post_id' => $post_id, '_price' => $price, '_stock_status' => $stock ) ); } private function seed_diverged( int $post_id, string $pm_price, string $flat_price ): void { global $wpdb; $wpdb->insert( self::POSTS, array( 'ID' => $post_id, 'post_type' => 'product', 'post_status' => 'publish' ) ); $wpdb->insert( self::POSTMETA, array( 'post_id' => $post_id, 'meta_key' => '_price', 'meta_value' => $pm_price ) ); $wpdb->insert( self::FLAT, array( 'post_id' => $post_id, '_price' => $flat_price ) ); } // ── sample_compare() ───────────────────────────────────────────────────── public function test_returns_expected_shape(): void { $result = WPDO_Post_Shadow_Verifier::sample_compare( 'product', 'wc_product', self::FLAT, array( '_price', '_stock_status' ), 5 ); $this->assertArrayHasKey( 'sampled', $result ); $this->assertArrayHasKey( 'matched', $result ); $this->assertArrayHasKey( 'diffs', $result ); $this->assertArrayHasKey( 'missing_flat', $result ); $this->assertArrayHasKey( 'missing_postmeta', $result ); } public function test_all_match_when_data_is_consistent(): void { for ( $i = 1; $i <= 5; $i++ ) { $this->seed_consistent( $i, '50.00', 'instock' ); } $result = WPDO_Post_Shadow_Verifier::sample_compare( 'product', 'wc_product', self::FLAT, array( '_price' ), 5 ); $this->assertSame( 5, $result['sampled'] ); $this->assertSame( 5, $result['matched'] ); $this->assertSame( 0, $result['diffs'] ); $this->assertSame( 0, $result['missing_flat'] ); } public function test_detects_divergence_between_flat_and_postmeta(): void { // Two diverged: pm has 50, flat has 60. $this->seed_diverged( 1, '50', '60' ); $this->seed_diverged( 2, '99', '88' ); $result = WPDO_Post_Shadow_Verifier::sample_compare( 'product', 'wc_product', self::FLAT, array( '_price' ), 5 ); $this->assertSame( 2, $result['sampled'] ); $this->assertSame( 0, $result['matched'] ); $this->assertSame( 2, $result['diffs'] ); } public function test_counts_missing_flat_when_flat_row_absent(): void { // Post + postmeta exist, but no flat row. global $wpdb; $wpdb->insert( self::POSTS, array( 'ID' => 100, 'post_type' => 'product', 'post_status' => 'publish' ) ); $wpdb->insert( self::POSTMETA, array( 'post_id' => 100, 'meta_key' => '_price', 'meta_value' => '99' ) ); $result = WPDO_Post_Shadow_Verifier::sample_compare( 'product', 'wc_product', self::FLAT, array( '_price' ), 5 ); $this->assertSame( 1, $result['sampled'] ); $this->assertSame( 1, $result['missing_flat'] ); } public function test_counts_missing_postmeta_when_pm_absent(): void { // Post + flat exist, but no postmeta. global $wpdb; $wpdb->insert( self::POSTS, array( 'ID' => 200, 'post_type' => 'product', 'post_status' => 'publish' ) ); $wpdb->insert( self::FLAT, array( 'post_id' => 200, '_price' => '50' ) ); $result = WPDO_Post_Shadow_Verifier::sample_compare( 'product', 'wc_product', self::FLAT, array( '_price' ), 5 ); $this->assertSame( 1, $result['sampled'] ); $this->assertSame( 1, $result['missing_postmeta'] ); } public function test_returns_zero_when_no_posts_of_type(): void { $result = WPDO_Post_Shadow_Verifier::sample_compare( 'product', 'wc_product', self::FLAT, array( '_price' ), 5 ); $this->assertSame( 0, $result['sampled'] ); $this->assertSame( 0, $result['matched'] ); } public function test_caps_sample_at_available_post_count(): void { // 3 posts, ask for 10 samples → only 3 sampled. $this->seed_consistent( 1, '10', 'instock' ); $this->seed_consistent( 2, '20', 'instock' ); $this->seed_consistent( 3, '30', 'instock' ); $result = WPDO_Post_Shadow_Verifier::sample_compare( 'product', 'wc_product', self::FLAT, array( '_price' ), 10 ); $this->assertSame( 3, $result['sampled'] ); $this->assertSame( 3, $result['matched'] ); } public function test_rejects_zero_sample_size(): void { $this->expectException( InvalidArgumentException::class ); WPDO_Post_Shadow_Verifier::sample_compare( 'product', 'wc_product', self::FLAT, array( '_price' ), 0 ); } public function test_rejects_empty_keys(): void { $this->expectException( InvalidArgumentException::class ); WPDO_Post_Shadow_Verifier::sample_compare( 'product', 'wc_product', self::FLAT, array(), 5 ); } // ── v2.10.5: serialize vs JSON loose equality ──────────────────────────── public function test_treats_serialized_array_equal_to_json_array(): void { // pm side has serialized array; flat side has JSON for the same data. // post_id=10, key=_stock_status (we reuse this column to inject test values). // Use a dedicated key for clarity by re-purposing the keys array. global $wpdb; $wpdb->insert( self::POSTS, array( 'ID' => 10, 'post_type' => 'product', 'post_status' => 'publish' ) ); $wpdb->insert( self::POSTMETA, array( 'post_id' => 10, 'meta_key' => '_sku', 'meta_value' => serialize( array( 'a', 'b', 'c' ) ), ) ); $wpdb->insert( self::FLAT, array( 'post_id' => 10, '_sku' => wp_json_encode( array( 'a', 'b', 'c' ) ), ) ); $result = WPDO_Post_Shadow_Verifier::sample_compare( 'product', 'wc_product', self::FLAT, array( '_sku' ), 5 ); $this->assertSame( 1, $result['matched'], 'serialized array vs JSON-encoded same array should match.' ); $this->assertSame( 0, $result['diffs'], 'No false-positive diff.' ); } public function test_treats_serialized_assoc_equal_to_json_assoc(): void { global $wpdb; $assoc = array( 'width' => 100, 'height' => 200 ); $wpdb->insert( self::POSTS, array( 'ID' => 11, 'post_type' => 'product', 'post_status' => 'publish' ) ); $wpdb->insert( self::POSTMETA, array( 'post_id' => 11, 'meta_key' => '_sku', 'meta_value' => serialize( $assoc ), ) ); $wpdb->insert( self::FLAT, array( 'post_id' => 11, '_sku' => wp_json_encode( $assoc ), ) ); $result = WPDO_Post_Shadow_Verifier::sample_compare( 'product', 'wc_product', self::FLAT, array( '_sku' ), 5 ); $this->assertSame( 1, $result['matched'] ); $this->assertSame( 0, $result['diffs'] ); } public function test_genuine_diff_still_detected_after_loose_equal_widening(): void { // Real divergence — must still be flagged even with loosened compare. global $wpdb; $wpdb->insert( self::POSTS, array( 'ID' => 12, 'post_type' => 'product', 'post_status' => 'publish' ) ); $wpdb->insert( self::POSTMETA, array( 'post_id' => 12, 'meta_key' => '_sku', 'meta_value' => serialize( array( 1, 2, 3 ) ), ) ); $wpdb->insert( self::FLAT, array( 'post_id' => 12, '_sku' => wp_json_encode( array( 9, 9, 9 ) ), ) ); $result = WPDO_Post_Shadow_Verifier::sample_compare( 'product', 'wc_product', self::FLAT, array( '_sku' ), 5 ); $this->assertSame( 0, $result['matched'] ); $this->assertSame( 1, $result['diffs'] ); } }