From 39ddeb94c1b5e49ae1220331bdc6c7814e052d5e Mon Sep 17 00:00:00 2001 From: Daniel Bannert <prisis@users.noreply.github.com> Date: Tue, 11 Sep 2018 11:45:49 +0200 Subject: [PATCH] adding composer-scripts configurator (#69) | Q | A | --------------- | --- | Bug fix? | no | New feature? | yes | BC breaks? | no | Deprecations? | no | Related tickets | - | License | MIT | Doc PR | - --- doc/CONFIGURATORS.md | 26 ++- src/Automatic/Automatic.php | 14 +- src/Automatic/Configurator.php | 10 +- .../ComposerAutoScriptsConfigurator.php | 100 ++++++++ .../ComposerScriptsConfigurator.php | 152 +++++++++++-- src/Automatic/QuestionFactory.php | 17 ++ src/Automatic/ScriptExecutor.php | 3 + .../Configurator/AbstractConfigurator.php | 2 +- .../ComposerAutoScriptsConfiguratorTest.php | 144 ++++++++++++ .../ComposerScriptsConfiguratorTest.php | 215 ++++++++++++++++-- .../CopyFromPackageConfiguratorTest.php | 42 ++-- .../Configurator/EnvConfiguratorTest.php | 6 +- .../GitignoreConfiguratorTest.php | 2 +- tests/Automatic/ConfiguratorTest.php | 14 +- tests/Automatic/PackageConfiguratorTest.php | 4 +- 15 files changed, 665 insertions(+), 86 deletions(-) create mode 100644 src/Automatic/Configurator/ComposerAutoScriptsConfigurator.php create mode 100644 tests/Automatic/Configurator/ComposerAutoScriptsConfiguratorTest.php diff --git a/doc/CONFIGURATORS.md b/doc/CONFIGURATORS.md index 5a35f8a2..14142613 100644 --- a/doc/CONFIGURATORS.md +++ b/doc/CONFIGURATORS.md @@ -9,7 +9,7 @@ Configurators define the different tasks executed when installing a dependency, The package only contain the tasks needed to install and configure the dependency, because Narrowspark Automatic Configurators are smart enough to reverse those tasks when uninstalling and unconfiguring the dependencies. -Narrowspark Automatic comes with several types of tasks, which are called **configurators**: `copy`, `env`, `composer-scripts`, `gitignore` and a special configurator `post-install-output`. +Narrowspark Automatic comes with several types of tasks, which are called **configurators**: `copy`, `env`, `composer-scripts`, `composer-auto-scripts`, `gitignore` and a special configurator `post-install-output`. ### Copy Configurator `copy` @@ -30,7 +30,7 @@ directory of the application: The `%BIN_DIR%` string is a special value that it's turned into the absolute path of the binaries directory. You can access any variable defined in -the `extra` section of your `composer.json` file: +the `extra` section of your root `composer.json` file: ```json { @@ -72,22 +72,38 @@ The `###> your-package-name-here ###` section separators are needed by Narrowspa to detect the contents added by this dependency in case you uninstall it later. > !!! Don't remove or modify these separators. -Composer Scripts Configurator `composer-scripts` +Composer Auto Scripts Configurator `composer-auto-scripts` -Registers scripts in the `auto-scripts` section of the `composer.json` file +Registers `auto-scripts` in the `composer-scripts` section of the root `composer.json` file to execute them automatically when running `composer install` and `composer update`. The value is an associative array where the key is the script to execute (including all its arguments and options) and the value is the type of script (`php-script` for PHP scripts, ``script`` for any shell script): ```json { "configurators": { - "composer-scripts": { + "composer-auto-scripts": { "echo \"hallo\";": "php-script", "bash -c \"echo hallo\"": "script" } } } ``` +Composer Scripts Configurator `composer-scripts` + +Registers [composer scripts](https://getcomposer.org/doc/articles/scripts.md) in the `composer-scripts` section of the root `composer.json` file. +Only the composer `command`, `installer` and `package` events are supported, you will get a warning if other events a used. + +```json +{ + "configurators": { + "composer-auto-scripts": { + "post-autoload-dump" : [ + "Your\\Namespace\\ComposerScripts::dump" + ] + } + } +} +``` You can create your own script executor, create a new class inside your Package Repository in a `Automatic` folder and extend `Narrowspark\Automatic\Common\ScriptExtender\AbstractScriptExtender` the example below shows you how it should look: diff --git a/src/Automatic/Automatic.php b/src/Automatic/Automatic.php index ec2141ec..d94cdea7 100644 --- a/src/Automatic/Automatic.php +++ b/src/Automatic/Automatic.php @@ -155,13 +155,13 @@ public static function getSubscribedEvents(): array InstallerEvents::POST_DEPENDENCIES_SOLVING => [['populateFilesCacheDir', \PHP_INT_MAX]], PackageEvents::PRE_PACKAGE_INSTALL => [['populateFilesCacheDir', ~\PHP_INT_MAX]], PackageEvents::PRE_PACKAGE_UPDATE => [['populateFilesCacheDir', ~\PHP_INT_MAX]], - PackageEvents::POST_PACKAGE_INSTALL => [['record'], ['auditPackage']], - PackageEvents::POST_PACKAGE_UPDATE => [['record'], ['auditPackage']], + PackageEvents::POST_PACKAGE_INSTALL => [['record'], ['auditPackage', \PHP_INT_MAX]], + PackageEvents::POST_PACKAGE_UPDATE => [['record'], ['auditPackage', \PHP_INT_MAX]], PackageEvents::POST_PACKAGE_UNINSTALL => 'record', PluginEvents::PRE_FILE_DOWNLOAD => 'onFileDownload', - ScriptEvents::POST_INSTALL_CMD => [['onPostInstall'], ['auditComposerLock']], - ScriptEvents::POST_UPDATE_CMD => [['onPostUpdate'], ['auditComposerLock']], - ScriptEvents::POST_CREATE_PROJECT_CMD => [['onPostCreateProject', \PHP_INT_MAX], ['runSkeletonGenerator']], + ScriptEvents::POST_INSTALL_CMD => [['onPostInstall', \PHP_INT_MAX - 1], ['auditComposerLock', \PHP_INT_MAX]], + ScriptEvents::POST_UPDATE_CMD => [['onPostUpdate', \PHP_INT_MAX - 1], ['auditComposerLock', \PHP_INT_MAX]], + ScriptEvents::POST_CREATE_PROJECT_CMD => [['onPostCreateProject', \PHP_INT_MAX], ['runSkeletonGenerator', ~\PHP_INT_MAX]], ]; } @@ -1121,13 +1121,13 @@ private function showWarningOnRemainingConfigurators( ): void { $packageConfigurators = \array_keys((array) $package->getConfig(ConfiguratorContract::TYPE)); - foreach (\array_keys($configurator->getConfigurators()) as $key) { + foreach (\array_keys($configurator->getConfigurators()) as $key => $value) { if (isset($packageConfigurators[$key])) { unset($packageConfigurators[$key]); } } - foreach (\array_keys($packageConfigurator->getConfigurators()) as $key) { + foreach (\array_keys($packageConfigurator->getConfigurators()) as $key => $value) { if (isset($packageConfigurators[$key])) { unset($packageConfigurators[$key]); } diff --git a/src/Automatic/Configurator.php b/src/Automatic/Configurator.php index db955963..2babcb2e 100644 --- a/src/Automatic/Configurator.php +++ b/src/Automatic/Configurator.php @@ -3,6 +3,7 @@ namespace Narrowspark\Automatic; use Narrowspark\Automatic\Common\Contract\Configurator as ConfiguratorContract; +use Narrowspark\Automatic\Configurator\ComposerAutoScriptsConfigurator; use Narrowspark\Automatic\Configurator\ComposerScriptsConfigurator; use Narrowspark\Automatic\Configurator\CopyFromPackageConfigurator; use Narrowspark\Automatic\Configurator\EnvConfigurator; @@ -16,10 +17,11 @@ final class Configurator extends AbstractConfigurator * @var array */ protected $configurators = [ - 'composer-scripts' => ComposerScriptsConfigurator::class, - 'copy' => CopyFromPackageConfigurator::class, - 'env' => EnvConfigurator::class, - 'gitignore' => GitIgnoreConfigurator::class, + 'composer-auto-scripts' => ComposerAutoScriptsConfigurator::class, + 'composer-scripts' => ComposerScriptsConfigurator::class, + 'copy' => CopyFromPackageConfigurator::class, + 'env' => EnvConfigurator::class, + 'gitignore' => GitIgnoreConfigurator::class, ]; /** diff --git a/src/Automatic/Configurator/ComposerAutoScriptsConfigurator.php b/src/Automatic/Configurator/ComposerAutoScriptsConfigurator.php new file mode 100644 index 00000000..6c1c549f --- /dev/null +++ b/src/Automatic/Configurator/ComposerAutoScriptsConfigurator.php @@ -0,0 +1,100 @@ +<?php +declare(strict_types=1); +namespace Narrowspark\Automatic\Configurator; + +use Composer\Composer; +use Composer\IO\IOInterface; +use Narrowspark\Automatic\Common\Configurator\AbstractConfigurator; +use Narrowspark\Automatic\Common\Contract\Configurator as ConfiguratorContract; +use Narrowspark\Automatic\Common\Contract\Package as PackageContract; +use Narrowspark\Automatic\Common\Util; + +final class ComposerAutoScriptsConfigurator extends AbstractConfigurator +{ + /** + * A json instance. + * + * @var \Composer\Json\JsonFile + */ + private $json; + + /** + * A json manipulator instance. + * + * @var \Composer\Json\JsonManipulator + */ + private $manipulator; + + /** + * {@inheritdoc} + */ + public function __construct(Composer $composer, IOInterface $io, array $options = []) + { + parent::__construct($composer, $io, $options); + + [$json, $manipulator] = Util::getComposerJsonFileAndManipulator(); + + $this->json = $json; + $this->manipulator = $manipulator; + } + + /** + * {@inheritdoc} + */ + public static function getName(): string + { + return 'composer-auto-scripts'; + } + + /** + * {@inheritdoc} + */ + public function configure(PackageContract $package): void + { + $autoScripts = $this->getComposerAutoScripts(); + + $autoScripts = \array_merge($autoScripts, (array) $package->getConfig(ConfiguratorContract::TYPE, self::getName())); + + $this->manipulateAndWrite($autoScripts); + } + + /** + * {@inheritdoc} + */ + public function unconfigure(PackageContract $package): void + { + $autoScripts = $this->getComposerAutoScripts(); + + foreach (\array_keys((array) $package->getConfig(ConfiguratorContract::TYPE, self::getName())) as $cmd) { + unset($autoScripts[$cmd]); + } + + $this->manipulateAndWrite($autoScripts); + } + + /** + * Get root composer.json content and the auto-scripts section. + * + * @return array + */ + private function getComposerAutoScripts(): array + { + $jsonContents = $this->json->read(); + + return $jsonContents['scripts']['auto-scripts'] ?? []; + } + + /** + * Manipulate the root composer.json with given auto-scripts. + * + * @param array $autoScripts + * + * @return void + */ + private function manipulateAndWrite(array $autoScripts): void + { + $this->manipulator->addSubNode('scripts', 'auto-scripts', $autoScripts); + + \file_put_contents($this->json->getPath(), $this->manipulator->getContents()); + } +} diff --git a/src/Automatic/Configurator/ComposerScriptsConfigurator.php b/src/Automatic/Configurator/ComposerScriptsConfigurator.php index 98e737e6..0771ccdf 100644 --- a/src/Automatic/Configurator/ComposerScriptsConfigurator.php +++ b/src/Automatic/Configurator/ComposerScriptsConfigurator.php @@ -3,13 +3,28 @@ namespace Narrowspark\Automatic\Configurator; use Composer\Composer; +use Composer\Installer\InstallerEvents; +use Composer\Installer\PackageEvents; use Composer\IO\IOInterface; +use Composer\Script\ScriptEvents; use Narrowspark\Automatic\Common\Configurator\AbstractConfigurator; +use Narrowspark\Automatic\Common\Contract\Configurator as ConfiguratorContract; use Narrowspark\Automatic\Common\Contract\Package as PackageContract; use Narrowspark\Automatic\Common\Util; +use Narrowspark\Automatic\QuestionFactory; final class ComposerScriptsConfigurator extends AbstractConfigurator { + /** + * @var string + */ + private const WHITELIST = 'composer-script-whitelist'; + + /** + * @var string + */ + private const BLACKLIST = 'composer-script-blacklist'; + /** * A json instance. * @@ -24,6 +39,34 @@ final class ComposerScriptsConfigurator extends AbstractConfigurator */ private $manipulator; + /** + * All allowed composer scripts. + * + * @var array + */ + private $allowedComposerEvents = [ + ScriptEvents::POST_ARCHIVE_CMD, + ScriptEvents::POST_AUTOLOAD_DUMP, + ScriptEvents::POST_CREATE_PROJECT_CMD, + ScriptEvents::POST_INSTALL_CMD, + ScriptEvents::POST_ROOT_PACKAGE_INSTALL, + ScriptEvents::POST_STATUS_CMD, + ScriptEvents::POST_UPDATE_CMD, + ScriptEvents::PRE_ARCHIVE_CMD, + ScriptEvents::PRE_AUTOLOAD_DUMP, + ScriptEvents::PRE_INSTALL_CMD, + ScriptEvents::PRE_STATUS_CMD, + ScriptEvents::PRE_UPDATE_CMD, + InstallerEvents::POST_DEPENDENCIES_SOLVING, + InstallerEvents::PRE_DEPENDENCIES_SOLVING, + PackageEvents::POST_PACKAGE_INSTALL, + PackageEvents::POST_PACKAGE_UNINSTALL, + PackageEvents::POST_PACKAGE_UPDATE, + PackageEvents::PRE_PACKAGE_INSTALL, + PackageEvents::PRE_PACKAGE_UNINSTALL, + PackageEvents::PRE_PACKAGE_UPDATE, + ]; + /** * {@inheritdoc} */ @@ -31,10 +74,7 @@ public function __construct(Composer $composer, IOInterface $io, array $options { parent::__construct($composer, $io, $options); - [$json, $manipulator] = Util::getComposerJsonFileAndManipulator(); - - $this->json = $json; - $this->manipulator = $manipulator; + [$this->json, $this->manipulator] = Util::getComposerJsonFileAndManipulator(); } /** @@ -50,11 +90,67 @@ public static function getName(): string */ public function configure(PackageContract $package): void { - $autoScripts = $this->getComposerContentAndAutoScripts(); + $packageEvents = (array) $package->getConfig(ConfiguratorContract::TYPE, self::getName()); + + if (\count($packageEvents) === 0) { + return; + } + + $composerContent = $this->json->read(); + + if (isset($composerContent['extra'][Util::COMPOSER_EXTRA_KEY][self::BLACKLIST])) { + $blackList = \array_flip($composerContent['extra'][Util::COMPOSER_EXTRA_KEY][self::BLACKLIST]); + + if (isset($blackList[$package->getName()])) { + $this->io->write(\sprintf('Composer scripts for [%s] skipped, because it was found in the [%s]', $package->getPrettyName(), self::BLACKLIST)); + + return; + } + } - $autoScripts = \array_merge($autoScripts, (array) $package->getConfig(self::getName())); + $allowedEvents = []; - $this->manipulateAndWrite($autoScripts); + foreach ($this->allowedComposerEvents as $event) { + if (isset($packageEvents[$event])) { + $allowedEvents[$event] = (array) $packageEvents[$event]; + + unset($packageEvents[$event]); + } + } + + $allowed = false; + + if (\count($allowedEvents) !== 0) { + if (isset($composerContent['extra'][Util::COMPOSER_EXTRA_KEY][self::WHITELIST])) { + $whiteList = \array_flip($composerContent['extra'][Util::COMPOSER_EXTRA_KEY][self::WHITELIST]); + + if (isset($whiteList[$package->getName()])) { + $allowed = true; + } + } + + if ($allowed === false) { + $allowed = $this->io->askConfirmation(QuestionFactory::getPackageScriptsQuestion($package->getPrettyName()), false); + } + } + + if (\count($packageEvents) !== 0) { + $this->io->write(\sprintf( + '<warning> Found not allowed composer events [%s] in [%s]</>' . \PHP_EOL, + \implode(', ', \array_keys($packageEvents)), + $package->getName() + )); + } + + if ($allowed) { + $this->manipulator->addSubNode( + 'extra', + Util::COMPOSER_EXTRA_KEY, + \array_merge($composerContent['extra'][Util::COMPOSER_EXTRA_KEY] ?? [], [self::WHITELIST => [$package->getName()]]) + ); + + $this->manipulateAndWrite(\array_merge($this->getComposerScripts(), $allowedEvents)); + } } /** @@ -62,13 +158,33 @@ public function configure(PackageContract $package): void */ public function unconfigure(PackageContract $package): void { - $autoScripts = $this->getComposerContentAndAutoScripts(); + $composerScripts = $this->getComposerScripts(); + + foreach ((array) $package->getConfig(ConfiguratorContract::TYPE, self::getName()) as $key => $scripts) { + foreach ((array) $scripts as $script) { + if (isset($this->allowedComposerEvents[$key], $composerScripts[$key][$script])) { + unset($composerScripts[$key][$script]); + } + } + } - foreach (\array_keys((array) $package->getConfig(self::getName())) as $cmd) { - unset($autoScripts[$cmd]); + $composerContent = $this->json->read(); + + if (isset($composerContent['extra'][Util::COMPOSER_EXTRA_KEY][self::WHITELIST])) { + $whiteList = \array_flip($composerContent['extra'][Util::COMPOSER_EXTRA_KEY][self::WHITELIST]); + + if (isset($whiteList[$package->getName()])) { + unset($composerContent['extra'][Util::COMPOSER_EXTRA_KEY][self::WHITELIST][$whiteList[$package->getName()]]); + + $this->manipulator->addSubNode( + 'extra', + Util::COMPOSER_EXTRA_KEY, + $composerContent['extra'][Util::COMPOSER_EXTRA_KEY] + ); + } } - $this->manipulateAndWrite($autoScripts); + $this->manipulateAndWrite($composerScripts); } /** @@ -76,21 +192,23 @@ public function unconfigure(PackageContract $package): void * * @return array */ - private function getComposerContentAndAutoScripts(): array + private function getComposerScripts(): array { $jsonContents = $this->json->read(); - return $jsonContents['scripts']['auto-scripts'] ?? []; + return $jsonContents['scripts'] ?? []; } /** - * Manipulate the root composer.json with given auto-scripts. + * Manipulate the root composer.json with given scripts. + * + * @param array $scripts * - * @param array $autoScripts + * @return void */ - private function manipulateAndWrite(array $autoScripts): void + private function manipulateAndWrite(array $scripts): void { - $this->manipulator->addSubNode('scripts', 'auto-scripts', $autoScripts); + $this->manipulator->addMainKey('scripts', $scripts); \file_put_contents($this->json->getPath(), $this->manipulator->getContents()); } diff --git a/src/Automatic/QuestionFactory.php b/src/Automatic/QuestionFactory.php index d050effd..bdbcb849 100644 --- a/src/Automatic/QuestionFactory.php +++ b/src/Automatic/QuestionFactory.php @@ -32,6 +32,23 @@ public static function getPackageQuestion(string $name, ?string $url): string return \sprintf(" Review the package from %s.\n" . $message, \str_replace('.git', '', $url), $name); } + /** + * Returns the questions for package scripts. + * + * @param string $name + * + * @return string + */ + public static function getPackageScriptsQuestion(string $name): string + { + $message = <<<'PHP' + Do you want to add this package [%s] composer scripts? + (defaults to <comment>no</comment>): +PHP; + + return \sprintf($message, $name); + } + /** * Validate given input answer. * diff --git a/src/Automatic/ScriptExecutor.php b/src/Automatic/ScriptExecutor.php index 2c055a9e..53f7c208 100644 --- a/src/Automatic/ScriptExecutor.php +++ b/src/Automatic/ScriptExecutor.php @@ -16,6 +16,9 @@ final class ScriptExecutor { use ExpandTargetDirTrait; + /** + * @var string + */ public const TYPE = 'script-extenders'; /** diff --git a/src/Common/Configurator/AbstractConfigurator.php b/src/Common/Configurator/AbstractConfigurator.php index 7ac57627..b9066fa2 100644 --- a/src/Common/Configurator/AbstractConfigurator.php +++ b/src/Common/Configurator/AbstractConfigurator.php @@ -76,7 +76,7 @@ protected function write($messages): void } foreach ($messages as $i => $message) { - $messages[$i] = ' · ' . $message; + $messages[$i] = ' - ' . $message; } $this->io->writeError($messages, true, IOInterface::VERBOSE); diff --git a/tests/Automatic/Configurator/ComposerAutoScriptsConfiguratorTest.php b/tests/Automatic/Configurator/ComposerAutoScriptsConfiguratorTest.php new file mode 100644 index 00000000..eee43011 --- /dev/null +++ b/tests/Automatic/Configurator/ComposerAutoScriptsConfiguratorTest.php @@ -0,0 +1,144 @@ +<?php +declare(strict_types=1); +namespace Narrowspark\Automatic\Test\Configurator; + +use Composer\Composer; +use Composer\IO\IOInterface; +use Composer\Json\JsonFile; +use Composer\Json\JsonManipulator; +use Narrowspark\Automatic\Common\Contract\Configurator as ConfiguratorContract; +use Narrowspark\Automatic\Common\Contract\Package as PackageContract; +use Narrowspark\Automatic\Common\Traits\GetGenericPropertyReaderTrait; +use Narrowspark\Automatic\Configurator\ComposerAutoScriptsConfigurator; +use Narrowspark\Automatic\Test\Fixture\ComposerJsonFactory; +use Narrowspark\TestingHelper\Phpunit\MockeryTestCase; + +/** + * @internal + */ +final class ComposerAutoScriptsConfiguratorTest extends MockeryTestCase +{ + use GetGenericPropertyReaderTrait; + + /** + * @var \Composer\Composer + */ + private $composer; + + /** + * @var \Composer\IO\IOInterface|\Mockery\MockInterface + */ + private $ioMock; + + /** + * @var \Composer\Json\JsonFile|\Mockery\MockInterface + */ + private $jsonMock; + + /** + * @var \Composer\Json\JsonManipulator|\Mockery\MockInterface + */ + private $jsonManipulatorMock; + + /** + * @var \Narrowspark\Automatic\Configurator\ComposerAutoScriptsConfigurator + */ + private $configurator; + + /** + * {@inheritdoc} + */ + protected function setUp(): void + { + parent::setUp(); + + $this->jsonMock = $this->mock(JsonFile::class); + $this->jsonManipulatorMock = $this->mock(JsonManipulator::class); + + $this->composer = new Composer(); + $this->ioMock = $this->mock(IOInterface::class); + + $this->configurator = new ComposerAutoScriptsConfigurator($this->composer, $this->ioMock, ['self-dir' => 'test']); + + $callback = $this->getGenericPropertyReader(); + + $json = &$callback($this->configurator, 'json'); + $json = $this->jsonMock; + + $manipulator = &$callback($this->configurator, 'manipulator'); + $manipulator = $this->jsonManipulatorMock; + } + + public function testGetName(): void + { + static::assertSame('composer-auto-scripts', ComposerAutoScriptsConfigurator::getName()); + } + + public function testConfigure(): void + { + $composerRootJsonString = ComposerJsonFactory::createComposerScriptJson('configure', ['auto-scripts' => []]); + $composerRootJsonData = ComposerJsonFactory::jsonToArray($composerRootJsonString); + + $script = ['php -v' => 'script']; + + $packageMock = $this->mock(PackageContract::class); + $packageMock->shouldReceive('getConfig') + ->once() + ->with(ConfiguratorContract::TYPE, ComposerAutoScriptsConfigurator::getName()) + ->andReturn($script); + + $this->jsonMock->shouldReceive('read') + ->andReturn($composerRootJsonData); + + $composerJsonPath = __DIR__ . '/composer.json'; + + $this->jsonMock->shouldReceive('getPath') + ->once() + ->andReturn($composerJsonPath); + + $this->jsonManipulatorMock->shouldReceive('addSubNode') + ->once() + ->with('scripts', 'auto-scripts', $script); + + $composerRootJsonData['scripts']['auto-scripts'] = $script; + + $this->jsonManipulatorMock->shouldReceive('getContents') + ->andReturn(ComposerJsonFactory::arrayToJson($composerRootJsonData)); + + $this->configurator->configure($packageMock); + + \unlink($composerJsonPath); + } + + public function testUnconfigure(): void + { + $composerRootJsonString = ComposerJsonFactory::createComposerScriptJson('unconfigure', ['auto-scripts' => ['php -v' => 'script', 'list' => 'cerebro-cmd']]); + $composerRootJsonData = ComposerJsonFactory::jsonToArray($composerRootJsonString); + + $packageMock = $this->mock(PackageContract::class); + $packageMock->shouldReceive('getConfig') + ->once() + ->with(ConfiguratorContract::TYPE, ComposerAutoScriptsConfigurator::getName()) + ->andReturn(['php -v' => 'script']); + + $this->jsonMock->shouldReceive('read') + ->andReturn($composerRootJsonData); + + $composerJsonPath = __DIR__ . '/composer.json'; + + $this->jsonMock->shouldReceive('getPath') + ->once() + ->andReturn($composerJsonPath); + + $this->jsonManipulatorMock->shouldReceive('addSubNode') + ->once() + ->with('scripts', 'auto-scripts', ['list' => 'cerebro-cmd']); + + $this->jsonManipulatorMock->shouldReceive('getContents') + ->andReturn(ComposerJsonFactory::arrayToJson($composerRootJsonData)); + + $this->configurator->unconfigure($packageMock); + + \unlink($composerJsonPath); + } +} diff --git a/tests/Automatic/Configurator/ComposerScriptsConfiguratorTest.php b/tests/Automatic/Configurator/ComposerScriptsConfiguratorTest.php index 9ad79646..463afc7e 100644 --- a/tests/Automatic/Configurator/ComposerScriptsConfiguratorTest.php +++ b/tests/Automatic/Configurator/ComposerScriptsConfiguratorTest.php @@ -6,9 +6,12 @@ use Composer\IO\IOInterface; use Composer\Json\JsonFile; use Composer\Json\JsonManipulator; -use Narrowspark\Automatic\Common\Contract\Package as PackageContract; +use Narrowspark\Automatic\Common\Contract\Configurator as ConfiguratorContract; +use Narrowspark\Automatic\Common\Package; use Narrowspark\Automatic\Common\Traits\GetGenericPropertyReaderTrait; +use Narrowspark\Automatic\Common\Util; use Narrowspark\Automatic\Configurator\ComposerScriptsConfigurator; +use Narrowspark\Automatic\QuestionFactory; use Narrowspark\Automatic\Test\Fixture\ComposerJsonFactory; use Narrowspark\TestingHelper\Phpunit\MockeryTestCase; @@ -75,69 +78,245 @@ public function testGetName(): void public function testConfigure(): void { - $composerRootJsonString = ComposerJsonFactory::createComposerScriptJson('configure', ['auto-scripts' => []]); + $composerRootJsonString = ComposerJsonFactory::createAutomaticComposerJson('stub/stub'); $composerRootJsonData = ComposerJsonFactory::jsonToArray($composerRootJsonString); - $script = ['php -v' => 'script']; + $package = new Package('Stub/stub', '1.0.0'); + $package->setConfig([ + ConfiguratorContract::TYPE => [ + ComposerScriptsConfigurator::getName() => [ + 'post-autoload-dump' => ['Foo\\Bar'], + ], + ], + ]); - $packageMock = $this->mock(PackageContract::class); - $packageMock->shouldReceive('getConfig') + $this->jsonMock->shouldReceive('read') + ->andReturn($composerRootJsonData); + + $composerJsonPath = __DIR__ . '/composer.json'; + \file_put_contents($composerJsonPath, \json_encode(['extra' => []])); + + $this->jsonMock->shouldReceive('getPath') ->once() - ->with(ComposerScriptsConfigurator::getName()) - ->andReturn($script); + ->andReturn($composerJsonPath); + + $whitelist = ['composer-script-whitelist' => [$package->getName()]]; + + $this->jsonManipulatorMock->shouldReceive('addSubNode') + ->once() + ->with('extra', Util::COMPOSER_EXTRA_KEY, $whitelist); + + $this->jsonManipulatorMock->shouldReceive('addMainKey') + ->once() + ->with('scripts', ['post-autoload-dump' => ['Foo\\Bar']]); + + $composerRootJsonData['scripts']['auto-scripts'] = $whitelist; + + $this->jsonManipulatorMock->shouldReceive('getContents') + ->andReturn(ComposerJsonFactory::arrayToJson($composerRootJsonData)); + + $this->ioMock->shouldReceive('askConfirmation') + ->once() + ->with(QuestionFactory::getPackageScriptsQuestion($package->getPrettyName()), false) + ->andReturn(true); + + $this->configurator->configure($package); + + \unlink($composerJsonPath); + } + + public function testConfigureWithUpdate(): void + { + $oldWhitelist = ['composer-script-whitelist' => ['stub/stub']]; + + $composerRootJsonString = ComposerJsonFactory::createAutomaticComposerJson('stub/stub', [], [], $oldWhitelist); + $composerRootJsonData = ComposerJsonFactory::jsonToArray($composerRootJsonString); + + $package = new Package('Stub/stub', '1.0.0'); + $package->setConfig([ + ConfiguratorContract::TYPE => [ + ComposerScriptsConfigurator::getName() => [ + 'post-autoload-dump' => ['Foo\\Bar'], + ], + ], + ]); $this->jsonMock->shouldReceive('read') ->andReturn($composerRootJsonData); $composerJsonPath = __DIR__ . '/composer.json'; + \file_put_contents($composerJsonPath, \json_encode(['extra' => []])); + $this->jsonMock->shouldReceive('getPath') ->once() ->andReturn($composerJsonPath); + $whitelist = ['composer-script-whitelist' => [$package->getName()]]; + $this->jsonManipulatorMock->shouldReceive('addSubNode') ->once() - ->with('scripts', 'auto-scripts', $script); + ->with('extra', Util::COMPOSER_EXTRA_KEY, $whitelist); + + $this->jsonManipulatorMock->shouldReceive('addMainKey') + ->once() + ->with('scripts', ['post-autoload-dump' => ['Foo\\Bar']]); - $composerRootJsonData['scripts']['auto-scripts'] = $script; + $composerRootJsonData['scripts']['auto-scripts'] = $whitelist; $this->jsonManipulatorMock->shouldReceive('getContents') ->andReturn(ComposerJsonFactory::arrayToJson($composerRootJsonData)); - $this->configurator->configure($packageMock); + $this->ioMock->shouldReceive('askConfirmation') + ->never(); + + $this->configurator->configure($package); \unlink($composerJsonPath); } - public function testUnconfigure(): void + public function testConfigureWithBlacklist(): void { - $composerRootJsonString = ComposerJsonFactory::createComposerScriptJson('unconfigure', ['auto-scripts' => ['php -v' => 'script', 'list' => 'cerebro-cmd']]); + $blackList = ['composer-script-blacklist' => ['stub/stub']]; + + $composerRootJsonString = ComposerJsonFactory::createAutomaticComposerJson('stub/stub', [], [], $blackList); $composerRootJsonData = ComposerJsonFactory::jsonToArray($composerRootJsonString); - $packageMock = $this->mock(PackageContract::class); - $packageMock->shouldReceive('getConfig') + $package = new Package('Stub/stub', '1.0.0'); + $package->setConfig([ + ConfiguratorContract::TYPE => [ + ComposerScriptsConfigurator::getName() => [ + 'post-autoload-dump' => ['Foo\\Bar'], + ], + ], + ]); + + $this->jsonMock->shouldReceive('read') + ->andReturn($composerRootJsonData); + $this->ioMock->shouldReceive('write') ->once() - ->with(ComposerScriptsConfigurator::getName()) - ->andReturn(['php -v' => 'script']); + ->with('Composer scripts for [Stub/stub] skipped, because it was found in the [composer-script-blacklist]'); + + $this->configurator->configure($package); + } + + public function testConfigureNotAllowedScripts(): void + { + $composerRootJsonString = ComposerJsonFactory::createAutomaticComposerJson('stub/stub'); + $composerRootJsonData = ComposerJsonFactory::jsonToArray($composerRootJsonString); + + $package = new Package('Stub/stub', '1.0.0'); + $package->setConfig([ + ConfiguratorContract::TYPE => [ + ComposerScriptsConfigurator::getName() => [ + 'post-autoload-dump' => ['Foo\\Bar'], + 'notallowed' => 'foo', + ], + ], + ]); $this->jsonMock->shouldReceive('read') ->andReturn($composerRootJsonData); $composerJsonPath = __DIR__ . '/composer.json'; + \file_put_contents($composerJsonPath, \json_encode(['extra' => []])); $this->jsonMock->shouldReceive('getPath') ->once() ->andReturn($composerJsonPath); + $whitelist = ['composer-script-whitelist' => [$package->getName()]]; + $this->jsonManipulatorMock->shouldReceive('addSubNode') ->once() - ->with('scripts', 'auto-scripts', ['list' => 'cerebro-cmd']); + ->with('extra', Util::COMPOSER_EXTRA_KEY, $whitelist); + + $this->jsonManipulatorMock->shouldReceive('addMainKey') + ->once() + ->with('scripts', ['post-autoload-dump' => ['Foo\\Bar']]); + + $composerRootJsonData['scripts']['auto-scripts'] = $whitelist; + + $this->jsonManipulatorMock->shouldReceive('getContents') + ->andReturn(ComposerJsonFactory::arrayToJson($composerRootJsonData)); + + $this->ioMock->shouldReceive('askConfirmation') + ->once() + ->with(QuestionFactory::getPackageScriptsQuestion($package->getPrettyName()), false) + ->andReturn(true); + + $this->ioMock->shouldReceive('write') + ->once() + ->with(\sprintf( + '<warning> Found not allowed composer events [notallowed] in [%s]</>' . \PHP_EOL, + $package->getName() + )) + ->andReturn(true); + + $this->configurator->configure($package); + + \unlink($composerJsonPath); + } + + public function testConfigureWithoutScripts(): void + { + $package = new Package('Stub/stub', '1.0.0'); + + $this->ioMock->shouldReceive('askConfirmation') + ->never(); + + $this->configurator->configure($package); + } + + public function testUnconfigure(): void + { + $package = new Package('Stub/stub', '1.0.0'); + $package->setConfig([ + ConfiguratorContract::TYPE => [ + ComposerScriptsConfigurator::getName() => [ + 'post-autoload-dump' => ['Foo\\Bar'], + 'notallowed' => 'foo', + ], + ], + ]); + + $whitelist = ['composer-script-whitelist' => [$package->getName()]]; + + $composerRootJsonString = ComposerJsonFactory::createAutomaticComposerJson('stub/stub', [], [], $whitelist); + $composerRootJsonData = ComposerJsonFactory::jsonToArray($composerRootJsonString); + + $this->jsonMock->shouldReceive('read') + ->andReturn($composerRootJsonData); + + $composerJsonPath = __DIR__ . '/composer.json'; + + $this->jsonMock->shouldReceive('getPath') + ->once() + ->andReturn($composerJsonPath); + + $this->jsonManipulatorMock->shouldReceive('addSubNode') + ->once() + ->with('extra', Util::COMPOSER_EXTRA_KEY, ['composer-script-whitelist' => []]); + + $this->jsonManipulatorMock->shouldReceive('addMainKey') + ->once() + ->with('scripts', []); $this->jsonManipulatorMock->shouldReceive('getContents') ->andReturn(ComposerJsonFactory::arrayToJson($composerRootJsonData)); - $this->configurator->unconfigure($packageMock); + $this->configurator->unconfigure($package); \unlink($composerJsonPath); } + + /** + * {@inheritdoc} + */ + protected function assertPreConditions(): void + { + parent::assertPreConditions(); + + $this->allowMockingNonExistentMethods(true); + } } diff --git a/tests/Automatic/Configurator/CopyFromPackageConfiguratorTest.php b/tests/Automatic/Configurator/CopyFromPackageConfiguratorTest.php index 7d32529a..759d93c5 100644 --- a/tests/Automatic/Configurator/CopyFromPackageConfiguratorTest.php +++ b/tests/Automatic/Configurator/CopyFromPackageConfiguratorTest.php @@ -57,10 +57,10 @@ public function testCopyFileFromPackage(): void $this->ioMock->shouldReceive('writeError') ->once() - ->with([' · Copying files'], true, IOInterface::VERBOSE); + ->with([' - Copying files'], true, IOInterface::VERBOSE); $this->ioMock->shouldReceive('writeError') ->once() - ->with([' · Created <fg=green>"copy_of_copy.txt"</>'], true, IOInterface::VERBOSE); + ->with([' - Created <fg=green>"copy_of_copy.txt"</>'], true, IOInterface::VERBOSE); $this->configurator->configure($package); @@ -79,10 +79,10 @@ public function testCopyDirWithFileFromPackage(): void $this->ioMock->shouldReceive('writeError') ->once() - ->with([' · Copying files'], true, IOInterface::VERBOSE); + ->with([' - Copying files'], true, IOInterface::VERBOSE); $this->ioMock->shouldReceive('writeError') ->once() - ->with([' · Created <fg=green>"/css/style.css"</>'], true, IOInterface::VERBOSE); + ->with([' - Created <fg=green>"/css/style.css"</>'], true, IOInterface::VERBOSE); $this->configurator->configure($package); @@ -104,10 +104,10 @@ public function testTryCopyAFileThatIsNotFoundFromPackage(): void $this->ioMock->shouldReceive('writeError') ->once() - ->with([' · Copying files'], true, IOInterface::VERBOSE); + ->with([' - Copying files'], true, IOInterface::VERBOSE); $this->ioMock->shouldReceive('writeError') ->once() - ->with([' · <fg=red>Failed to create "notfound.txt"</>; Error message: Failed to copy "' . __DIR__ . '/Stub/stub/notfound.txt" because file does not exist.'], true, IOInterface::VERBOSE); + ->with([' - <fg=red>Failed to create "notfound.txt"</>; Error message: Failed to copy "' . __DIR__ . '/Stub/stub/notfound.txt" because file does not exist.'], true, IOInterface::VERBOSE); $this->configurator->configure($package); @@ -124,19 +124,19 @@ public function testUnconfigureAFileFromPackage(): void $this->ioMock->shouldReceive('writeError') ->once() - ->with([' · Copying files'], true, IOInterface::VERBOSE); + ->with([' - Copying files'], true, IOInterface::VERBOSE); $this->ioMock->shouldReceive('writeError') ->once() - ->with([' · Created <fg=green>"copy_of_copy.txt"</>'], true, IOInterface::VERBOSE); + ->with([' - Created <fg=green>"copy_of_copy.txt"</>'], true, IOInterface::VERBOSE); $this->configurator->configure($package); $this->ioMock->shouldReceive('writeError') ->once() - ->with([' · Removing files'], true, IOInterface::VERBOSE); + ->with([' - Removing files'], true, IOInterface::VERBOSE); $this->ioMock->shouldReceive('writeError') ->once() - ->with([' · Removed <fg=green>"copy_of_copy.txt"</>'], true, IOInterface::VERBOSE); + ->with([' - Removed <fg=green>"copy_of_copy.txt"</>'], true, IOInterface::VERBOSE); $this->configurator->unconfigure($package); } @@ -149,19 +149,19 @@ public function testUnconfigureADirWithFileFromPackage(): void $this->ioMock->shouldReceive('writeError') ->once() - ->with([' · Copying files'], true, IOInterface::VERBOSE); + ->with([' - Copying files'], true, IOInterface::VERBOSE); $this->ioMock->shouldReceive('writeError') ->once() - ->with([' · Created <fg=green>"/css/style.css"</>'], true, IOInterface::VERBOSE); + ->with([' - Created <fg=green>"/css/style.css"</>'], true, IOInterface::VERBOSE); $this->configurator->configure($package); $this->ioMock->shouldReceive('writeError') ->once() - ->with([' · Removing files'], true, IOInterface::VERBOSE); + ->with([' - Removing files'], true, IOInterface::VERBOSE); $this->ioMock->shouldReceive('writeError') ->once() - ->with([' · Removed <fg=green>"/css/style.css"</>'], true, IOInterface::VERBOSE); + ->with([' - Removed <fg=green>"/css/style.css"</>'], true, IOInterface::VERBOSE); $this->configurator->unconfigure($package); @@ -180,10 +180,10 @@ public function testUnconfigureWithAIOException(): void $this->ioMock->shouldReceive('writeError') ->once() - ->with([' · Copying files'], true, IOInterface::VERBOSE); + ->with([' - Copying files'], true, IOInterface::VERBOSE); $this->ioMock->shouldReceive('writeError') ->once() - ->with([' · Created <fg=green>"/css/style.css"</>'], true, IOInterface::VERBOSE); + ->with([' - Created <fg=green>"/css/style.css"</>'], true, IOInterface::VERBOSE); $this->configurator->configure($package); @@ -197,10 +197,10 @@ public function testUnconfigureWithAIOException(): void $this->ioMock->shouldReceive('writeError') ->once() - ->with([' · Removing files'], true, IOInterface::VERBOSE); + ->with([' - Removing files'], true, IOInterface::VERBOSE); $this->ioMock->shouldReceive('writeError') ->once() - ->with([' · <fg=red>Failed to remove "/css/style.css"</>; Error message: '], true, IOInterface::VERBOSE); + ->with([' - <fg=red>Failed to remove "/css/style.css"</>; Error message: '], true, IOInterface::VERBOSE); $this->configurator->unconfigure($package); @@ -221,10 +221,10 @@ public function testCopyFileFromPackageWithConfig(): void $this->ioMock->shouldReceive('writeError') ->once() - ->with([' · Copying files'], true, IOInterface::VERBOSE); + ->with([' - Copying files'], true, IOInterface::VERBOSE); $this->ioMock->shouldReceive('writeError') ->once() - ->with([' · Created <fg=green>"test/copy_of_copy.txt"</>'], true, IOInterface::VERBOSE); + ->with([' - Created <fg=green>"test/copy_of_copy.txt"</>'], true, IOInterface::VERBOSE); $this->configurator->configure($package); @@ -262,7 +262,7 @@ private function arrangePackageWithConfig(string $from, string $to): Package ->andReturn(__DIR__); $package = new Package('Stub/stub', '1.0.0'); - $package->setConfig([ConfiguratorContract::TYPE => ['copy' => [$from => $to]]]); + $package->setConfig([ConfiguratorContract::TYPE => [CopyFromPackageConfigurator::getName() => [$from => $to]]]); return $package; } diff --git a/tests/Automatic/Configurator/EnvConfiguratorTest.php b/tests/Automatic/Configurator/EnvConfiguratorTest.php index 7bb5def2..d320ce14 100644 --- a/tests/Automatic/Configurator/EnvConfiguratorTest.php +++ b/tests/Automatic/Configurator/EnvConfiguratorTest.php @@ -78,7 +78,7 @@ public function testConfigure(): void $package = new Package('fixtures/test', '1.0.0'); $package->setConfig([ ConfiguratorContract::TYPE => [ - 'env' => [ + EnvConfigurator::getName() => [ 'APP_ENV' => 'test bar', 'APP_DEBUG' => '0', 'APP_PARAGRAPH' => "foo\n\"bar\"\\t", @@ -130,7 +130,7 @@ public function testUnconfigure(): void ]; $package = new Package('fixtures/env2', '1.0.0'); - $package->setConfig([ConfiguratorContract::TYPE => ['env' => $envConfig]]); + $package->setConfig([ConfiguratorContract::TYPE => [EnvConfigurator::getName() => $envConfig]]); $this->configurator->configure($package); @@ -149,7 +149,7 @@ public function testUnconfigure(): void static::assertStringEqualsFile($this->envPath, $envContents); $package = new Package('fixtures/env2', '1.1.0'); - $package->setConfig([ConfiguratorContract::TYPE => ['env' => $envConfig]]); + $package->setConfig([ConfiguratorContract::TYPE => [EnvConfigurator::getName() => $envConfig]]); $this->configurator->unconfigure($package); diff --git a/tests/Automatic/Configurator/GitignoreConfiguratorTest.php b/tests/Automatic/Configurator/GitignoreConfiguratorTest.php index 3878d312..0d8732b6 100644 --- a/tests/Automatic/Configurator/GitignoreConfiguratorTest.php +++ b/tests/Automatic/Configurator/GitignoreConfiguratorTest.php @@ -150,7 +150,7 @@ public function testUnconfigureWithNotFoundPackage(): void private function arrangePackageWithConfig(string $name, array $config): Package { $package = new Package($name, '1.0.0'); - $package->setConfig([ConfiguratorContract::TYPE => ['gitignore' => $config]]); + $package->setConfig([ConfiguratorContract::TYPE => [GitIgnoreConfigurator::getName() => $config]]); return $package; } diff --git a/tests/Automatic/ConfiguratorTest.php b/tests/Automatic/ConfiguratorTest.php index e148ba5d..355861f7 100644 --- a/tests/Automatic/ConfiguratorTest.php +++ b/tests/Automatic/ConfiguratorTest.php @@ -41,11 +41,11 @@ public function testConfigureWithCopy(): void $this->ioMock->shouldReceive('writeError') ->once() - ->with([' · Copying files'], true, IOInterface::VERBOSE); + ->with([' - Copying files'], true, IOInterface::VERBOSE); $this->ioMock->shouldReceive('writeError') ->once() - ->with([' · Created <fg=green>"' . $this->copyFileName . '"</>'], true, IOInterface::VERBOSE); + ->with([' - Created <fg=green>"' . $this->copyFileName . '"</>'], true, IOInterface::VERBOSE); $this->configurator->configure($package); @@ -56,7 +56,7 @@ public function testConfigureWithCopy(): void public function testGetConfigurators(): void { - static::assertCount(4, $this->configurator->getConfigurators()); + static::assertCount(5, $this->configurator->getConfigurators()); } public function testUnconfigureWithCopy(): void @@ -67,11 +67,11 @@ public function testUnconfigureWithCopy(): void $this->ioMock->shouldReceive('writeError') ->once() - ->with([' · Copying files'], true, IOInterface::VERBOSE); + ->with([' - Copying files'], true, IOInterface::VERBOSE); $this->ioMock->shouldReceive('writeError') ->once() - ->with([' · Created <fg=green>"' . $this->copyFileName . '"</>'], true, IOInterface::VERBOSE); + ->with([' - Created <fg=green>"' . $this->copyFileName . '"</>'], true, IOInterface::VERBOSE); $this->configurator->configure($package); @@ -79,11 +79,11 @@ public function testUnconfigureWithCopy(): void $this->ioMock->shouldReceive('writeError') ->once() - ->with([' · Removing files'], true, IOInterface::VERBOSE); + ->with([' - Removing files'], true, IOInterface::VERBOSE); $this->ioMock->shouldReceive('writeError') ->once() - ->with([' · Removed <fg=green>"' . $this->copyFileName . '"</>'], true, IOInterface::VERBOSE); + ->with([' - Removed <fg=green>"' . $this->copyFileName . '"</>'], true, IOInterface::VERBOSE); $this->configurator->unconfigure($package); diff --git a/tests/Automatic/PackageConfiguratorTest.php b/tests/Automatic/PackageConfiguratorTest.php index 94c1f952..6ebb483a 100644 --- a/tests/Automatic/PackageConfiguratorTest.php +++ b/tests/Automatic/PackageConfiguratorTest.php @@ -28,7 +28,7 @@ public function testConfiguratorWithPackageConfigurator(): void $this->ioMock->shouldReceive('writeError') ->twice() - ->with([' · test'], true, IOInterface::VERBOSE); + ->with([' - test'], true, IOInterface::VERBOSE); $this->configurator->add('mock', MockConfigurator::class); @@ -51,7 +51,7 @@ public function testConfiguratorOutWithPackageConfigurator(): void $this->ioMock->shouldReceive('writeError') ->never() - ->with([' · test'], true, IOInterface::VERBOSE); + ->with([' - test'], true, IOInterface::VERBOSE); $this->configurator->configure($package); $this->configurator->unconfigure($package);