From 4332714a67caacb9fdb8b29eb93bb7951da97132 Mon Sep 17 00:00:00 2001 From: Huzaifa Al Mesbah Date: Wed, 21 Oct 2026 10:10:28 +0600 Subject: [PATCH 1/5] Tests: Add unit test coverage for validate_active_plugins(). --- .../includes/plugin/validateActivePlugins.php | 190 ++++++++++++++++++ 1 file changed, 190 insertions(+) create mode 100644 tests/phpunit/tests/admin/includes/plugin/validateActivePlugins.php diff --git a/tests/phpunit/tests/admin/includes/plugin/validateActivePlugins.php b/tests/phpunit/tests/admin/includes/plugin/validateActivePlugins.php new file mode 100644 index 0000000000000..f4431df4bc446 --- /dev/null +++ b/tests/phpunit/tests/admin/includes/plugin/validateActivePlugins.php @@ -0,0 +1,190 @@ +user->create( array( 'role' => 'administrator' ) ); + + if ( is_multisite() ) { + grant_super_admin( self::$super_admin_id ); + } + } + + /** + * Revokes the super admin grant before the fixture users are deleted. + */ + public static function wpTearDownAfterClass() { + if ( is_multisite() ) { + revoke_super_admin( self::$super_admin_id ); + } + } + + /** + * Creates a plugin file without a plugin header and resets the active plugins. + */ + public function set_up() { + parent::set_up(); + + $dir = WP_PLUGIN_DIR . '/' . self::NO_HEADER_DIR; + $this->assertDirectoryDoesNotExist( $dir, 'The test directory should not exist yet.' ); + $this->assertTrue( mkdir( $dir ), 'The test directory should be created.' ); + $this->assertNotFalse( file_put_contents( $dir . '/functions.php', "assertSame( array(), validate_active_plugins(), 'No invalid plugins should be returned.' ); + $this->assertSame( array(), get_option( 'active_plugins' ), 'The option should be reset.' ); + } + + /** + * Data provider. + * + * @return array[] + */ + public static function data_invalid_options() { + return array( + 'string' => array( 'hello.php' ), + 'integer' => array( 1 ), + ); + } + + /** + * Tests that valid active plugins are kept. + * + * @ticket 65819 + */ + public function test_should_keep_valid_plugins() { + update_option( 'active_plugins', array( 'hello.php', 'internationalized-plugin.php' ) ); + + $this->assertSame( array(), validate_active_plugins(), 'No invalid plugins should be returned.' ); + $this->assertSame( array( 'hello.php', 'internationalized-plugin.php' ), get_option( 'active_plugins' ), 'The plugins should stay active.' ); + } + + /** + * Tests that invalid active plugins are reported and deactivated silently. + * + * @ticket 65819 + */ + public function test_should_report_and_deactivate_invalid_plugins() { + $no_header = self::NO_HEADER_DIR . '/functions.php'; + update_option( 'active_plugins', array( 'hello.php', '../outside.php', 'missing/missing.php', $no_header ) ); + + $deactivate = new MockAction(); + add_action( 'deactivate_plugin', array( $deactivate, 'action' ) ); + + $invalid = validate_active_plugins(); + + $this->assertSame( array( '../outside.php', 'missing/missing.php', $no_header ), array_keys( $invalid ), 'Each invalid plugin should be reported.' ); + $this->assertSame( + array( + '../outside.php' => array( 'plugin_invalid', 'Invalid plugin path.' ), + 'missing/missing.php' => array( 'plugin_not_found', 'Plugin file does not exist.' ), + $no_header => array( 'no_plugin_header', 'The plugin does not have a valid header.' ), + ), + array_map( + static function ( $error ) { + return array( $error->get_error_code(), $error->get_error_message() ); + }, + $invalid + ), + 'Each invalid plugin should have its error.' + ); + $this->assertSame( array( 'hello.php' ), array_values( get_option( 'active_plugins' ) ), 'Only the valid plugin should stay active.' ); + $this->assertSame( 0, $deactivate->get_call_count(), 'Invalid plugins should be deactivated silently.' ); + } + + /** + * Tests that network-active plugins are validated for a user who can manage network plugins. + * + * @ticket 65819 + * + * @group ms-required + */ + public function test_should_validate_network_plugins_for_network_admin() { + wp_set_current_user( self::$super_admin_id ); + + update_option( 'active_plugins', array() ); + update_site_option( + 'active_sitewide_plugins', + array( + 'hello.php' => 1000, + 'missing/missing.php' => 1000, + ) + ); + + $this->assertSame( array( 'missing/missing.php' ), array_keys( validate_active_plugins() ), 'The invalid network plugin should be reported.' ); + $this->assertSame( array( 'hello.php' => 1000 ), get_site_option( 'active_sitewide_plugins' ), 'The invalid network plugin should be deactivated.' ); + } + + /** + * Tests that network-active plugins are not validated for other users. + * + * @ticket 65819 + * + * @group ms-required + */ + public function test_should_not_validate_network_plugins_for_other_users() { + update_option( 'active_plugins', array() ); + update_site_option( 'active_sitewide_plugins', array( 'missing/missing.php' => 1000 ) ); + + $this->assertSame( array(), validate_active_plugins(), 'No plugins should be validated.' ); + $this->assertSame( array( 'missing/missing.php' => 1000 ), get_site_option( 'active_sitewide_plugins' ), 'The network plugins should not change.' ); + } +} From 7d99d5d48826102d8d3386e033176852d79e701f Mon Sep 17 00:00:00 2001 From: Huzaifa Al Mesbah Date: Wed, 21 Oct 2026 12:12:45 +0600 Subject: [PATCH 2/5] Tests: Remove a leftover test plugin before creating it in the validate_active_plugins() tests. --- .../includes/plugin/validateActivePlugins.php | 21 +++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/tests/phpunit/tests/admin/includes/plugin/validateActivePlugins.php b/tests/phpunit/tests/admin/includes/plugin/validateActivePlugins.php index f4431df4bc446..a0470bfb56688 100644 --- a/tests/phpunit/tests/admin/includes/plugin/validateActivePlugins.php +++ b/tests/phpunit/tests/admin/includes/plugin/validateActivePlugins.php @@ -53,8 +53,9 @@ public static function wpTearDownAfterClass() { public function set_up() { parent::set_up(); + $this->remove_plugin(); + $dir = WP_PLUGIN_DIR . '/' . self::NO_HEADER_DIR; - $this->assertDirectoryDoesNotExist( $dir, 'The test directory should not exist yet.' ); $this->assertTrue( mkdir( $dir ), 'The test directory should be created.' ); $this->assertNotFalse( file_put_contents( $dir . '/functions.php', "remove_plugin(); wp_cache_delete( 'plugins', 'plugins' ); @@ -187,4 +184,16 @@ public function test_should_not_validate_network_plugins_for_other_users() { $this->assertSame( array(), validate_active_plugins(), 'No plugins should be validated.' ); $this->assertSame( array( 'missing/missing.php' => 1000 ), get_site_option( 'active_sitewide_plugins' ), 'The network plugins should not change.' ); } + + /** + * Removes the plugin file without a header, including one left over by an interrupted run. + */ + private function remove_plugin() { + $dir = WP_PLUGIN_DIR . '/' . self::NO_HEADER_DIR; + + if ( is_dir( $dir ) ) { + $this->rmdir( $dir ); + rmdir( $dir ); + } + } } From f8932ec7115c304ffb48a4dd0638ab333c9419cb Mon Sep 17 00:00:00 2001 From: Lance Willett Date: Tue, 27 Oct 2026 18:56:17 -0700 Subject: [PATCH 3/5] Tests: Scope plugin fixture setup and strengthen network permission check. --- .../includes/plugin/validateActivePlugins.php | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/tests/phpunit/tests/admin/includes/plugin/validateActivePlugins.php b/tests/phpunit/tests/admin/includes/plugin/validateActivePlugins.php index a0470bfb56688..7e19caead09db 100644 --- a/tests/phpunit/tests/admin/includes/plugin/validateActivePlugins.php +++ b/tests/phpunit/tests/admin/includes/plugin/validateActivePlugins.php @@ -48,17 +48,13 @@ public static function wpTearDownAfterClass() { } /** - * Creates a plugin file without a plugin header and resets the active plugins. + * Removes stale plugin files and resets the active plugins. */ public function set_up() { parent::set_up(); $this->remove_plugin(); - $dir = WP_PLUGIN_DIR . '/' . self::NO_HEADER_DIR; - $this->assertTrue( mkdir( $dir ), 'The test directory should be created.' ); - $this->assertNotFalse( file_put_contents( $dir . '/functions.php', "assertTrue( mkdir( $dir ), 'The test directory should be created.' ); + $this->assertNotFalse( file_put_contents( $dir . '/functions.php', "user->create( array( 'role' => 'administrator' ) ) ); + update_option( 'active_plugins', array() ); update_site_option( 'active_sitewide_plugins', array( 'missing/missing.php' => 1000 ) ); From 0c2575c60ed25fc259fc339616cba7d1cfff1ba3 Mon Sep 17 00:00:00 2001 From: Huzaifa Al Mesbah Date: Wed, 28 Oct 2026 18:03:05 +0600 Subject: [PATCH 4/5] Tests: Consolidate validate_active_plugins() coverage and improve test hygiene. - Move legacy validate_active_plugins() tests from includesPlugin.php into validateActivePlugins.php. - Use wp_clean_plugins_cache( false ) for clearing plugins cache. - Use unique directory naming and delete_folders() for safe temporary fixture cleanup. - Add corrupted option data sets for boolean, null, and object. - Assert error codes directly instead of localized error strings. - Verify deactivation of a missing plugin present in both active_plugins and active_sitewide_plugins. - Check deactivated_plugin and deactivate_{$plugin} hooks during silent deactivation. Follow-up to #14608 per committer feedback. --- .../includes/plugin/validateActivePlugins.php | 72 +++++++++++++------ tests/phpunit/tests/admin/includesPlugin.php | 19 ----- 2 files changed, 52 insertions(+), 39 deletions(-) diff --git a/tests/phpunit/tests/admin/includes/plugin/validateActivePlugins.php b/tests/phpunit/tests/admin/includes/plugin/validateActivePlugins.php index 7e19caead09db..ca31740efd70d 100644 --- a/tests/phpunit/tests/admin/includes/plugin/validateActivePlugins.php +++ b/tests/phpunit/tests/admin/includes/plugin/validateActivePlugins.php @@ -12,11 +12,11 @@ class Tests_Admin_Includes_Plugin_ValidateActivePlugins extends WP_UnitTestCase { /** - * The directory of the test plugin without a header. + * Unique directory name for the test plugin without a header. * * @var string */ - const NO_HEADER_DIR = 'validate-test-no-header'; + private $no_header_dir; /** * Super admin user ID. @@ -53,9 +53,10 @@ public static function wpTearDownAfterClass() { public function set_up() { parent::set_up(); + $this->no_header_dir = 'validate-test-no-header-' . wp_generate_password( 6, false ); $this->remove_plugin(); - wp_cache_delete( 'plugins', 'plugins' ); + wp_clean_plugins_cache( false ); update_site_option( 'active_sitewide_plugins', array() ); } @@ -65,7 +66,7 @@ public function set_up() { public function tear_down() { $this->remove_plugin(); - wp_cache_delete( 'plugins', 'plugins' ); + wp_clean_plugins_cache( false ); parent::tear_down(); } @@ -95,9 +96,23 @@ public static function data_invalid_options() { return array( 'string' => array( 'hello.php' ), 'integer' => array( 1 ), + 'boolean' => array( false ), + 'null' => array( null ), + 'object' => array( new stdClass() ), ); } + /** + * Tests that an empty array is returned when no plugins are active. + * + * @ticket 65819 + */ + public function test_should_return_empty_array_when_no_active_plugins() { + update_option( 'active_plugins', array() ); + + $this->assertSame( array(), validate_active_plugins() ); + } + /** * Tests that valid active plugins are kept. * @@ -116,41 +131,48 @@ public function test_should_keep_valid_plugins() { * @ticket 65819 */ public function test_should_report_and_deactivate_invalid_plugins() { - $dir = WP_PLUGIN_DIR . '/' . self::NO_HEADER_DIR; + if ( ! is_writable( WP_PLUGIN_DIR ) ) { + $this->markTestSkipped( 'WP_PLUGIN_DIR is not writable.' ); + } + + $dir = WP_PLUGIN_DIR . '/' . $this->no_header_dir; $this->assertTrue( mkdir( $dir ), 'The test directory should be created.' ); $this->assertNotFalse( file_put_contents( $dir . '/functions.php', "no_header_dir . '/functions.php'; update_option( 'active_plugins', array( 'hello.php', '../outside.php', 'missing/missing.php', $no_header ) ); $deactivate = new MockAction(); add_action( 'deactivate_plugin', array( $deactivate, 'action' ) ); + add_action( 'deactivated_plugin', array( $deactivate, 'action' ) ); + add_action( "deactivate_{$no_header}", array( $deactivate, 'action' ) ); $invalid = validate_active_plugins(); $this->assertSame( array( '../outside.php', 'missing/missing.php', $no_header ), array_keys( $invalid ), 'Each invalid plugin should be reported.' ); $this->assertSame( array( - '../outside.php' => array( 'plugin_invalid', 'Invalid plugin path.' ), - 'missing/missing.php' => array( 'plugin_not_found', 'Plugin file does not exist.' ), - $no_header => array( 'no_plugin_header', 'The plugin does not have a valid header.' ), + '../outside.php' => 'plugin_invalid', + 'missing/missing.php' => 'plugin_not_found', + $no_header => 'no_plugin_header', ), array_map( static function ( $error ) { - return array( $error->get_error_code(), $error->get_error_message() ); + return $error->get_error_code(); }, $invalid ), - 'Each invalid plugin should have its error.' + 'Each invalid plugin should have its expected error code.' ); $this->assertSame( array( 'hello.php' ), array_values( get_option( 'active_plugins' ) ), 'Only the valid plugin should stay active.' ); $this->assertSame( 0, $deactivate->get_call_count(), 'Invalid plugins should be deactivated silently.' ); } /** - * Tests that network-active plugins are validated for a user who can manage network plugins. + * Tests that network-active plugins are validated for a user who can manage network plugins, + * and that a missing plugin present in both active_plugins and active_sitewide_plugins is removed from both. * * @ticket 65819 * @@ -159,7 +181,7 @@ static function ( $error ) { public function test_should_validate_network_plugins_for_network_admin() { wp_set_current_user( self::$super_admin_id ); - update_option( 'active_plugins', array() ); + update_option( 'active_plugins', array( 'missing/missing.php' ) ); update_site_option( 'active_sitewide_plugins', array( @@ -169,7 +191,8 @@ public function test_should_validate_network_plugins_for_network_admin() { ); $this->assertSame( array( 'missing/missing.php' ), array_keys( validate_active_plugins() ), 'The invalid network plugin should be reported.' ); - $this->assertSame( array( 'hello.php' => 1000 ), get_site_option( 'active_sitewide_plugins' ), 'The invalid network plugin should be deactivated.' ); + $this->assertSame( array( 'hello.php' => 1000 ), get_site_option( 'active_sitewide_plugins' ), 'The invalid network plugin should be deactivated from network.' ); + $this->assertNotContains( 'missing/missing.php', get_option( 'active_plugins' ), 'The invalid plugin should also be deactivated from site active plugins.' ); } /** @@ -190,14 +213,23 @@ public function test_should_not_validate_network_plugins_for_other_users() { } /** - * Removes the plugin file without a header, including one left over by an interrupted run. + * Removes the plugin file without a header, including any left over by an interrupted run. */ private function remove_plugin() { - $dir = WP_PLUGIN_DIR . '/' . self::NO_HEADER_DIR; + if ( $this->no_header_dir ) { + $dir = WP_PLUGIN_DIR . '/' . $this->no_header_dir; + if ( is_dir( $dir ) ) { + $this->rmdir( $dir ); + $this->delete_folders( $dir ); + } + } - if ( is_dir( $dir ) ) { - $this->rmdir( $dir ); - rmdir( $dir ); + $dirs = glob( WP_PLUGIN_DIR . '/validate-test-no-header-*', GLOB_ONLYDIR ); + if ( is_array( $dirs ) ) { + foreach ( $dirs as $leftover ) { + $this->rmdir( $leftover ); + $this->delete_folders( $leftover ); + } } } } diff --git a/tests/phpunit/tests/admin/includesPlugin.php b/tests/phpunit/tests/admin/includesPlugin.php index 6eff4098a0b22..064e6ccd69e3e 100644 --- a/tests/phpunit/tests/admin/includesPlugin.php +++ b/tests/phpunit/tests/admin/includesPlugin.php @@ -713,25 +713,6 @@ public function test_activate_plugins_single_array() { deactivate_plugins( $name ); } - /** - * @covers ::validate_active_plugins - */ - public function test_validate_active_plugins_remove_invalid() { - $plugin = $this->_create_plugin(); - - activate_plugin( $plugin[0] ); - unlink( $plugin[1] ); - - $result = validate_active_plugins(); - $this->assertArrayHasKey( $plugin[0], $result ); - } - - /** - * @covers ::validate_active_plugins - */ - public function test_validate_active_plugins_empty() { - $this->assertSame( array(), validate_active_plugins() ); - } /** * @covers ::is_uninstallable_plugin From f348dd6514685eab2bf760866ba012d40c0fac11 Mon Sep 17 00:00:00 2001 From: Huzaifa Al Mesbah Date: Wed, 28 Oct 2026 20:39:04 +0600 Subject: [PATCH 5/5] Tests: Restrict temporary plugin cleanup to the active test directory in validateActivePlugins.php. --- .../admin/includes/plugin/validateActivePlugins.php | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/tests/phpunit/tests/admin/includes/plugin/validateActivePlugins.php b/tests/phpunit/tests/admin/includes/plugin/validateActivePlugins.php index ca31740efd70d..e49dfef026ff8 100644 --- a/tests/phpunit/tests/admin/includes/plugin/validateActivePlugins.php +++ b/tests/phpunit/tests/admin/includes/plugin/validateActivePlugins.php @@ -213,7 +213,7 @@ public function test_should_not_validate_network_plugins_for_other_users() { } /** - * Removes the plugin file without a header, including any left over by an interrupted run. + * Removes the plugin file without a header. */ private function remove_plugin() { if ( $this->no_header_dir ) { @@ -223,13 +223,5 @@ private function remove_plugin() { $this->delete_folders( $dir ); } } - - $dirs = glob( WP_PLUGIN_DIR . '/validate-test-no-header-*', GLOB_ONLYDIR ); - if ( is_array( $dirs ) ) { - foreach ( $dirs as $leftover ) { - $this->rmdir( $leftover ); - $this->delete_folders( $leftover ); - } - } } }