diff --git a/CHANGELOG.md b/CHANGELOG.md index afe2cdf09..e02f2fa4a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ This project adheres to [Semantic Versioning](http://semver.org/). ## [unreleased] Unreleased +### Added + +- The `WPLoader::silentlyActivatePlugins` configuration parameter to activate plugins without firing the `activated_plugin` action. + ## [4.0.14] 2023-12-06; ### Added diff --git a/docs/modules/WPLoader.md b/docs/modules/WPLoader.md index 6cbe58227..5c541e98c 100644 --- a/docs/modules/WPLoader.md +++ b/docs/modules/WPLoader.md @@ -48,6 +48,7 @@ When used in this mode, the module supports the following configuration paramete `WP_PLUGIN_DIR` constant. * `plugins` - a list of plugins to activate and load in the WordPress installation. Each plugin must be specified in a format like `hello.php` or `my-plugin/my-plugin.php` format. +* `silentlyActivatePlugins` - a list of plugins to activate **silently**, without firing their activation hooks. Depending on the plugin, a silent activation might cause the plugin to not work correctly. The list must be in the same format as the `plugins` parameter and plugin should be activated silently only if they are not working correctly during normal activation and are known to work correctly when activated silently. * `bootstrapActions` - a list of actions or callables to call **after** WordPress is loaded and before the tests run. * `theme` - the theme to activate and load in the WordPress installation. The theme must be specified in slug format like diff --git a/src/Module/WPLoader.php b/src/Module/WPLoader.php index ec9ceee9b..17511adb5 100644 --- a/src/Module/WPLoader.php +++ b/src/Module/WPLoader.php @@ -103,6 +103,7 @@ class WPLoader extends Module * configFile: string|string[], * pluginsFolder: string, * plugins: string[], + * silentlyActivatePlugins: string[], * bootstrapActions: string|string[], * theme: string, * AUTH_KEY: string, @@ -141,6 +142,7 @@ class WPLoader extends Module 'configFile' => '', 'pluginsFolder' => '', 'plugins' => [], + 'silentlyActivatePlugins' => [], 'bootstrapActions' => '', 'theme' => '', 'AUTH_KEY' => '', @@ -193,6 +195,37 @@ protected function validateConfig(): void $this->config['dbCharset'] = $this->config['DB_CHARSET'] ?? $this->config['dbCharset'] ?? ''; $this->config['dbCollate'] = $this->config['DB_COLLATE'] ?? $this->config['dbCollate'] ?? ''; $this->config['multisite'] = (bool)($this->config['WP_TESTS_MULTISITE'] ?? $this->config['multisite'] ?? false); + + if (!( + is_array($this->config['plugins']) + && Arr::containsOnly($this->config['plugins'], 'string')) + ) { + throw new ModuleConfigException( + __CLASS__, + 'The `plugins` configuration parameter must be an array of plugin names ' . + 'in the my-plugin/plugin.php or plugin.php format.' + ); + } + + if (!( + is_array($this->config['silentlyActivatePlugins']) + && Arr::containsOnly($this->config['silentlyActivatePlugins'], 'string')) + ) { + throw new ModuleConfigException( + __CLASS__, + 'The `silentlyActivatePlugins` configuration parameter must be an array of plugin names ' . + 'in the my-plugin/plugin.php or plugin.php format.' + ); + } + + if (count(array_intersect($this->config['plugins'], $this->config['silentlyActivatePlugins']))) { + throw new ModuleConfigException( + __CLASS__, + 'The `plugins` and `silentlyActivatePlugins` configuration parameters must not contain the ' . + 'same plugins.' + ); + } + $this->config['theme'] = $this->config['WP_TESTS_MULTISITE'] ?? $this->config['theme'] ?? ''; if (!is_string($this->config['theme'])) { @@ -314,6 +347,7 @@ public function _initialize(): void * configFile: string|string[], * pluginsFolder: string, * plugins: string[], + * silentlyActivatePlugins: string[], * bootstrapActions: string|string[], * theme: string, * AUTH_KEY: string, @@ -645,14 +679,23 @@ private function activatePluginsSwitchThemeInSeparateProcess(): void { /** @var array $plugins */ $plugins = (array)($this->config['plugins'] ?: []); + $silentlyActivatePlugins = (array)($this->config['silentlyActivatePlugins'] ?: []); + $allPlugins = array_merge($plugins, $silentlyActivatePlugins); $multisite = (bool)($this->config['multisite'] ?? false); $closuresFactory = $this->getCodeExecutionFactory(); + $silentFlags = array_merge( + array_fill(0, count($plugins), false), + array_fill(0, count($silentlyActivatePlugins), true) + ); $jobs = array_combine( - array_map(static fn(string $plugin): string => 'plugin::' . $plugin, $plugins), + array_map(static fn(string $plugin): string => 'plugin::' . $plugin, $allPlugins), array_map( - static fn(string $plugin): Closure => $closuresFactory->toActivatePlugin($plugin, $multisite), - $plugins + static function (string $plugin, bool $silent) use ($closuresFactory, $multisite): Closure { + return $closuresFactory->toActivatePlugin($plugin, $multisite, $silent); + }, + $allPlugins, + $silentFlags ) ); diff --git a/src/WordPress/CodeExecution/ActivatePluginAction.php b/src/WordPress/CodeExecution/ActivatePluginAction.php index 47f6a3826..fcac7555f 100644 --- a/src/WordPress/CodeExecution/ActivatePluginAction.php +++ b/src/WordPress/CodeExecution/ActivatePluginAction.php @@ -17,22 +17,23 @@ public function __construct( FileRequest $request, string $wpRootDir, string $plugin, - bool $multisite + bool $multisite, + bool $silent = false ) { $request->setTargetFile($wpRootDir . '/wp-load.php') ->runInFastMode($wpRootDir) ->defineConstant('MULTISITE', $multisite) - ->addAfterLoadClosure(fn() => $this->activatePlugin($plugin, $multisite)); + ->addAfterLoadClosure(fn() => $this->activatePlugin($plugin, $multisite, $silent)); $this->request = $request; } /** * @throws InstallationException */ - private function activatePlugin(string $plugin, bool $multisite): void + private function activatePlugin(string $plugin, bool $multisite, bool $silent = false): void { require_once ABSPATH . 'wp-admin/includes/plugin.php'; - $activated = activate_plugin($plugin, '', $multisite); + $activated = activate_plugin($plugin, '', $multisite, $silent); $activatedString = $multisite ? 'network activated' : 'activated'; $message = "Plugin $plugin could not be $activatedString."; diff --git a/src/WordPress/CodeExecution/CodeExecutionFactory.php b/src/WordPress/CodeExecution/CodeExecutionFactory.php index f47166358..3d095096f 100644 --- a/src/WordPress/CodeExecution/CodeExecutionFactory.php +++ b/src/WordPress/CodeExecution/CodeExecutionFactory.php @@ -38,14 +38,14 @@ public function toCheckIfWpIsInstalled(bool $multisite): Closure ->getClosure(); } - public function toActivatePlugin(string $plugin, bool $multisite): Closure + public function toActivatePlugin(string $plugin, bool $multisite, bool $silent = false): Closure { $request = $this->requestFactory->buildGetRequest() ->blockHttpRequests() ->setRedirectFiles($this->redirectFiles) ->addPresetGlobalVars($this->presetGlobalVars); - return (new ActivatePluginAction($request, $this->wpRootDir, $plugin, $multisite)) + return (new ActivatePluginAction($request, $this->wpRootDir, $plugin, $multisite, $silent)) ->getClosure(); } diff --git a/tests/unit/lucatume/WPBrowser/Events/Module/WPLoaderTest.php b/tests/unit/lucatume/WPBrowser/Events/Module/WPLoaderTest.php index 516f43b45..5ac694ea1 100644 --- a/tests/unit/lucatume/WPBrowser/Events/Module/WPLoaderTest.php +++ b/tests/unit/lucatume/WPBrowser/Events/Module/WPLoaderTest.php @@ -28,6 +28,7 @@ use lucatume\WPBrowser\WordPress\InstallationException; use lucatume\WPBrowser\WordPress\InstallationState\InstallationStateInterface; use lucatume\WPBrowser\WordPress\InstallationState\Scaffolded; +use PHPUnit\Framework\AssertionFailedError; use PHPUnit\Framework\TestResult; use stdClass; use tad\Codeception\SnapshotAssertions\SnapshotAssertions; @@ -2396,16 +2397,7 @@ public function should_skip_installation_when_skip_install_is_true(): void 'woocommerce' ] ]); - if (!mkdir($wpRootDir . '/tests/some_test_suite', 0777, true)) { - throw new \RuntimeException('Failed to create the test suite folder.'); - } - $moduleContainerConfig = [ - 'config' => [ - 'suite' => 'some_test_suite', - 'path' => $wpRootDir . '/tests/some_test_suite' - ] - ]; - $moduleConfig = [ + $this->config = [ 'wpRootFolder' => $wpRootDir, 'dbUrl' => $installationDb->getDbUrl(), 'tablePrefix' => 'test_', @@ -2415,7 +2407,7 @@ public function should_skip_installation_when_skip_install_is_true(): void ]; // Run the module a first time: it should create the flag file indicating the database was installed. - $wpLoader = $this->module($moduleContainerConfig, $moduleConfig); + $wpLoader = $this->module(); $moduleSplObjectHash = spl_object_hash($wpLoader); $this->assertInIsolation( static function () use ($wpLoader, $moduleSplObjectHash) { @@ -2463,7 +2455,7 @@ static function () use ($wpLoader, $moduleSplObjectHash) { $this->assertEquals('twentytwenty', $checkDb->getOption('stylesheet')); // Run a second time, this time the installation should be skipped. - $wpLoader = $this->module($moduleContainerConfig, $moduleConfig); + $wpLoader = $this->module(); $this->assertInIsolation( static function () use ($moduleSplObjectHash, $wpLoader) { $beforeInstallCalled = false; @@ -2499,9 +2491,9 @@ static function () use ($moduleSplObjectHash, $wpLoader) { ); // Now run in --debug mode, the installation should run again. - $wpLoader = $this->module($moduleContainerConfig, $moduleConfig); + $wpLoader = $this->module(); $this->assertInIsolation( - static function () use ($moduleSplObjectHash, $wpLoader) { + static function () use ($wpLoader) { $beforeInstallCalled = false; $afterInstallCalled = false; $afterBootstrapCalled = false; @@ -2539,4 +2531,161 @@ static function () use ($moduleSplObjectHash, $wpLoader) { } ); } + + /** + * It should throw if silentlyActivatePlugins config parameter is not a list of strings + * + * @test + * @dataProvider notArrayOfStringsProvider + */ + public function should_throw_if_silently_activate_plugins_config_parameter_is_not_a_list_of_strings($input): void + { + $wpRootDir = Env::get('WORDPRESS_ROOT_DIR'); + $db = (new Installation($wpRootDir))->getDb(); + $this->config = [ + 'wpRootFolder' => $wpRootDir, + 'dbUrl' => $db->getDbUrl(), + 'silentlyActivatePlugins' => $input, + ]; + + $this->expectException(ModuleConfigException::class); + + $this->module(); + } + + /** + * It should throw if plugin appears in both plugins and silentlyActivatePlugins config parameters + * + * @test + */ + public function should_throw_if_plugin_appears_in_both_plugins_and_silently_activate_plugins_config_parameters( + ): void + { + $wpRootDir = Env::get('WORDPRESS_ROOT_DIR'); + $db = (new Installation($wpRootDir))->getDb(); + $this->config = [ + 'wpRootFolder' => $wpRootDir, + 'dbUrl' => $db->getDbUrl(), + 'plugins' => ['woocommerce/woocommerce.php', 'my-plugin/plugin.php'], + 'silentlyActivatePlugins' => ['foo-plugin/plugin.php', 'woocommerce/woocommerce.php'], + ]; + + $this->expectException(ModuleConfigException::class); + + $this->module(); + } + + /** + * It should fail to activate when plugins generate unexpected output + * + * @test + */ + public function should_fail_to_activate_when_plugins_generate_unexpected_output(): void + { + $wpRootDir = FS::tmpDir('wploader_'); + $installation = Installation::scaffold($wpRootDir); + $dbName = Random::dbName(); + $dbHost = Env::get('WORDPRESS_DB_HOST'); + $dbUser = Env::get('WORDPRESS_DB_USER'); + $dbPassword = Env::get('WORDPRESS_DB_PASSWORD'); + $installationDb = new MysqlDatabase($dbName, $dbUser, $dbPassword, $dbHost, 'wp_'); + $installation->configure($installationDb); + $this->copyOverContentFromTheMainInstallation($installation, [ + 'plugins' => [ + 'woocommerce' + ] + ]); + // Create a plugin that will raise a doing_it_wrong error on activation. + FS::mkdirp($wpRootDir . '/wp-content/plugins', [ + 'my-plugin' => [ + 'plugin.php' => <<< PHP +config = [ + 'wpRootFolder' => $wpRootDir, + 'dbUrl' => $installationDb->getDbUrl(), + 'tablePrefix' => 'test_', + 'plugins' => ['woocommerce/woocommerce.php', 'my-plugin/plugin.php'], + ]; + + // Run a first initialization that should fail due to the doing_it_wrong error. + $wpLoader = $this->module(); + + $this->expectException(ModuleException::class); + + $this->assertInIsolation( + static function () use ($wpLoader) { + $wpLoader->_initialize(); + } + ); + } + + /** + * It should allow activating plugins silently + * + * @test + */ + public function should_allow_activating_plugins_silently(): void + { + $wpRootDir = FS::tmpDir('wploader_'); + $installation = Installation::scaffold($wpRootDir); + $dbName = Random::dbName(); + $dbHost = Env::get('WORDPRESS_DB_HOST'); + $dbUser = Env::get('WORDPRESS_DB_USER'); + $dbPassword = Env::get('WORDPRESS_DB_PASSWORD'); + $installationDb = new MysqlDatabase($dbName, $dbUser, $dbPassword, $dbHost, 'wp_'); + $installation->configure($installationDb); + $this->copyOverContentFromTheMainInstallation($installation, [ + 'plugins' => [ + 'woocommerce' + ] + ]); + // Create a plugin that will raise a doing_it_wrong error on activation. + FS::mkdirp($wpRootDir . '/wp-content/plugins', [ + 'my-plugin' => [ + 'plugin.php' => <<< PHP +config = [ + 'wpRootFolder' => $wpRootDir, + 'dbUrl' => $installationDb->getDbUrl(), + 'tablePrefix' => 'test_', + 'plugins' => ['woocommerce/woocommerce.php'], + 'silentlyActivatePlugins' => ['my-plugin/plugin.php'], + ]; + + // Run a first initialization that should fail due to the doing_it_wrong error. + $wpLoader = $this->module(); + + $this->assertInIsolation( + static function () use ($wpLoader) { + $wpLoader->_initialize(); + + assertEquals('', get_option('my_plugin_activated')); + assertEquals('__loaded__', get_option('my_plugin_loaded')); + } + ); + } }