diff --git a/phpunit.xml b/phpunit.xml index e55ef1c1..6f7b5b5c 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -9,8 +9,8 @@ processIsolation="false" stopOnFailure="false"> - - ./tests/replacers/ + + ./tests/ \ No newline at end of file diff --git a/src/Console/Commands/Compose.php b/src/Console/Commands/Compose.php index 98a570b7..436cfb96 100644 --- a/src/Console/Commands/Compose.php +++ b/src/Console/Commands/Compose.php @@ -37,8 +37,26 @@ protected function execute(InputInterface $input, OutputInterface $output) $workingDir = getcwd(); $this->workingDir = $workingDir; - $config = json_decode(file_get_contents($workingDir . '/composer.json')); - $config = $config->extra->mozart; + $composerFile = $workingDir . '/composer.json'; + if (!file_exists($composerFile)) { + $output->write('No composer.json found at current directory: ' . $workingDir); + return 1; + } + + $composer = json_decode(file_get_contents($composerFile)); + // If the json was malformed. + if (!is_object($composer)) { + $output->write('Unable to parse composer.json read at: ' . $workingDir); + return 1; + } + + // if `extra` is missing or not an object or if it does not have a `mozart` key which is an object. + if (!isset($composer->extra) || !is_object($composer->extra) + || !isset($composer->extra->mozart) || !is_object($composer->extra->mozart)) { + $output->write('Mozart config not readable in composer.json at extra->mozart'); + return 1; + } + $config = $composer->extra->mozart; $config->dep_namespace = preg_replace("/\\\{2,}$/", "\\", "$config->dep_namespace\\"); @@ -47,7 +65,12 @@ protected function execute(InputInterface $input, OutputInterface $output) $this->mover = new Mover($workingDir, $config); $this->replacer = new Replacer($workingDir, $config); - $require = empty($config->packages) ? array_keys(get_object_vars($this->config->require)) : $config->packages; + $require = array(); + if (isset($config->packages) && is_array($config->packages)) { + $require = $config->packages; + } elseif (isset($composer->require) && is_object($composer->require)) { + $require = array_keys(get_object_vars($composer->require)); + } $packages = $this->findPackages($require); diff --git a/tests/Console/Commands/ComposeTest.php b/tests/Console/Commands/ComposeTest.php new file mode 100644 index 00000000..0203fae4 --- /dev/null +++ b/tests/Console/Commands/ComposeTest.php @@ -0,0 +1,225 @@ +createMock(InputInterface::class); + $outputInterfaceMock = $this->createMock(OutputInterface::class); + + $outputInterfaceMock->expects($this->exactly(1)) + ->method('write'); + + $compose = new class( $inputInterfaceMock, $outputInterfaceMock ) extends Compose { + public function __construct($inputInterfaceMock, $outputInterfaceMock) + { + parent::__construct(); + + $this->execute($inputInterfaceMock, $outputInterfaceMock); + } + }; + } + + /** + * When json_decode fails, instead of + * "Trying to get property 'extra' of non-object" + * a better message should be written to the OutputInterface. + * + * @test + */ + public function it_handles_malformed_json_with_grace(): void + { + + $badComposerJson = '{ "name": "coenjacobs/mozart", }'; + + file_put_contents(__DIR__ . '/composer.json', $badComposerJson); + + $inputInterfaceMock = $this->createMock(InputInterface::class); + $outputInterfaceMock = $this->createMock(OutputInterface::class); + + $outputInterfaceMock->expects($this->exactly(1)) + ->method('write'); + + $compose = new class( $inputInterfaceMock, $outputInterfaceMock ) extends Compose { + public function __construct($inputInterfaceMock, $outputInterfaceMock) + { + parent::__construct(); + + $this->execute($inputInterfaceMock, $outputInterfaceMock); + } + }; + } + + /** + * When composer.json->extra is absent, instead of + * "Undefined property: stdClass::$extra" + * a better message should be written to the OutputInterface. + * + * @test + */ + public function it_handles_absent_extra_config_with_grace(): void + { + + $badComposerJson = '{ "name": "coenjacobs/mozart" }'; + + file_put_contents(__DIR__ . '/composer.json', $badComposerJson); + + $inputInterfaceMock = $this->createMock(InputInterface::class); + $outputInterfaceMock = $this->createMock(OutputInterface::class); + + $outputInterfaceMock->expects($this->exactly(1)) + ->method('write'); + + $compose = new class( $inputInterfaceMock, $outputInterfaceMock ) extends Compose { + public function __construct($inputInterfaceMock, $outputInterfaceMock) + { + parent::__construct(); + + $this->execute($inputInterfaceMock, $outputInterfaceMock); + } + }; + } + + + /** + * When composer.json->extra is not an object, instead of + * "Trying to get property 'mozart' of non-object" + * a better message should be written to the OutputInterface. + * + * @test + */ + public function it_handles_malformed_extra_config_with_grace(): void + { + + $badComposerJson = '{ "name": "coenjacobs/mozart", "extra": [] }'; + + file_put_contents(__DIR__ . '/composer.json', $badComposerJson); + + $inputInterfaceMock = $this->createMock(InputInterface::class); + $outputInterfaceMock = $this->createMock(OutputInterface::class); + + $outputInterfaceMock->expects($this->exactly(1)) + ->method('write'); + + $compose = new class( $inputInterfaceMock, $outputInterfaceMock ) extends Compose { + public function __construct($inputInterfaceMock, $outputInterfaceMock) + { + parent::__construct(); + + $this->execute($inputInterfaceMock, $outputInterfaceMock); + } + }; + } + + /** + * When composer.json->extra->mozart is absent, instead of + * "Undefined property: stdClass::$mozart" + * a better message should be written to the OutputInterface. + * + * @test + */ + public function it_handles_absent_mozart_config_with_grace(): void + { + + $badComposerJson = '{ "name": "coenjacobs/mozart", "extra": { "moozart": {} } }'; + + file_put_contents(__DIR__ . '/composer.json', $badComposerJson); + + $inputInterfaceMock = $this->createMock(InputInterface::class); + $outputInterfaceMock = $this->createMock(OutputInterface::class); + + $outputInterfaceMock->expects($this->exactly(1)) + ->method('write'); + + $compose = new class( $inputInterfaceMock, $outputInterfaceMock ) extends Compose { + public function __construct($inputInterfaceMock, $outputInterfaceMock) + { + parent::__construct(); + + $this->execute($inputInterfaceMock, $outputInterfaceMock); + } + }; + } + + /** + * When composer.json->extra->mozart is malformed, instead of + * "Undefined property: stdClass::$mozart" + * a better message should be written to the OutputInterface. + * + * is_object() added. + * + * @test + */ + public function it_handles_malformed_mozart_config__with_grace(): void + { + + $badComposerJson = '{ "name": "coenjacobs/mozart", "extra": { "mozart": [] } }'; + + file_put_contents(__DIR__ . '/composer.json', $badComposerJson); + + $inputInterfaceMock = $this->createMock(InputInterface::class); + $outputInterfaceMock = $this->createMock(OutputInterface::class); + + $outputInterfaceMock->expects($this->exactly(1)) + ->method('write'); + + $compose = new class( $inputInterfaceMock, $outputInterfaceMock ) extends Compose { + public function __construct($inputInterfaceMock, $outputInterfaceMock) + { + parent::__construct(); + + $this->execute($inputInterfaceMock, $outputInterfaceMock); + } + }; + } + + public function tearDown(): void + { + parent::tearDown(); + + $composer_json = __DIR__ . '/composer.json'; + if (file_exists($composer_json)) { + unlink($composer_json); + } + } + + public static function tearDownAfterClass(): void + { + parent::tearDownAfterClass(); + chdir(self::$cwd); + } +}