diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml new file mode 100644 index 0000000000..20475ee617 --- /dev/null +++ b/.github/workflows/unit-tests.yml @@ -0,0 +1,123 @@ +name: Unit Tests + +on: + push: + branches: [ trunk ] + pull_request: + branches: [ trunk ] + +jobs: + # Standalone PHP tests — no WordPress or database dependency. + php-standalone: + name: "PHP: ${{ matrix.name }}" + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + include: + - name: Serve Happy API + working-directory: api.wordpress.org/public_html/core/serve-happy/1.0 + phpunit-args: "--configuration phpunit.xml --exclude-group serve-happy-live-http" + bootstrap: compat + - name: Browse Happy API + working-directory: api.wordpress.org/public_html/core/browse-happy/1.0 + phpunit-args: "--configuration phpunit.xml" + bootstrap: none + - name: Slack Trac Bot + working-directory: common/includes/tests/slack/trac + phpunit-args: "bot.php" + bootstrap: compat + - name: Slack Props Library + working-directory: common/includes/slack/props/tests + phpunit-args: "." + bootstrap: wpdb-stub + steps: + - uses: actions/checkout@v4 + + - name: Set up PHP + uses: shivammathur/setup-php@v2 + with: + php-version: "8.0" + tools: phpunit:^9 + + - name: Create PHPUnit compatibility bootstrap + if: matrix.bootstrap == 'compat' + run: | + cat > /tmp/phpunit-bootstrap.php << 'PHPEOF' + /tmp/phpunit-bootstrap.php << 'PHPEOF' + assertTrue( $parsed['upgrade'] ); + return; + } + $versions = get_browser_current_versions(); if ( ! empty( $versions[ $parsed['name'] ] ) ) { diff --git a/wordpress.org/public_html/wp-content/plugins/handbook/phpunit/bootstrap.php b/wordpress.org/public_html/wp-content/plugins/handbook/phpunit/bootstrap.php index 294962ca04..20df609eaf 100644 --- a/wordpress.org/public_html/wp-content/plugins/handbook/phpunit/bootstrap.php +++ b/wordpress.org/public_html/wp-content/plugins/handbook/phpunit/bootstrap.php @@ -14,6 +14,10 @@ if ( ! $_tests_dir && false !== ( $pos = stripos( __FILE__, '/src/wp-content/plugins/' ) ) ) { $_tests_dir = substr( __FILE__, 0, $pos ) . '/tests/phpunit/'; } +// Check for wp-env test directory. +elseif ( ! $_tests_dir && file_exists( '/wordpress-phpunit/includes/functions.php' ) ) { + $_tests_dir = '/wordpress-phpunit/'; +} // Elseif no path yet, assume a temp directory path. elseif ( ! $_tests_dir ) { $_tests_dir = rtrim( sys_get_temp_dir(), '/\\' ) . '/wordpress-tests-lib/tests/phpunit/'; diff --git a/wordpress.org/public_html/wp-content/plugins/handbook/phpunit/tests/handbook.php b/wordpress.org/public_html/wp-content/plugins/handbook/phpunit/tests/handbook.php index 9199c54ccc..2c5e00429f 100644 --- a/wordpress.org/public_html/wp-content/plugins/handbook/phpunit/tests/handbook.php +++ b/wordpress.org/public_html/wp-content/plugins/handbook/phpunit/tests/handbook.php @@ -17,7 +17,7 @@ class WPorg_Handbook_Handbook_Test extends WP_UnitTestCase { protected $handbook; - public function setUp() { + public function setUp(): void { parent::setup(); WPorg_Handbook_Init::init(); @@ -25,7 +25,7 @@ public function setUp() { $this->handbook = reset( $handbooks ); } - public function tearDown() { + public function tearDown(): void { parent::tearDown(); foreach ( WPorg_Handbook_Init::get_handbook_objects() as $obj ) { @@ -455,7 +455,7 @@ public function test_handbook_sidebar() { $this->assertTrue( isset( $wp_registered_sidebars[ 'handbook' ] ) ); $this->assertSame( - [ 'name', 'id', 'description', 'class', 'before_widget', 'after_widget', 'before_title', 'after_title', 'before_sidebar', 'after_sidebar' ], + [ 'name', 'id', 'description', 'class', 'before_widget', 'after_widget', 'before_title', 'after_title', 'before_sidebar', 'after_sidebar', 'show_in_rest' ], array_keys( $wp_registered_sidebars[ 'handbook' ] ) ); $this->assertEquals( 'handbook', $wp_registered_sidebars[ 'handbook' ]['id'] ); diff --git a/wordpress.org/public_html/wp-content/plugins/handbook/phpunit/tests/init.php b/wordpress.org/public_html/wp-content/plugins/handbook/phpunit/tests/init.php index 1e1fd2f050..9b03669507 100644 --- a/wordpress.org/public_html/wp-content/plugins/handbook/phpunit/tests/init.php +++ b/wordpress.org/public_html/wp-content/plugins/handbook/phpunit/tests/init.php @@ -4,13 +4,13 @@ class WPorg_Handbook_Init_Test extends WP_UnitTestCase { - public function setUp() { + public function setUp(): void { parent::setup(); WPorg_Handbook_Init::init(); } - public function tearDown() { + public function tearDown(): void { parent::tearDown(); WPorg_Handbook_Init::reset( true ); diff --git a/wordpress.org/public_html/wp-content/plugins/handbook/phpunit/tests/template-tags.php b/wordpress.org/public_html/wp-content/plugins/handbook/phpunit/tests/template-tags.php index aa9b6d91b5..acf9dbad22 100644 --- a/wordpress.org/public_html/wp-content/plugins/handbook/phpunit/tests/template-tags.php +++ b/wordpress.org/public_html/wp-content/plugins/handbook/phpunit/tests/template-tags.php @@ -4,13 +4,13 @@ class WPorg_Handbook_Template_Tags_Test extends WP_UnitTestCase { - public function setUp() { + public function setUp(): void { parent::setup(); WPorg_Handbook_Init::init(); } - public function tearDown() { + public function tearDown(): void { parent::tearDown(); WPorg_Handbook_Init::reset( true ); diff --git a/wordpress.org/public_html/wp-content/plugins/plugin-directory/.wp-env.json b/wordpress.org/public_html/wp-content/plugins/plugin-directory/.wp-env.json new file mode 100644 index 0000000000..970c05e2a2 --- /dev/null +++ b/wordpress.org/public_html/wp-content/plugins/plugin-directory/.wp-env.json @@ -0,0 +1,16 @@ +{ + "core": "WordPress/WordPress#master", + "plugins": [ + ".", + "https://downloads.wordpress.org/plugin/jetpack.latest-stable.zip", + "https://downloads.wordpress.org/plugin/advanced-post-cache.latest-stable.zip", + "https://github.com/WordPress/plugin-check" + ], + "themes": [ + "../../../themes/pub/wporg-plugins-2024", + "https://github.com/WordPress/wporg-parent-2021" + ], + "mappings": { + "wp-content/mu-plugins/wporg-ratings-stub.php": "./tests/stubs/wporg-ratings-stub.php" + } +} diff --git a/wordpress.org/public_html/wp-content/plugins/plugin-directory/tests/bootstrap.php b/wordpress.org/public_html/wp-content/plugins/plugin-directory/tests/bootstrap.php index 4a387afef9..e2b66e38b3 100644 --- a/wordpress.org/public_html/wp-content/plugins/plugin-directory/tests/bootstrap.php +++ b/wordpress.org/public_html/wp-content/plugins/plugin-directory/tests/bootstrap.php @@ -6,6 +6,29 @@ return; } +// Find the WordPress PHPUnit test framework. +$_tests_dir = getenv( 'WP_TESTS_DIR' ); + +if ( ! $_tests_dir ) { + // wp-env test directory. + if ( file_exists( '/wordpress-phpunit/includes/functions.php' ) ) { + $_tests_dir = '/wordpress-phpunit/'; + } else { + $_tests_dir = rtrim( sys_get_temp_dir(), '/\\' ) . '/wordpress-tests-lib/tests/phpunit/'; + } +} + +if ( ! file_exists( $_tests_dir . '/includes/functions.php' ) ) { + echo "Could not find {$_tests_dir}/includes/functions.php\n"; + exit( 1 ); +} + +// Give access to tests_add_filter() function. +require_once $_tests_dir . '/includes/functions.php'; + +// Load Jetpack Search stub if Jetpack is not installed. +require_once __DIR__ . '/stubs/jetpack-search-stub.php'; + /** * Manually load the plugin being tested. */ @@ -14,3 +37,6 @@ function manually_load_plugin() { } tests_add_filter( 'muplugins_loaded', __NAMESPACE__ . '\manually_load_plugin' ); + +// Start up the WP testing environment. +require $_tests_dir . '/includes/bootstrap.php'; diff --git a/wordpress.org/public_html/wp-content/plugins/plugin-directory/tests/phpunit/tests/test-markdown.php b/wordpress.org/public_html/wp-content/plugins/plugin-directory/tests/phpunit/tests/test-markdown.php new file mode 100644 index 0000000000..f7ded4b9d7 --- /dev/null +++ b/wordpress.org/public_html/wp-content/plugins/plugin-directory/tests/phpunit/tests/test-markdown.php @@ -0,0 +1,132 @@ +transform( 'Hello World' ); + $this->assertSame( '
Hello World
', $result ); + } + + function test_transform_empty_string(): void { + $md = new Markdown(); + $result = $md->transform( '' ); + $this->assertSame( '', $result ); + } + + function test_transform_trims_output(): void { + $md = new Markdown(); + $result = $md->transform( " \n Hello \n " ); + $this->assertSame( $result, trim( $result ) ); + } + + /** + * Test the custom `= Section Title =` header syntax. + * + * This is WordPress plugin readme specific — converts `= Title =` to blocks preserve underscores in code.
+ *
+ * This is custom logic in Markdown::code_trick() — pre-existing HTML code blocks
+ * are converted to backtick format before markdown processing, so markdown
+ * does not mangle underscores and other special characters inside code.
+ *
+ * The block needs surrounding content so trim() does not strip indentation.
+ */
+ function test_code_trick_preserves_underscores_in_pre_code(): void {
+ $md = new Markdown();
+ $input = "Some text before.\n\n\$my_var = some_function();\n\$other_var = 1;
\n\nSome text after.";
+ $result = $md->transform( $input );
+
+ // Underscores should NOT be converted to tags inside code blocks.
+ $this->assertStringNotContainsString( '', $result );
+ $this->assertStringContainsString( 'my_var', $result );
+ $this->assertStringContainsString( 'some_function', $result );
+ }
+
+ function test_code_trick_inline_code_preserves_underscores(): void {
+ $md = new Markdown();
+ $input = "Use my_var_name for the setting.";
+ $result = $md->transform( $input );
+
+ // Inline code should also preserve underscores.
+ $this->assertStringNotContainsString( '', $result );
+ $this->assertStringContainsString( 'my_var_name', $result );
+ }
+
+ /**
+ * Test code_trick: bbPress-style backtick code blocks at line start are
+ * converted to indented code (4 spaces) for markdown processing.
+ */
+ function test_code_trick_bbpress_backtick_block(): void {
+ $md = new Markdown();
+ $input = "Some text.\n\n`some_code_here`\n\nMore text.";
+ $result = $md->transform( $input );
+
+ $this->assertStringContainsString( 'some_code_here', $result );
+ }
+
+ /**
+ * Test that inline markdown code (backticks) in mid-line is preserved.
+ */
+ function test_inline_backtick_code_preserved(): void {
+ $md = new Markdown();
+ $result = $md->transform( 'Use `add_filter()` to modify output.' );
+ $this->assertStringContainsString( 'add_filter()', $result );
+ }
+
+ /**
+ * Test standard markdown features (these verify the upstream MarkdownExtra
+ * library works correctly through our transform() wrapper).
+ */
+ function test_transform_bold(): void {
+ $md = new Markdown();
+ $result = $md->transform( '**bold text**' );
+ $this->assertStringContainsString( 'bold text', $result );
+ }
+
+ function test_transform_italic(): void {
+ $md = new Markdown();
+ $result = $md->transform( '*italic text*' );
+ $this->assertStringContainsString( 'italic text', $result );
+ }
+
+ function test_transform_link(): void {
+ $md = new Markdown();
+ $result = $md->transform( '[Example](https://example.com)' );
+ $this->assertStringContainsString( 'Example', $result );
+ }
+
+ function test_transform_unordered_list(): void {
+ $md = new Markdown();
+ $result = $md->transform( "* Item 1\n* Item 2\n* Item 3" );
+ $this->assertStringContainsString( 'Item 1 ', $result );
+ $this->assertStringContainsString( '', $result );
+ }
+}
diff --git a/wordpress.org/public_html/wp-content/plugins/plugin-directory/tests/phpunit/tests/test-plugin-directory.php b/wordpress.org/public_html/wp-content/plugins/plugin-directory/tests/phpunit/tests/test-plugin-directory.php
new file mode 100644
index 0000000000..2c44e870de
--- /dev/null
+++ b/wordpress.org/public_html/wp-content/plugins/plugin-directory/tests/phpunit/tests/test-plugin-directory.php
@@ -0,0 +1,259 @@
+post->create( array_merge( [
+ 'post_type' => 'plugin',
+ 'post_status' => 'publish',
+ 'post_modified' => $now,
+ 'post_modified_gmt' => $gmt,
+ ], $args ) );
+ }
+
+ /**
+ * Test that the 'plugin' post type is registered.
+ */
+ function test_plugin_post_type_registered(): void {
+ $this->assertTrue( post_type_exists( 'plugin' ) );
+ }
+
+ function test_plugin_post_type_is_public(): void {
+ $post_type = get_post_type_object( 'plugin' );
+ $this->assertTrue( $post_type->public );
+ }
+
+ function test_plugin_post_type_has_rest_support(): void {
+ $post_type = get_post_type_object( 'plugin' );
+ $this->assertTrue( $post_type->show_in_rest );
+ }
+
+ /**
+ * Test that expected taxonomies are registered.
+ *
+ * @dataProvider data_expected_taxonomies
+ */
+ function test_taxonomy_registered( $taxonomy ): void {
+ $this->assertTrue( taxonomy_exists( $taxonomy ), "Taxonomy '{$taxonomy}' should be registered." );
+ }
+
+ function data_expected_taxonomies(): array {
+ return [
+ 'plugin_section' => [ 'plugin_section' ],
+ 'plugin_tags' => [ 'plugin_tags' ],
+ 'plugin_category' => [ 'plugin_category' ],
+ 'plugin_contributors' => [ 'plugin_contributors' ],
+ 'plugin_built_for' => [ 'plugin_built_for' ],
+ 'plugin_business_model' => [ 'plugin_business_model' ],
+ 'plugin_committers' => [ 'plugin_committers' ],
+ 'plugin_support_reps' => [ 'plugin_support_reps' ],
+ ];
+ }
+
+ /**
+ * Test that custom post statuses are registered.
+ *
+ * @dataProvider data_expected_post_statuses
+ */
+ function test_post_status_registered( $status ): void {
+ $this->assertNotFalse(
+ get_post_status_object( $status ),
+ "Post status '{$status}' should be registered."
+ );
+ }
+
+ function data_expected_post_statuses(): array {
+ return [
+ 'new' => [ 'new' ],
+ 'pending' => [ 'pending' ],
+ 'disabled' => [ 'disabled' ],
+ 'approved' => [ 'approved' ],
+ 'closed' => [ 'closed' ],
+ 'rejected' => [ 'rejected' ],
+ ];
+ }
+
+ /**
+ * Test that a plugin post can be created.
+ */
+ function test_can_create_plugin_post(): void {
+ $post_id = $this->create_plugin_post( [
+ 'post_title' => 'Test Tool',
+ 'post_name' => 'test-tool',
+ ] );
+
+ $this->assertIsInt( $post_id );
+ $this->assertGreaterThan( 0, $post_id );
+
+ $post = get_post( $post_id );
+ $this->assertSame( 'plugin', $post->post_type );
+ $this->assertSame( 'Test Tool', $post->post_title );
+ }
+
+ /**
+ * Test that plugin meta fields can be stored and retrieved.
+ */
+ function test_plugin_meta_fields(): void {
+ $post_id = $this->create_plugin_post( [ 'post_name' => 'meta-test' ] );
+
+ update_post_meta( $post_id, 'stable_tag', '2.0.0' );
+ update_post_meta( $post_id, 'tested', '6.4' );
+ update_post_meta( $post_id, 'requires', '5.0' );
+ update_post_meta( $post_id, 'requires_php', '7.4' );
+ update_post_meta( $post_id, 'active_installs', 50000 );
+ update_post_meta( $post_id, 'downloads', 100000 );
+ update_post_meta( $post_id, 'rating', 90 );
+
+ $this->assertSame( '2.0.0', get_post_meta( $post_id, 'stable_tag', true ) );
+ $this->assertSame( '6.4', get_post_meta( $post_id, 'tested', true ) );
+ $this->assertSame( '5.0', get_post_meta( $post_id, 'requires', true ) );
+ $this->assertSame( '7.4', get_post_meta( $post_id, 'requires_php', true ) );
+ $this->assertEquals( 50000, get_post_meta( $post_id, 'active_installs', true ) );
+ $this->assertEquals( 100000, get_post_meta( $post_id, 'downloads', true ) );
+ $this->assertEquals( 90, get_post_meta( $post_id, 'rating', true ) );
+ }
+
+ /**
+ * Test that terms can be assigned to the plugin_tags taxonomy.
+ */
+ function test_assign_plugin_tags(): void {
+ $post_id = $this->create_plugin_post();
+
+ wp_set_object_terms( $post_id, [ 'seo', 'performance' ], 'plugin_tags' );
+
+ $terms = wp_get_object_terms( $post_id, 'plugin_tags', [ 'fields' => 'names' ] );
+ $this->assertContains( 'seo', $terms );
+ $this->assertContains( 'performance', $terms );
+ }
+
+ /**
+ * Test that terms can be assigned to the plugin_contributors taxonomy.
+ */
+ function test_assign_plugin_contributors(): void {
+ $post_id = $this->create_plugin_post();
+
+ wp_set_object_terms( $post_id, [ 'johndoe' ], 'plugin_contributors' );
+
+ $terms = wp_get_object_terms( $post_id, 'plugin_contributors', [ 'fields' => 'names' ] );
+ $this->assertContains( 'johndoe', $terms );
+ }
+
+ /**
+ * Test disabled post status is public (visible to non-logged-in users).
+ */
+ function test_disabled_status_is_public(): void {
+ $status = get_post_status_object( 'disabled' );
+ $this->assertTrue( $status->public );
+ }
+
+ /**
+ * Test closed post status is public (visible to non-logged-in users).
+ */
+ function test_closed_status_is_public(): void {
+ $status = get_post_status_object( 'closed' );
+ $this->assertTrue( $status->public );
+ }
+
+ /**
+ * Test that 'new' post status is not public.
+ */
+ function test_new_status_is_not_public(): void {
+ $status = get_post_status_object( 'new' );
+ $this->assertFalse( $status->public );
+ }
+
+ /**
+ * Test filter_wp_insert_post_data preserves post_modified for plugin posts.
+ */
+ function test_filter_wp_insert_post_data_preserves_modified(): void {
+ $instance = Plugin_Directory::instance();
+
+ $data = [
+ 'post_modified' => '2024-01-15 10:00:00',
+ 'post_modified_gmt' => '2024-01-15 10:00:00',
+ 'post_status' => 'publish',
+ 'post_name' => 'test',
+ ];
+ $postarr = [
+ 'post_type' => 'plugin',
+ 'post_modified' => '2024-01-15 10:00:00',
+ 'post_modified_gmt' => '2024-01-15 10:00:00',
+ 'post_status' => 'publish',
+ ];
+
+ $result = $instance->filter_wp_insert_post_data( $data, $postarr );
+
+ $this->assertSame( '2024-01-15 10:00:00', $result['post_modified'] );
+ $this->assertSame( '2024-01-15 10:00:00', $result['post_modified_gmt'] );
+ }
+
+ /**
+ * Test filter_wp_insert_post_data ignores non-plugin post types.
+ */
+ function test_filter_wp_insert_post_data_ignores_non_plugin(): void {
+ $instance = Plugin_Directory::instance();
+
+ $data = [
+ 'post_modified' => '2024-01-15 10:00:00',
+ 'post_modified_gmt' => '2024-01-15 10:00:00',
+ 'post_status' => 'publish',
+ 'post_name' => 'test',
+ ];
+ $postarr = [
+ 'post_type' => 'post',
+ 'post_modified' => '2024-06-01 12:00:00',
+ 'post_modified_gmt' => '2024-06-01 12:00:00',
+ ];
+
+ $result = $instance->filter_wp_insert_post_data( $data, $postarr );
+
+ // Should return data unchanged for non-plugin types.
+ $this->assertSame( $data, $result );
+ }
+
+ /**
+ * Test filter_wp_insert_post_data preserves slug for pending plugin posts.
+ */
+ function test_filter_wp_insert_post_data_preserves_pending_slug(): void {
+ $instance = Plugin_Directory::instance();
+
+ $now = current_time( 'mysql' );
+ $gmt = current_time( 'mysql', true );
+
+ $post_id = $this->create_plugin_post( [
+ 'post_name' => 'my-pending-tool',
+ 'post_status' => 'pending',
+ ] );
+
+ $data = [
+ 'post_modified' => $now,
+ 'post_modified_gmt' => $gmt,
+ 'post_status' => 'pending',
+ 'post_name' => '', // WP clears slug for pending posts.
+ ];
+ $postarr = [
+ 'post_type' => 'plugin',
+ 'post_modified' => $now,
+ 'post_modified_gmt' => $gmt,
+ 'post_status' => 'pending',
+ 'ID' => $post_id,
+ ];
+
+ $result = $instance->filter_wp_insert_post_data( $data, $postarr );
+
+ $this->assertSame( 'my-pending-tool', $result['post_name'] );
+ }
+}
diff --git a/wordpress.org/public_html/wp-content/plugins/plugin-directory/tests/phpunit/tests/test-readme-parser.php b/wordpress.org/public_html/wp-content/plugins/plugin-directory/tests/phpunit/tests/test-readme-parser.php
new file mode 100644
index 0000000000..49f0124acc
--- /dev/null
+++ b/wordpress.org/public_html/wp-content/plugins/plugin-directory/tests/phpunit/tests/test-readme-parser.php
@@ -0,0 +1,243 @@
+assertSame( 'My Test Plugin', $parser->name );
+ }
+
+ function test_parses_contributors(): void {
+ // Create a user so the contributor sanitization finds them.
+ self::factory()->user->create( [ 'user_login' => 'johndoe', 'user_nicename' => 'johndoe' ] );
+
+ $parser = new Parser( self::$valid_readme );
+ $this->assertSame( [ 'johndoe' ], $parser->contributors );
+ }
+
+ function test_invalid_contributor_produces_warning(): void {
+ $parser = new Parser( self::$valid_readme );
+
+ // 'johndoe' does not exist as a WP user, so it should be ignored with a warning.
+ $this->assertEmpty( $parser->contributors );
+ $this->assertArrayHasKey( 'contributor_ignored', $parser->warnings );
+ }
+
+ function test_parses_tags(): void {
+ $parser = new Parser( self::$valid_readme );
+ $this->assertSame( [ 'test', 'unit-test' ], $parser->tags );
+ }
+
+ function test_parses_requires(): void {
+ $parser = new Parser( self::$valid_readme );
+ $this->assertSame( '5.0', $parser->requires );
+ }
+
+ function test_parses_tested(): void {
+ $parser = new Parser( self::$valid_readme );
+ $this->assertSame( '6.4', $parser->tested );
+ }
+
+ function test_parses_stable_tag(): void {
+ $parser = new Parser( self::$valid_readme );
+ $this->assertSame( '1.2.3', $parser->stable_tag );
+ }
+
+ function test_parses_requires_php(): void {
+ $parser = new Parser( self::$valid_readme );
+ $this->assertSame( '7.4', $parser->requires_php );
+ }
+
+ function test_parses_short_description(): void {
+ $parser = new Parser( self::$valid_readme );
+ $this->assertSame( 'A short description of the plugin.', $parser->short_description );
+ }
+
+ function test_parses_license(): void {
+ $parser = new Parser( self::$valid_readme );
+ $this->assertSame( 'GPLv2', $parser->license );
+ }
+
+ function test_parses_sections(): void {
+ $parser = new Parser( self::$valid_readme );
+
+ $this->assertArrayHasKey( 'description', $parser->sections );
+ $this->assertArrayHasKey( 'installation', $parser->sections );
+ $this->assertArrayHasKey( 'faq', $parser->sections );
+ $this->assertArrayHasKey( 'changelog', $parser->sections );
+ }
+
+ function test_description_section_content(): void {
+ $parser = new Parser( self::$valid_readme );
+ $this->assertStringContainsString( 'longer description', $parser->sections['description'] );
+ }
+
+ function test_faq_parsed(): void {
+ $parser = new Parser( self::$valid_readme );
+ $this->assertNotEmpty( $parser->faq );
+ $this->assertArrayHasKey( 'How does it work?', $parser->faq );
+ }
+
+ function test_empty_readme_produces_warnings(): void {
+ $parser = new Parser( '' );
+ $this->assertEmpty( $parser->name );
+ }
+
+ function test_readme_without_name_header(): void {
+ $readme = <<<'README'
+Contributors: johndoe
+Tags: test
+Requires at least: 5.0
+Tested up to: 6.0
+Stable tag: 1.0
+
+Short description.
+
+== Description ==
+
+Long description.
+README;
+ $parser = new Parser( $readme );
+ $this->assertArrayHasKey( 'invalid_plugin_name_header', $parser->warnings );
+ }
+
+ function test_readme_alias_sections(): void {
+ $readme = <<<'README'
+=== Test Plugin ===
+Contributors: johndoe
+Stable tag: 1.0
+
+Short description.
+
+== Frequently Asked Questions ==
+
+= Question? =
+
+Answer.
+
+== Change Log ==
+
+= 1.0 =
+* Initial release.
+README;
+ $parser = new Parser( $readme );
+
+ // 'frequently_asked_questions' is aliased to 'faq'.
+ $this->assertArrayHasKey( 'faq', $parser->sections );
+
+ // 'change_log' is aliased to 'changelog'.
+ $this->assertArrayHasKey( 'changelog', $parser->sections );
+ }
+
+ function test_ignored_tags_filtered(): void {
+ $readme = <<<'README'
+=== Test Plugin ===
+Contributors: johndoe
+Tags: plugin, wordpress, seo, test
+Stable tag: 1.0
+
+Short description.
+
+== Description ==
+
+A plugin.
+README;
+ $parser = new Parser( $readme );
+
+ // 'plugin' and 'wordpress' should be filtered out.
+ $this->assertNotContains( 'plugin', $parser->tags );
+ $this->assertNotContains( 'wordpress', $parser->tags );
+ $this->assertContains( 'seo', $parser->tags );
+ $this->assertContains( 'test', $parser->tags );
+ }
+
+ function test_valid_headers_mapping(): void {
+ // 'tested up to' and 'tested' both map to 'tested'.
+ $readme_tested_up_to = <<<'README'
+=== Test ===
+Contributors: johndoe
+Tested up to: 6.5
+Stable tag: 1.0
+
+Short desc.
+README;
+ $parser = new Parser( $readme_tested_up_to );
+ $this->assertSame( '6.5', $parser->tested );
+
+ // 'requires at least' maps to 'requires'.
+ $readme_requires = <<<'README'
+=== Test ===
+Contributors: johndoe
+Requires at least: 5.5
+Stable tag: 1.0
+
+Short desc.
+README;
+ $parser = new Parser( $readme_requires );
+ $this->assertSame( '5.5', $parser->requires );
+ }
+
+ function test_long_short_description_produces_warning(): void {
+ $long_desc = str_repeat( 'a ', 100 ); // 200 chars.
+ $readme = "=== Test ===\nContributors: johndoe\nStable tag: 1.0\n\n{$long_desc}\n\n== Description ==\n\nDesc.";
+
+ $parser = new Parser( $readme );
+ $this->assertArrayHasKey( 'trimmed_short_description', $parser->warnings );
+ }
+
+ function test_donate_link_parsed(): void {
+ $readme = <<<'README'
+=== Test ===
+Contributors: johndoe
+Donate link: https://example.com/donate
+Stable tag: 1.0
+
+Short desc.
+
+== Description ==
+
+Desc.
+README;
+ $parser = new Parser( $readme );
+ $this->assertSame( 'https://example.com/donate', $parser->donate_link );
+ }
+}
diff --git a/wordpress.org/public_html/wp-content/plugins/plugin-directory/tests/phpunit/tests/test-readme-validator.php b/wordpress.org/public_html/wp-content/plugins/plugin-directory/tests/phpunit/tests/test-readme-validator.php
new file mode 100644
index 0000000000..09149494a4
--- /dev/null
+++ b/wordpress.org/public_html/wp-content/plugins/plugin-directory/tests/phpunit/tests/test-readme-validator.php
@@ -0,0 +1,366 @@
+user->create( [ 'user_login' => 'validatoruser', 'user_nicename' => 'validatoruser' ] );
+ }
+
+ function test_valid_readme_has_no_errors(): void {
+ $validator = Validator::instance();
+ $result = $validator->validate( self::$valid_readme );
+
+ $this->assertEmpty( $result['errors'], 'Expected no errors for valid readme. Got: ' . print_r( $result['errors'], true ) );
+ }
+
+ function test_valid_readme_has_no_warnings(): void {
+ $validator = Validator::instance();
+ $result = $validator->validate( self::$valid_readme );
+
+ $this->assertEmpty( $result['warnings'], 'Expected no warnings for valid readme. Got: ' . print_r( $result['warnings'], true ) );
+ }
+
+ function test_missing_plugin_name_is_error(): void {
+ $readme = <<<'README'
+Contributors: johndoe
+Stable tag: 1.0
+
+Short description.
+
+== Description ==
+
+Long description.
+README;
+ $validator = Validator::instance();
+ $result = $validator->validate( $readme );
+
+ $this->assertArrayHasKey( 'invalid_plugin_name_header', $result['errors'] );
+ }
+
+ function test_missing_tested_is_warning(): void {
+ $readme = <<<'README'
+=== Test Tool ===
+Contributors: validatoruser
+Requires at least: 5.0
+Stable tag: 1.0
+License: GPLv2
+
+Short description.
+
+== Description ==
+
+Long description.
+README;
+ $validator = Validator::instance();
+ $result = $validator->validate( $readme );
+
+ $this->assertArrayHasKey( 'tested_header_missing', $result['warnings'] );
+ }
+
+ function test_missing_stable_tag_is_warning(): void {
+ $readme = <<<'README'
+=== Test Tool ===
+Contributors: validatoruser
+Tested up to: 6.4
+License: GPLv2
+
+Short description.
+
+== Description ==
+
+Long description.
+README;
+ $validator = Validator::instance();
+ $result = $validator->validate( $readme );
+
+ $this->assertArrayHasKey( 'stable_tag_invalid', $result['warnings'] );
+ }
+
+ function test_trunk_stable_tag_is_warning(): void {
+ $readme = <<<'README'
+=== Test Tool ===
+Contributors: validatoruser
+Tested up to: 6.4
+Stable tag: trunk
+License: GPLv2
+
+Short description.
+
+== Description ==
+
+Long description.
+README;
+ $validator = Validator::instance();
+ $result = $validator->validate( $readme );
+
+ $this->assertArrayHasKey( 'stable_tag_invalid', $result['warnings'] );
+ }
+
+ function test_missing_requires_is_note(): void {
+ $readme = <<<'README'
+=== Test Tool ===
+Contributors: validatoruser
+Tested up to: 6.4
+Stable tag: 1.0
+License: GPLv2
+
+Short description.
+
+== Description ==
+
+Long description.
+README;
+ $validator = Validator::instance();
+ $result = $validator->validate( $readme );
+
+ $this->assertArrayHasKey( 'requires_header_missing', $result['notes'] );
+ }
+
+ function test_missing_requires_php_is_note(): void {
+ $readme = <<<'README'
+=== Test Tool ===
+Contributors: validatoruser
+Tested up to: 6.4
+Stable tag: 1.0
+Requires at least: 5.0
+License: GPLv2
+
+Short description.
+
+== Description ==
+
+Long description.
+README;
+ $validator = Validator::instance();
+ $result = $validator->validate( $readme );
+
+ $this->assertArrayHasKey( 'requires_php_header_missing', $result['notes'] );
+ }
+
+ function test_missing_faq_is_note(): void {
+ $readme = <<<'README'
+=== Test Tool ===
+Contributors: validatoruser
+Tested up to: 6.4
+Stable tag: 1.0
+License: GPLv2
+
+Short description.
+
+== Description ==
+
+Long description.
+
+== Changelog ==
+
+= 1.0 =
+* Initial release.
+README;
+ $validator = Validator::instance();
+ $result = $validator->validate( $readme );
+
+ $this->assertArrayHasKey( 'faq_missing', $result['notes'] );
+ }
+
+ function test_missing_changelog_is_note(): void {
+ $readme = <<<'README'
+=== Test Tool ===
+Contributors: validatoruser
+Tested up to: 6.4
+Stable tag: 1.0
+License: GPLv2
+
+Short description.
+
+== Description ==
+
+Long description.
+README;
+ $validator = Validator::instance();
+ $result = $validator->validate( $readme );
+
+ $this->assertArrayHasKey( 'changelog_missing', $result['notes'] );
+ }
+
+ function test_missing_screenshots_is_note(): void {
+ $readme = <<<'README'
+=== Test Tool ===
+Contributors: validatoruser
+Tested up to: 6.4
+Stable tag: 1.0
+License: GPLv2
+
+Short description.
+
+== Description ==
+
+Long description.
+README;
+ $validator = Validator::instance();
+ $result = $validator->validate( $readme );
+
+ $this->assertArrayHasKey( 'screenshots_missing', $result['notes'] );
+ }
+
+ function test_missing_donate_link_is_note(): void {
+ $readme = <<<'README'
+=== Test Tool ===
+Contributors: validatoruser
+Tested up to: 6.4
+Stable tag: 1.0
+License: GPLv2
+
+Short description.
+
+== Description ==
+
+Long description.
+README;
+ $validator = Validator::instance();
+ $result = $validator->validate( $readme );
+
+ $this->assertArrayHasKey( 'donate_link_missing', $result['notes'] );
+ }
+
+ function test_missing_license_is_warning(): void {
+ $readme = <<<'README'
+=== Test Tool ===
+Contributors: validatoruser
+Tested up to: 6.4
+Stable tag: 1.0
+
+Short description.
+
+== Description ==
+
+Long description.
+README;
+ $validator = Validator::instance();
+ $result = $validator->validate( $readme );
+
+ $this->assertArrayHasKey( 'license_missing', $result['warnings'] );
+ }
+
+ /**
+ * Test translate_code_to_message returns strings for known codes.
+ *
+ * @dataProvider data_translate_code_to_message
+ */
+ function test_translate_code_to_message( $code ): void {
+ $validator = Validator::instance();
+ $result = $validator->translate_code_to_message( $code );
+
+ $this->assertIsString( $result );
+ $this->assertNotEmpty( $result );
+ }
+
+ function data_translate_code_to_message(): array {
+ return [
+ 'invalid name' => [ 'invalid_plugin_name_header' ],
+ 'tested missing' => [ 'tested_header_missing' ],
+ 'stable tag invalid' => [ 'stable_tag_invalid' ],
+ 'contributors missing' => [ 'contributors_missing' ],
+ 'faq missing' => [ 'faq_missing' ],
+ 'changelog missing' => [ 'changelog_missing' ],
+ 'screenshots missing' => [ 'screenshots_missing' ],
+ 'donate link missing' => [ 'donate_link_missing' ],
+ 'license missing' => [ 'license_missing' ],
+ 'requires missing' => [ 'requires_header_missing' ],
+ 'requires php missing' => [ 'requires_php_header_missing' ],
+ 'upgrade notice' => [ 'upgrade_notice_missing' ],
+ ];
+ }
+
+ function test_translate_code_to_message_unknown_code_returns_false(): void {
+ $validator = Validator::instance();
+ $result = $validator->translate_code_to_message( 'nonexistent_code' );
+
+ $this->assertFalse( $result );
+ }
+
+ function test_translate_code_contributor_ignored_with_data(): void {
+ $validator = Validator::instance();
+ $result = $validator->translate_code_to_message( 'contributor_ignored', [ 'fakeuser1', 'fakeuser2' ] );
+
+ $this->assertIsString( $result );
+ $this->assertStringContainsString( 'fakeuser1', $result );
+ $this->assertStringContainsString( 'fakeuser2', $result );
+ }
+
+ function test_translate_code_contributor_ignored_without_data(): void {
+ $validator = Validator::instance();
+ $result = $validator->translate_code_to_message( 'contributor_ignored' );
+
+ $this->assertIsString( $result );
+ $this->assertStringContainsString( 'Contributors', $result );
+ }
+
+ function test_validate_content_returns_translated_messages(): void {
+ $validator = Validator::instance();
+ $result = $validator->validate_content( '' );
+
+ // validate_content translates error codes to human-readable strings.
+ foreach ( $result['errors'] as $message ) {
+ $this->assertIsString( $message );
+ }
+ foreach ( $result['warnings'] as $message ) {
+ $this->assertIsString( $message );
+ }
+ foreach ( $result['notes'] as $message ) {
+ $this->assertIsString( $message );
+ }
+ }
+
+ function test_last_content_stored(): void {
+ $validator = Validator::instance();
+ $readme = "=== Test ===\nStable tag: 1.0\n\nDesc.";
+ $validator->validate( $readme );
+
+ $this->assertSame( $readme, $validator->last_content );
+ }
+}
diff --git a/wordpress.org/public_html/wp-content/plugins/plugin-directory/tests/phpunit/tests/test-template.php b/wordpress.org/public_html/wp-content/plugins/plugin-directory/tests/phpunit/tests/test-template.php
new file mode 100644
index 0000000000..8656dfeb80
--- /dev/null
+++ b/wordpress.org/public_html/wp-content/plugins/plugin-directory/tests/phpunit/tests/test-template.php
@@ -0,0 +1,258 @@
+post->create_and_get( array_merge( [
+ 'post_type' => 'plugin',
+ 'post_status' => 'publish',
+ 'post_modified' => $now,
+ 'post_modified_gmt' => $gmt,
+ ], $args ) );
+ }
+
+ /**
+ * @dataProvider data_sanitize_active_installs
+ */
+ function test_sanitize_active_installs( $input, $expected ): void {
+ // floor() returns float, so use assertEquals for loose type comparison.
+ $this->assertEquals( $expected, Template::sanitize_active_installs( $input ) );
+ }
+
+ function data_sanitize_active_installs(): array {
+ return [
+ 'zero' => [ 0, 0 ],
+ 'single digit' => [ 5, 0 ],
+ 'ten' => [ 10, 10 ],
+ 'twelve' => [ 12, 10 ],
+ 'ninety nine' => [ 99, 90 ],
+ 'one hundred' => [ 100, 100 ],
+ 'hundred fifty' => [ 150, 100 ],
+ 'nine ninety nine' => [ 999, 900 ],
+ 'one thousand' => [ 1000, 1000 ],
+ 'one thousand five' => [ 1500, 1000 ],
+ 'ten thousand' => [ 10000, 10000 ],
+ 'fifteen thousand' => [ 15000, 10000 ],
+ 'hundred thousand' => [ 100000, 100000 ],
+ 'five hundred k' => [ 500000, 500000 ],
+ 'one million' => [ 1000000, 1000000 ],
+ 'five million' => [ 5000000, 5000000 ],
+ 'ten million' => [ 10000000, 10000000 ],
+ 'over ten million' => [ 15000000, 10000000 ],
+ 'fifty million' => [ 50000000, 10000000 ],
+ ];
+ }
+
+ /**
+ * @dataProvider data_format_active_installs_for_display
+ */
+ function test_format_active_installs_for_display( $input, $expected ): void {
+ $this->assertSame( $expected, Template::format_active_installs_for_display( $input ) );
+ }
+
+ function data_format_active_installs_for_display(): array {
+ return [
+ 'zero' => [ 0, 'Fewer than 10' ],
+ 'five' => [ 5, 'Fewer than 10' ],
+ 'nine' => [ 9, 'Fewer than 10' ],
+ 'ten' => [ 10, '10+' ],
+ 'hundred' => [ 100, '100+' ],
+ 'thousand' => [ 1000, '1,000+' ],
+ 'ten thousand' => [ 10000, '10,000+' ],
+ 'one million' => [ 1000000, '1+ million' ],
+ 'two million' => [ 2000000, '2+ million' ],
+ 'ten million' => [ 10000000, '10+ million' ],
+ ];
+ }
+
+ function test_get_plugin_section_titles(): void {
+ $titles = Template::get_plugin_section_titles();
+
+ $this->assertIsArray( $titles );
+
+ $expected_keys = [
+ 'description',
+ 'installation',
+ 'faq',
+ 'screenshots',
+ 'changelog',
+ 'stats',
+ 'support',
+ 'reviews',
+ 'developers',
+ 'other_notes',
+ 'blocks',
+ ];
+
+ $this->assertSame( $expected_keys, array_keys( $titles ) );
+ }
+
+ function test_get_current_major_wp_version_returns_float(): void {
+ $version = Template::get_current_major_wp_version();
+
+ $this->assertIsFloat( $version );
+ $this->assertGreaterThan( 0, $version );
+ }
+
+ /**
+ * @dataProvider data_encode
+ */
+ function test_encode( $input, $expected ): void {
+ $this->assertSame( $expected, Template::encode( $input ) );
+ }
+
+ function data_encode(): array {
+ return [
+ 'plain ascii' => [ 'Hello World', 'Hello World' ],
+ 'empty string' => [ '', '' ],
+ 'numeric entity' => [ 'café', 'café' ],
+ 'utf8 e-acute' => [ "caf\xC3\xA9", 'café' ],
+ 'utf8 em-dash' => [ "\xE2\x80\x94", '—' ],
+ 'utf8 copyright' => [ "\xC2\xA9", '©' ],
+ // encode() round-trips & < > via htmlentities + htmlspecialchars_decode (ENT_NOQUOTES).
+ 'ampersand' => [ 'A & B', 'A & B' ],
+ 'angle brackets' => [ 'a < b > c', 'a < b > c' ],
+ ];
+ }
+
+ /**
+ * Test dashicons_stars output.
+ *
+ * @dataProvider data_dashicons_stars
+ */
+ function test_dashicons_stars( $rating, $filled, $half, $empty_stars ): void {
+ $output = Template::dashicons_stars( $rating );
+
+ $this->assertSame( $filled, substr_count( $output, 'dashicons-star-filled' ) );
+ $this->assertSame( $half, substr_count( $output, 'dashicons-star-half' ) );
+ $this->assertSame( $empty_stars, substr_count( $output, 'dashicons-star-empty' ) );
+ }
+
+ function data_dashicons_stars(): array {
+ // [ rating, filled, half, empty ]
+ return [
+ 'zero stars' => [ 0, 0, 0, 5 ],
+ 'one star' => [ 1, 1, 0, 4 ],
+ 'two and a half' => [ 2.5, 2, 1, 2 ],
+ 'four stars' => [ 4, 4, 0, 1 ],
+ 'five stars' => [ 5, 5, 0, 0 ],
+ 'three point 3' => [ 3.3, 3, 1, 1 ],
+ ];
+ }
+
+ function test_get_close_reasons_returns_expected_keys(): void {
+ $reasons = Template::get_close_reasons();
+
+ $this->assertIsArray( $reasons );
+ $this->assertArrayHasKey( 'security-issue', $reasons );
+ $this->assertArrayHasKey( 'author-request', $reasons );
+ $this->assertArrayHasKey( 'guideline-violation', $reasons );
+ $this->assertArrayHasKey( 'licensing-trademark-violation', $reasons );
+ $this->assertArrayHasKey( 'merged-into-core', $reasons );
+ $this->assertArrayHasKey( 'unused', $reasons );
+ }
+
+ function test_get_rejection_reasons_returns_expected_keys(): void {
+ $reasons = Template::get_rejection_reasons();
+
+ $this->assertIsArray( $reasons );
+ $this->assertArrayHasKey( '3-month', $reasons );
+ $this->assertArrayHasKey( 'security', $reasons );
+ $this->assertArrayHasKey( 'duplicate', $reasons );
+ $this->assertArrayHasKey( 'banned', $reasons );
+ }
+
+ function test_download_link_with_version(): void {
+ $plugin = $this->create_plugin_post( [ 'post_name' => 'test-download' ] );
+ update_post_meta( $plugin->ID, 'stable_tag', '2.1.0' );
+
+ $link = Template::download_link( $plugin, '1.0.0' );
+ $this->assertSame( 'https://downloads.wordpress.org/plugin/test-download.1.0.0.zip', $link );
+
+ $link = Template::download_link( $plugin, 'latest' );
+ $this->assertSame( 'https://downloads.wordpress.org/plugin/test-download.2.1.0.zip', $link );
+
+ $link = Template::download_link( $plugin, 'trunk' );
+ $this->assertSame( 'https://downloads.wordpress.org/plugin/test-download.zip', $link );
+ }
+
+ function test_get_support_url_standard(): void {
+ $plugin = $this->create_plugin_post( [ 'post_name' => 'my-cool-tool' ] );
+
+ $url = Template::get_support_url( $plugin );
+ $this->assertSame( 'https://wordpress.org/support/plugin/my-cool-tool/', $url );
+ }
+
+ function test_get_support_url_buddypress(): void {
+ $plugin = $this->create_plugin_post( [ 'post_name' => 'buddypress' ] );
+
+ $url = Template::get_support_url( $plugin );
+ $this->assertSame( 'https://buddypress.org/support/', $url );
+ }
+
+ function test_get_support_url_bbpress(): void {
+ $plugin = $this->create_plugin_post( [ 'post_name' => 'bbpress' ] );
+
+ $url = Template::get_support_url( $plugin );
+ $this->assertSame( 'https://bbpress.org/forums/', $url );
+ }
+
+ function test_geopattern_icon_url_format(): void {
+ $plugin = $this->create_plugin_post( [ 'post_name' => 'geo-test' ] );
+
+ $url = Template::get_geopattern_icon_url( $plugin );
+ $this->assertStringContainsString( 'geo-test', $url );
+ $this->assertStringContainsString( 'geopattern-icon', $url );
+ $this->assertStringEndsWith( '.svg', $url );
+ }
+
+ function test_geopattern_icon_url_with_color(): void {
+ $plugin = $this->create_plugin_post( [ 'post_name' => 'geo-color' ] );
+
+ $url = Template::get_geopattern_icon_url( $plugin, 'ff5500' );
+ $this->assertStringContainsString( 'geo-color_ff5500', $url );
+ }
+
+ function test_geopattern_icon_url_with_invalid_color(): void {
+ $plugin = $this->create_plugin_post( [ 'post_name' => 'geo-invalid' ] );
+
+ $url = Template::get_geopattern_icon_url( $plugin, 'notacolor' );
+ $this->assertStringNotContainsString( '_notacolor', $url );
+ }
+
+ function test_is_plugin_outdated(): void {
+ $plugin = $this->create_plugin_post( [ 'post_name' => 'old-tool' ] );
+
+ // Set tested to a very old version.
+ update_post_meta( $plugin->ID, 'tested', '4.0' );
+ $this->assertTrue( Template::is_plugin_outdated( $plugin ) );
+
+ // Set tested to current version.
+ $current = Template::get_current_major_wp_version();
+ update_post_meta( $plugin->ID, 'tested', (string) $current );
+ $this->assertFalse( Template::is_plugin_outdated( $plugin ) );
+ }
+
+ function test_get_rollout_strategies(): void {
+ $strategies = Template::get_rollout_strategies();
+
+ $this->assertIsArray( $strategies );
+ $this->assertArrayHasKey( '', $strategies );
+ $this->assertArrayHasKey( 'manual-updates-24hr', $strategies );
+ $this->assertArrayHasKey( 'name', $strategies[''] );
+ $this->assertArrayHasKey( 'description', $strategies[''] );
+ }
+}
diff --git a/wordpress.org/public_html/wp-content/plugins/plugin-directory/tests/phpunit/tests/test-trademarks.php b/wordpress.org/public_html/wp-content/plugins/plugin-directory/tests/phpunit/tests/test-trademarks.php
new file mode 100644
index 0000000000..f7904bf61f
--- /dev/null
+++ b/wordpress.org/public_html/wp-content/plugins/plugin-directory/tests/phpunit/tests/test-trademarks.php
@@ -0,0 +1,120 @@
+assertFalse( Trademarks::check_slug( $slug ) );
+ }
+
+ function data_clean_slugs(): array {
+ // Note: 'plugin' is a trademarked slug, so avoid it in clean slugs.
+ return [
+ 'generic slug' => [ 'my-cool-tool' ],
+ 'simple slug' => [ 'hello-world' ],
+ 'numeric slug' => [ 'acme-2024' ],
+ 'short slug' => [ 'foo' ],
+ ];
+ }
+
+ /**
+ * Test that trademarked prefix slugs are detected.
+ *
+ * @dataProvider data_trademarked_prefix_slugs
+ */
+ function test_trademarked_prefix_slugs( $slug, $expected_trademark ): void {
+ $result = Trademarks::check_slug( $slug );
+ $this->assertIsArray( $result );
+ $this->assertContains( $expected_trademark, $result );
+ }
+
+ function data_trademarked_prefix_slugs(): array {
+ return [
+ 'google prefix' => [ 'google-analytics-tool', 'google-' ],
+ 'facebook in slug' => [ 'my-facebook-share', 'facebook' ],
+ 'jetpack prefix' => [ 'jetpack-addon', 'jetpack-' ],
+ 'stripe prefix' => [ 'stripe-payments', 'stripe-' ],
+ 'paypal prefix' => [ 'paypal-checkout', 'paypal-' ],
+ 'woocommerce slug' => [ 'woocommerce-extras', 'woocommerce' ],
+ 'instagram slug' => [ 'instagram-feed-widget', 'instagram' ],
+ 'twitter slug' => [ 'twitter-cards', 'twitter-' ],
+ 'chatgpt prefix' => [ 'chatgpt-assistant', 'chatgpt-' ],
+ ];
+ }
+
+ /**
+ * Test the for-woocommerce exception.
+ */
+ function test_for_woocommerce_exception(): void {
+ // "something-for-woocommerce" is allowed.
+ $this->assertFalse( Trademarks::check_slug( 'my-payments-for-woocommerce' ) );
+
+ // But "woocommerce-something" is not.
+ $result = Trademarks::check_slug( 'woocommerce-payments' );
+ $this->assertIsArray( $result );
+ }
+
+ /**
+ * Test that check() converts a plugin name to slug first.
+ */
+ function test_check_converts_name_to_slug(): void {
+ // "My Cool Tool" should be clean (note: 'plugin' is trademarked).
+ $this->assertFalse( Trademarks::check( 'My Cool Tool' ) );
+
+ // "Google Maps Helper" should be trademarked (contains 'google-').
+ $result = Trademarks::check( 'Google Maps Helper' );
+ $this->assertIsArray( $result );
+ }
+
+ /**
+ * Test that trademark exceptions work.
+ */
+ function test_trademark_exceptions(): void {
+ // Without exception, jetpack- is trademarked.
+ $result = Trademarks::check_slug( 'jetpack-boost-extra' );
+ $this->assertIsArray( $result );
+
+ // With automattic.com exception, jetpack- is allowed.
+ $result = Trademarks::check_slug( 'jetpack-boost-extra', [ 'automattic.com' ] );
+ $this->assertFalse( $result );
+ }
+
+ /**
+ * Test published plugin exception for wp- prefix.
+ */
+ function test_published_plugin_wp_prefix_exception(): void {
+ // wp- is flagged for new plugins.
+ $result = Trademarks::check_slug( 'wp-super-cache' );
+ $this->assertIsArray( $result );
+
+ // published-plugin exception allows wp-.
+ $result = Trademarks::check_slug( 'wp-super-cache', [ 'published-plugin' ] );
+ $this->assertFalse( $result );
+ }
+
+ /**
+ * Test portmanteau detection (e.g. woopress).
+ */
+ function test_portmanteau_detection(): void {
+ $result = Trademarks::check_slug( 'woopress' );
+ $this->assertIsArray( $result );
+ }
+
+ /**
+ * Test that the trademarked slugs list is not empty.
+ */
+ function test_trademarked_slugs_list_is_populated(): void {
+ $this->assertNotEmpty( Trademarks::$trademarked_slugs );
+ $this->assertGreaterThan( 50, count( Trademarks::$trademarked_slugs ) );
+ }
+}
diff --git a/wordpress.org/public_html/wp-content/plugins/plugin-directory/tests/phpunit/tests/wporg-plugin-api-performance.php b/wordpress.org/public_html/wp-content/plugins/plugin-directory/tests/phpunit/tests/wporg-plugin-api-performance.php
index 1dd1c8b7b3..5ebaf06640 100644
--- a/wordpress.org/public_html/wp-content/plugins/plugin-directory/tests/phpunit/tests/wporg-plugin-api-performance.php
+++ b/wordpress.org/public_html/wp-content/plugins/plugin-directory/tests/phpunit/tests/wporg-plugin-api-performance.php
@@ -44,12 +44,12 @@ class Tests_Plugins_API_Performance extends WP_UnitTestCase {
'contributors' => true,
);
- function setUp() {
+ function setUp(): void {
parent::setUp();
add_filter( 'http_headers_useragent', array( $this, 'filter_http_headers_useragent' ) );
}
- function tearDown() {
+ function tearDown(): void {
remove_filter( 'http_headers_useragent', array( $this, 'filter_http_headers_useragent' ) );
parent::tearDown();
}
@@ -64,7 +64,7 @@ static function averages( $values, $decimals = 4 ) {
}
- static function tearDownAfterClass() {
+ static function tearDownAfterClass(): void {
global $wporg_plugin_api_performance;
echo 'Performance summary for ' . get_called_class() . ":\n";
diff --git a/wordpress.org/public_html/wp-content/plugins/plugin-directory/tests/phpunit/tests/wporg-plugin-api.php b/wordpress.org/public_html/wp-content/plugins/plugin-directory/tests/phpunit/tests/wporg-plugin-api.php
index 7f04e61be3..4d837d6336 100644
--- a/wordpress.org/public_html/wp-content/plugins/plugin-directory/tests/phpunit/tests/wporg-plugin-api.php
+++ b/wordpress.org/public_html/wp-content/plugins/plugin-directory/tests/phpunit/tests/wporg-plugin-api.php
@@ -44,12 +44,12 @@ class Tests_Plugins_API extends WP_UnitTestCase {
'donate_link' => true,
);
- function setUp() {
+ function setUp(): void {
parent::setUp();
add_filter( 'http_headers_useragent', array( $this, 'filter_http_headers_useragent' ) );
}
- function tearDown() {
+ function tearDown(): void {
remove_filter( 'http_headers_useragent', array( $this, 'filter_http_headers_useragent' ) );
parent::tearDown();
}
diff --git a/wordpress.org/public_html/wp-content/plugins/plugin-directory/tests/stubs/jetpack-search-stub.php b/wordpress.org/public_html/wp-content/plugins/plugin-directory/tests/stubs/jetpack-search-stub.php
new file mode 100644
index 0000000000..79f97f7971
--- /dev/null
+++ b/wordpress.org/public_html/wp-content/plugins/plugin-directory/tests/stubs/jetpack-search-stub.php
@@ -0,0 +1,42 @@
+found_posts`.
+ *
+ * @return array|null
+ */
+ public function get_last_query_info() {
+ return null;
+ }
+ }
+}
diff --git a/wordpress.org/public_html/wp-content/plugins/plugin-directory/tests/stubs/wporg-ratings-stub.php b/wordpress.org/public_html/wp-content/plugins/plugin-directory/tests/stubs/wporg-ratings-stub.php
new file mode 100644
index 0000000000..e8fb8eb147
--- /dev/null
+++ b/wordpress.org/public_html/wp-content/plugins/plugin-directory/tests/stubs/wporg-ratings-stub.php
@@ -0,0 +1,34 @@
+