diff --git a/.gitea/workflows/test.yml b/.gitea/workflows/test.yml index 0549f90..e1abcaa 100644 --- a/.gitea/workflows/test.yml +++ b/.gitea/workflows/test.yml @@ -44,3 +44,36 @@ jobs: - name: Run unit tests working-directory: 2meet-data-optimizer-woocommerce-addon run: php vendor/bin/phpunit --configuration phpunit.xml --testdox + + integration: + name: Integration Tests + runs-on: ubuntu-latest + # Host-mode runner: no service containers. Uses the host MariaDB, same as + # the core plugin's integration job. + steps: + - uses: actions/checkout@v4 + with: + path: 2meet-data-optimizer-woocommerce-addon + + - name: Checkout core plugin + uses: actions/checkout@v4 + with: + repository: wpdev/2meet-data-optimizer + path: 2meet-data-optimizer + + - name: Install dependencies + working-directory: 2meet-data-optimizer-woocommerce-addon + run: composer install --no-interaction --prefer-dist + + - name: Install core dev dependencies (test bootstrap needs them) + working-directory: 2meet-data-optimizer + run: composer install --no-interaction --prefer-dist + + - name: Run integration tests + working-directory: 2meet-data-optimizer-woocommerce-addon + env: + TMDO_TEST_DB_HOST: 127.0.0.1 + TMDO_TEST_DB_USER: dbo + TMDO_TEST_DB_PASS: ${{ secrets.TMDO_TEST_DB_PASS }} + TMDO_TEST_DB_NAME: wp_wpdo_test + run: php vendor/bin/phpunit --configuration phpunit-integration.xml --testdox diff --git a/phpunit-integration.xml b/phpunit-integration.xml new file mode 100644 index 0000000..22efc20 --- /dev/null +++ b/phpunit-integration.xml @@ -0,0 +1,15 @@ + + + + + tests/integration + + + diff --git a/tests/integration/WCTermCountFilterTest.php b/tests/integration/WCTermCountFilterTest.php new file mode 100644 index 0000000..75b276e --- /dev/null +++ b/tests/integration/WCTermCountFilterTest.php @@ -0,0 +1,186 @@ +query( + 'CREATE TABLE IF NOT EXISTS `' . self::TERMMETA . '` ( + meta_id bigint(20) unsigned NOT NULL AUTO_INCREMENT, + term_id bigint(20) unsigned NOT NULL DEFAULT 0, + meta_key varchar(255) DEFAULT NULL, + meta_value longtext, + PRIMARY KEY (meta_id), + KEY term_id (term_id), + KEY meta_key (meta_key(191)) + ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci' + ); + } + + protected function setUp(): void { + global $wpdb; + $wpdb->query( 'TRUNCATE TABLE `' . self::TERMMETA . '`' ); + $GLOBALS['_wp_options'] = array(); + update_option( WPDO_WC_Term_Count_Filter::OPT_ENABLED, '1' ); + } + + // ── is_target_key ──────────────────────────────────────────────────────── + + public function test_is_target_key_matches_product_count_prefix(): void { + $this->assertTrue( WPDO_WC_Term_Count_Filter::is_target_key( 'product_count_product_cat' ) ); + $this->assertTrue( WPDO_WC_Term_Count_Filter::is_target_key( 'product_count_product_tag' ) ); + $this->assertTrue( WPDO_WC_Term_Count_Filter::is_target_key( 'product_count_pa_color' ) ); + } + + public function test_is_target_key_rejects_non_product_count(): void { + $this->assertFalse( WPDO_WC_Term_Count_Filter::is_target_key( 'hp_sort_order' ) ); + $this->assertFalse( WPDO_WC_Term_Count_Filter::is_target_key( 'hp_default' ) ); + $this->assertFalse( WPDO_WC_Term_Count_Filter::is_target_key( 'product_count' ) ); // exact, not prefix + $this->assertFalse( WPDO_WC_Term_Count_Filter::is_target_key( 'something_product_count_foo' ) ); + } + + public function test_is_target_key_rejects_non_string(): void { + $this->assertFalse( WPDO_WC_Term_Count_Filter::is_target_key( null ) ); + $this->assertFalse( WPDO_WC_Term_Count_Filter::is_target_key( 123 ) ); + } + + // ── translate_key ──────────────────────────────────────────────────────── + + public function test_translate_key_namespaces_by_term_id(): void { + $key1 = WPDO_WC_Term_Count_Filter::translate_key( 5, 'product_count_product_cat' ); + $key2 = WPDO_WC_Term_Count_Filter::translate_key( 6, 'product_count_product_cat' ); + $this->assertNotSame( $key1, $key2 ); + $this->assertStringContainsString( 'wpdo_wc_termcount_5_', $key1 ); + $this->assertStringContainsString( 'wpdo_wc_termcount_6_', $key2 ); + } + + public function test_translate_key_md5_handles_long_taxonomy(): void { + $long_key = 'product_count_my_super_long_taxonomy_slug_' . str_repeat( 'x', 200 ); + $translated = WPDO_WC_Term_Count_Filter::translate_key( 1, $long_key ); + $option_name = '_transient_' . $translated; + $this->assertLessThanOrEqual( 191, strlen( $option_name ) ); + } + + // ── on_read ────────────────────────────────────────────────────────────── + + public function test_on_read_returns_pre_for_non_target_key(): void { + $result = WPDO_WC_Term_Count_Filter::on_read( null, 1, 'hp_sort_order', true ); + $this->assertNull( $result ); + } + + public function test_on_read_returns_value_when_present(): void { + $translated = WPDO_WC_Term_Count_Filter::translate_key( 5, 'product_count_product_cat' ); + update_option( '_transient_' . $translated, '42' ); + + $result = WPDO_WC_Term_Count_Filter::on_read( null, 5, 'product_count_product_cat', true ); + $this->assertSame( array( '42' ), $result ); + } + + public function test_on_read_falls_through_on_cache_miss(): void { + $result = WPDO_WC_Term_Count_Filter::on_read( null, 5, 'product_count_product_cat', true ); + $this->assertNull( $result, 'Cache miss should fall through (return $pre)' ); + } + + // ── on_add / on_update ─────────────────────────────────────────────────── + + public function test_on_add_writes_to_wp_options_and_short_circuits(): void { + $result = WPDO_WC_Term_Count_Filter::on_add( null, 5, 'product_count_product_cat', '42', false ); + $this->assertTrue( $result ); + + $translated = WPDO_WC_Term_Count_Filter::translate_key( 5, 'product_count_product_cat' ); + $this->assertSame( '42', get_option( '_transient_' . $translated ) ); + } + + public function test_on_update_writes_to_wp_options(): void { + WPDO_WC_Term_Count_Filter::on_update( null, 7, 'product_count_product_tag', '15', '' ); + $translated = WPDO_WC_Term_Count_Filter::translate_key( 7, 'product_count_product_tag' ); + $this->assertSame( '15', get_option( '_transient_' . $translated ) ); + } + + public function test_on_add_passes_through_non_target(): void { + $this->assertNull( WPDO_WC_Term_Count_Filter::on_add( null, 5, 'hp_sort_order', '5', false ) ); + } + + // ── on_delete ──────────────────────────────────────────────────────────── + + public function test_on_delete_clears_wp_options_entry(): void { + WPDO_WC_Term_Count_Filter::on_update( null, 5, 'product_count_product_cat', '42', '' ); + $translated = WPDO_WC_Term_Count_Filter::translate_key( 5, 'product_count_product_cat' ); + $this->assertSame( '42', get_option( '_transient_' . $translated ) ); + + WPDO_WC_Term_Count_Filter::on_delete( null, 5, 'product_count_product_cat', '', false ); + $this->assertFalse( get_option( '_transient_' . $translated ) ); + } + + // ── Round-trip ─────────────────────────────────────────────────────────── + + public function test_full_round_trip(): void { + WPDO_WC_Term_Count_Filter::on_update( null, 12, 'product_count_product_cat', 'serialized_value', '' ); + + $value = WPDO_WC_Term_Count_Filter::on_read( null, 12, 'product_count_product_cat', true ); + $this->assertSame( array( 'serialized_value' ), $value ); + } + + // ── count/purge legacy ────────────────────────────────────────────────── + + public function test_count_legacy_termmeta_returns_zero_for_clean(): void { + $this->assertSame( 0, WPDO_WC_Term_Count_Filter::count_legacy_termmeta_rows() ); + } + + public function test_count_legacy_termmeta_counts_only_product_count(): void { + global $wpdb; + $rows = array( + array( 'term_id' => 1, 'meta_key' => 'product_count_product_cat', 'meta_value' => '42' ), + array( 'term_id' => 2, 'meta_key' => 'product_count_product_tag', 'meta_value' => '15' ), + array( 'term_id' => 1, 'meta_key' => 'hp_sort_order', 'meta_value' => '5' ), // not target + ); + foreach ( $rows as $r ) { + $wpdb->insert( self::TERMMETA, $r ); + } + + $this->assertSame( 2, WPDO_WC_Term_Count_Filter::count_legacy_termmeta_rows() ); + } + + public function test_purge_legacy_termmeta_deletes_only_product_count(): void { + global $wpdb; + $wpdb->insert( self::TERMMETA, array( 'term_id' => 1, 'meta_key' => 'product_count_product_cat', 'meta_value' => '42' ) ); + $wpdb->insert( self::TERMMETA, array( 'term_id' => 2, 'meta_key' => 'hp_sort_order', 'meta_value' => '5' ) ); + + $deleted = WPDO_WC_Term_Count_Filter::purge_legacy_termmeta_rows(); + $this->assertSame( 1, $deleted ); + + $remaining = (int) $wpdb->get_var( 'SELECT COUNT(*) FROM `' . self::TERMMETA . '`' ); + $this->assertSame( 1, $remaining ); + } + + // ── is_enabled ────────────────────────────────────────────────────────── + + public function test_is_enabled_defaults_true(): void { + delete_option( WPDO_WC_Term_Count_Filter::OPT_ENABLED ); + $this->assertTrue( WPDO_WC_Term_Count_Filter::is_enabled() ); + } + + public function test_is_enabled_respects_zero_value(): void { + update_option( WPDO_WC_Term_Count_Filter::OPT_ENABLED, '0' ); + $this->assertFalse( WPDO_WC_Term_Count_Filter::is_enabled() ); + } +} diff --git a/tests/integration/bootstrap.php b/tests/integration/bootstrap.php new file mode 100644 index 0000000..0a6d383 --- /dev/null +++ b/tests/integration/bootstrap.php @@ -0,0 +1,52 @@ +