diff --git a/config/config.php b/config/config.php index 0ca3f26..1a048c9 100644 --- a/config/config.php +++ b/config/config.php @@ -23,6 +23,7 @@ Console\Helper\ProcessHelper::class => Factory\ProcessHelperFactory::class, Service\InstallationCommandsRunner::class => ConfigAbstractFactory::class, + Service\InstallationRunner::class => ConfigAbstractFactory::class, Service\ShlinkAssetsHandler::class => ConfigAbstractFactory::class, Config\ConfigGenerator::class => ConfigAbstractFactory::class, Config\ConfigOptionsManager::class => Config\ConfigOptionsManagerFactory::class, @@ -209,17 +210,14 @@ PhpExecutableFinder::class, 'config.installer.installation_commands', ], - - Command\InstallCommand::class => [ - ConfigWriter::class, - Service\ShlinkAssetsHandler::class, - Config\ConfigGenerator::class, - ], - Command\UpdateCommand::class => [ + Service\InstallationRunner::class => [ ConfigWriter::class, Service\ShlinkAssetsHandler::class, Config\ConfigGenerator::class, ], + + Command\InstallCommand::class => [Service\InstallationRunner::class], + Command\UpdateCommand::class => [Service\InstallationRunner::class], Command\SetOptionCommand::class => [ ConfigWriter::class, Service\ShlinkAssetsHandler::class, diff --git a/src/Command/AbstractInstallCommand.php b/src/Command/AbstractInstallCommand.php deleted file mode 100644 index 6d3c753..0000000 --- a/src/Command/AbstractInstallCommand.php +++ /dev/null @@ -1,92 +0,0 @@ -text([ - 'Welcome to Shlink!!', - 'This tool will guide you through the installation process.', - ]); - - // Check if a cached config file exists and drop it if so - $this->assetsHandler->dropCachedConfigIfAny($io); - - $importedConfig = $this->resolvePreviousConfig($io); - if ($this->isUpdate()) { - $this->assetsHandler->importShlinkAssetsFromPath($io, $importedConfig->importPath); - } - $config = $this->configGenerator->generateConfigInteractively($io, $importedConfig->importedConfig); - $normalizedConfig = Utils::normalizeAndKeepEnvVarKeys($config); - - // Generate config params files - $this->configWriter->toFile(ShlinkAssetsHandler::GENERATED_CONFIG_PATH, $normalizedConfig); - $io->text('Custom configuration properly generated!'); - $io->newLine(); - - if (! $this->execInitCommand($io, $importedConfig)) { - return -1; - } - - $io->success('Installation complete!'); - return 0; - } - - private function resolvePreviousConfig(SymfonyStyle $io): ImportedConfig - { - if ($this->isUpdate()) { - return $this->assetsHandler->resolvePreviousConfig($io); - } - - return ImportedConfig::notImported(); - } - - private function execInitCommand(SymfonyStyle $io, ImportedConfig $importedConfig): bool - { - $isUpdate = $this->isUpdate(); - $input = [ - InitOption::SKIP_INITIALIZE_DB->asCliFlag() => $isUpdate, - InitOption::CLEAR_DB_CACHE->asCliFlag() => $isUpdate, - InitOption::DOWNLOAD_RR_BINARY->asCliFlag() => - $isUpdate && $this->assetsHandler->roadRunnerBinaryExistsInPath($importedConfig->importPath), - ]; - - if (! $isUpdate) { - $input[InitOption::INITIAL_API_KEY->asCliFlag()] = null; - } - - $command = $this->getApplication()?->find(InitCommand::NAME); - $exitCode = $command?->run(new ArrayInput($input), $io); - - return $exitCode === 0; - } - - abstract protected function isUpdate(): bool; -} diff --git a/src/Command/InstallCommand.php b/src/Command/InstallCommand.php index 0abc537..6d1edac 100644 --- a/src/Command/InstallCommand.php +++ b/src/Command/InstallCommand.php @@ -4,19 +4,24 @@ namespace Shlinkio\Shlink\Installer\Command; -class InstallCommand extends AbstractInstallCommand +use Shlinkio\Shlink\Installer\Service\InstallationRunnerInterface; +use Symfony\Component\Console\Attribute\AsCommand; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Style\SymfonyStyle; + +#[AsCommand(InstallCommand::NAME, 'Guides you through the installation process, to get Shlink up and running')] +class InstallCommand extends Command { public const string NAME = 'install'; - protected function configure(): void + public function __construct(private readonly InstallationRunnerInterface $installationRunner) { - $this - ->setName(self::NAME) - ->setDescription('Guides you through the installation process, to get Shlink up and running.'); + parent::__construct(); } - protected function isUpdate(): bool + public function __invoke(SymfonyStyle $io): int { - return false; + $initCommand = $this->getApplication()?->find(InitCommand::NAME); + return $this->installationRunner->runInstallation($io, $initCommand); } } diff --git a/src/Command/UpdateCommand.php b/src/Command/UpdateCommand.php index 5aedb54..72cf882 100644 --- a/src/Command/UpdateCommand.php +++ b/src/Command/UpdateCommand.php @@ -4,19 +4,24 @@ namespace Shlinkio\Shlink\Installer\Command; -class UpdateCommand extends AbstractInstallCommand +use Shlinkio\Shlink\Installer\Service\InstallationRunnerInterface; +use Symfony\Component\Console\Attribute\AsCommand; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Style\SymfonyStyle; + +#[AsCommand(UpdateCommand::NAME, 'Helps you import Shlink\'s config from an older version to a new one')] +class UpdateCommand extends Command { public const string NAME = 'update'; - protected function configure(): void + public function __construct(private readonly InstallationRunnerInterface $installationRunner) { - $this - ->setName(self::NAME) - ->setDescription('Helps you import Shlink\'s config from an older version to a new one.'); + parent::__construct(); } - protected function isUpdate(): bool + public function __invoke(SymfonyStyle $io): int { - return true; + $initCommand = $this->getApplication()?->find(InitCommand::NAME); + return $this->installationRunner->runUpdate($io, $initCommand); } } diff --git a/src/Model/ImportedConfig.php b/src/Model/ImportedConfig.php index 5f33e1c..ebb1685 100644 --- a/src/Model/ImportedConfig.php +++ b/src/Model/ImportedConfig.php @@ -4,9 +4,9 @@ namespace Shlinkio\Shlink\Installer\Model; -final class ImportedConfig +final readonly class ImportedConfig { - private function __construct(public readonly string $importPath, public readonly array $importedConfig) + private function __construct(public string $importPath, public array $importedConfig) { } diff --git a/src/Service/InstallationRunner.php b/src/Service/InstallationRunner.php new file mode 100644 index 0000000..db28b92 --- /dev/null +++ b/src/Service/InstallationRunner.php @@ -0,0 +1,83 @@ +asCliFlag() => null]; + return $this->run($io, $initCommand, $initCommandInput, ImportedConfig::notImported()); + } + + /** @inheritDoc */ + public function runUpdate(SymfonyStyle $io, Command|null $initCommand): int + { + $importConfig = $this->assetsHandler->resolvePreviousConfig($io); + + // Check if a cached config file exists and drop it if so + $this->assetsHandler->dropCachedConfigIfAny($io); + $this->assetsHandler->importShlinkAssetsFromPath($io, $importConfig->importPath); + + $initCommandInput = [ + InitOption::SKIP_INITIALIZE_DB->asCliFlag() => null, + InitOption::CLEAR_DB_CACHE->asCliFlag() => null, + ]; + + if ($this->assetsHandler->roadRunnerBinaryExistsInPath($importConfig->importPath)) { + $initCommandInput[InitOption::DOWNLOAD_RR_BINARY->asCliFlag()] = null; + } + + return $this->run($io, $initCommand, $initCommandInput, $importConfig); + } + + /** + * @return Command::SUCCESS|Command::FAILURE + */ + private function run( + SymfonyStyle $io, + Command|null $initCommand, + array $initCommandInput, + ImportedConfig $importedConfig, + ): int { + $io->text([ + 'Welcome to Shlink!!', + 'This tool will guide you through the installation process.', + ]); + + $config = $this->configGenerator->generateConfigInteractively($io, $importedConfig->importedConfig); + $normalizedConfig = Utils::normalizeAndKeepEnvVarKeys($config); + + // Generate config params files + $this->configWriter->toFile(ShlinkAssetsHandler::GENERATED_CONFIG_PATH, $normalizedConfig); + $io->text('Custom configuration properly generated!'); + $io->newLine(); + + $initCommandResult = $initCommand?->run(new ArrayInput($initCommandInput), $io); + if ($initCommandResult !== Command::SUCCESS) { + return Command::FAILURE; + } + + $io->success('Installation complete!'); + return Command::SUCCESS; + } +} diff --git a/src/Service/InstallationRunnerInterface.php b/src/Service/InstallationRunnerInterface.php new file mode 100644 index 0000000..12ba159 --- /dev/null +++ b/src/Service/InstallationRunnerInterface.php @@ -0,0 +1,21 @@ +assetsHandler = $this->createMock(ShlinkAssetsHandlerInterface::class); - $this->assetsHandler->expects($this->once())->method('dropCachedConfigIfAny'); - - $this->configWriter = $this->createMock(ConfigWriterInterface::class); - - $configGenerator = $this->createMock(ConfigGeneratorInterface::class); - $configGenerator->method('generateConfigInteractively')->willReturn([]); - - $app = new Application(); - $command = new InstallCommand( - $this->configWriter, - $this->assetsHandler, - $configGenerator, - ); - $app->addCommand($command); - - $this->initCommand = $this->createMock(Command::class); - $this->initCommand->method('getName')->willReturn(InitCommand::NAME); - $this->initCommand->method('isEnabled')->willReturn(true); - $app->addCommand($this->initCommand); + $this->installationRunner = $this->createMock(InstallationRunnerInterface::class); + $command = new InstallCommand($this->installationRunner); $this->commandTester = new CommandTester($command); } #[Test] - public function commandIsExecutedAsExpected(): void + #[TestWith([Command::SUCCESS])] + #[TestWith([Command::FAILURE])] + public function commandIsExecutedAsExpected(int $statusCode): void { - $this->initCommand->expects($this->once())->method('run')->with( - $this->callback(function (ArrayInput $input) { - Assert::assertEquals( - '--skip-initialize-db --clear-db-cache --download-rr-binary --initial-api-key', - $input->__toString(), - ); - return true; - }), - $this->anything(), - )->willReturn(0); - $this->assetsHandler->expects($this->never())->method('resolvePreviousConfig'); - $this->assetsHandler->expects($this->never())->method('importShlinkAssetsFromPath'); - $this->configWriter->expects($this->once())->method('toFile')->with($this->anything(), $this->isArray()); - - $this->commandTester->setInputs(['no']); + $this->installationRunner->expects($this->once())->method('runInstallation')->willReturn($statusCode); $this->commandTester->execute([]); + + self::assertEquals($statusCode, $this->commandTester->getStatusCode()); } } diff --git a/test/Command/UpdateCommandTest.php b/test/Command/UpdateCommandTest.php index cc10921..00ce845 100644 --- a/test/Command/UpdateCommandTest.php +++ b/test/Command/UpdateCommandTest.php @@ -4,84 +4,36 @@ namespace ShlinkioTest\Shlink\Installer\Command; -use PHPUnit\Framework\Assert; -use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Test; +use PHPUnit\Framework\Attributes\TestWith; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; -use Shlinkio\Shlink\Installer\Command\InitCommand; use Shlinkio\Shlink\Installer\Command\UpdateCommand; -use Shlinkio\Shlink\Installer\Config\ConfigGeneratorInterface; -use Shlinkio\Shlink\Installer\Model\ImportedConfig; -use Shlinkio\Shlink\Installer\Service\ShlinkAssetsHandlerInterface; -use Shlinkio\Shlink\Installer\Util\ConfigWriterInterface; -use Symfony\Component\Console\Application; +use Shlinkio\Shlink\Installer\Service\InstallationRunnerInterface; use Symfony\Component\Console\Command\Command; -use Symfony\Component\Console\Input\ArrayInput; use Symfony\Component\Console\Tester\CommandTester; class UpdateCommandTest extends TestCase { private CommandTester $commandTester; - private MockObject & ConfigWriterInterface $configWriter; - private MockObject & ShlinkAssetsHandlerInterface $assetsHandler; - private MockObject & Command $initCommand; + private MockObject & InstallationRunnerInterface $installationRunner; public function setUp(): void { - $this->assetsHandler = $this->createMock(ShlinkAssetsHandlerInterface::class); - $this->assetsHandler->expects($this->once())->method('dropCachedConfigIfAny'); - - $this->configWriter = $this->createMock(ConfigWriterInterface::class); - - $generator = $this->createMock(ConfigGeneratorInterface::class); - $generator->method('generateConfigInteractively')->willReturn([]); - - $app = new Application(); - $command = new UpdateCommand($this->configWriter, $this->assetsHandler, $generator); - $app->addCommand($command); - - $this->initCommand = $this->createMock(Command::class); - $this->initCommand->method('getName')->willReturn(InitCommand::NAME); - $this->initCommand->method('isEnabled')->willReturn(true); - $app->addCommand($this->initCommand); + $this->installationRunner = $this->createMock(InstallationRunnerInterface::class); + $command = new UpdateCommand($this->installationRunner); $this->commandTester = new CommandTester($command); } - #[Test, DataProvider('provideCommands')] - public function commandIsExecutedAsExpected(bool $rrBinExists, string $postUpdateCommands): void + #[Test] + #[TestWith([Command::SUCCESS])] + #[TestWith([Command::FAILURE])] + public function commandIsExecutedAsExpected(int $statusCode): void { - $this->initCommand->expects($this->once())->method('run')->with( - $this->callback(function (ArrayInput $input) use ($postUpdateCommands) { - Assert::assertEquals( - $postUpdateCommands, - $input->__toString(), - ); - return true; - }), - $this->anything(), - )->willReturn(0); - $this->assetsHandler->expects($this->once())->method('roadRunnerBinaryExistsInPath')->willReturn($rrBinExists); - $this->assetsHandler->expects($this->once())->method('resolvePreviousConfig')->willReturn( - ImportedConfig::notImported(), - ); - $this->assetsHandler->expects($this->once())->method('importShlinkAssetsFromPath'); - $this->configWriter->expects($this->once())->method('toFile')->with($this->anything(), $this->isArray()); - - $this->commandTester->setInputs(['no']); + $this->installationRunner->expects($this->once())->method('runUpdate')->willReturn($statusCode); $this->commandTester->execute([]); - } - public static function provideCommands(): iterable - { - yield 'no rr binary' => [ - false, - '--skip-initialize-db=1 --clear-db-cache=1 --download-rr-binary', - ]; - yield 'rr binary' => [ - true, - '--skip-initialize-db=1 --clear-db-cache=1 --download-rr-binary=1', - ]; + self::assertEquals($statusCode, $this->commandTester->getStatusCode()); } } diff --git a/test/Service/InstallationRunnerTest.php b/test/Service/InstallationRunnerTest.php new file mode 100644 index 0000000..d7c2654 --- /dev/null +++ b/test/Service/InstallationRunnerTest.php @@ -0,0 +1,95 @@ +initCommand = $this->createMock(Command::class); + $this->assetsHandler = $this->createMock(ShlinkAssetsHandlerInterface::class); + $this->configWriter = $this->createMock(ConfigWriterInterface::class); + + $configGenerator = $this->createStub(ConfigGeneratorInterface::class); + $configGenerator->method('generateConfigInteractively')->willReturn([]); + + $this->installationRunner = new InstallationRunner($this->configWriter, $this->assetsHandler, $configGenerator); + } + + #[Test] + public function installationIsExecutedAsExpected(): void + { + $this->initCommand->expects($this->once())->method('run')->with( + $this->callback(function (ArrayInput $input) { + Assert::assertEquals('--initial-api-key', $input->__toString()); + return true; + }), + $this->anything(), + )->willReturn(Command::SUCCESS); + + $this->assetsHandler->expects($this->never())->method('dropCachedConfigIfAny'); + $this->assetsHandler->expects($this->never())->method('resolvePreviousConfig'); + $this->assetsHandler->expects($this->never())->method('roadRunnerBinaryExistsInPath'); + $this->assetsHandler->expects($this->never())->method('importShlinkAssetsFromPath'); + + $this->configWriter->expects($this->once())->method('toFile')->with($this->anything(), $this->isArray()); + + $this->installationRunner->runInstallation($this->createStub(SymfonyStyle::class), $this->initCommand); + } + + #[Test, DataProvider('provideCommands')] + public function updateIsExecutedAsExpected(bool $rrBinExists, string $postUpdateCommands): void + { + $this->initCommand->expects($this->once())->method('run')->with( + $this->callback(function (ArrayInput $input) use ($postUpdateCommands) { + Assert::assertEquals($postUpdateCommands, $input->__toString()); + return true; + }), + $this->anything(), + )->willReturn(0); + + $this->assetsHandler->expects($this->once())->method('dropCachedConfigIfAny'); + $this->assetsHandler->expects($this->once())->method('resolvePreviousConfig')->willReturn( + ImportedConfig::notImported(), + ); + $this->assetsHandler->expects($this->once())->method('roadRunnerBinaryExistsInPath')->willReturn($rrBinExists); + $this->assetsHandler->expects($this->once())->method('importShlinkAssetsFromPath'); + + $this->configWriter->expects($this->once())->method('toFile')->with($this->anything(), $this->isArray()); + + $this->installationRunner->runUpdate($this->createStub(SymfonyStyle::class), $this->initCommand); + } + + public static function provideCommands(): iterable + { + yield 'update with no rr binary' => [ + false, + '--skip-initialize-db --clear-db-cache', + ]; + yield 'update with rr binary' => [ + true, + '--skip-initialize-db --clear-db-cache --download-rr-binary', + ]; + } +}