Created new Installer module and moved everything from CLI there

This commit is contained in:
Alejandro Celaya
2018-09-29 09:52:32 +02:00
parent d5dc6cea99
commit a65ce649ac
26 changed files with 106 additions and 76 deletions

View File

@@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
return [
'translator' => [
'translation_file_patterns' => [
[
'type' => 'gettext',
'base_dir' => __DIR__ . '/../lang',
'pattern' => '%s.mo',
],
],
],
];

View File

@@ -0,0 +1,255 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\Installer\Command;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Shlinkio\Shlink\Installer\Config\ConfigCustomizerManagerInterface;
use Shlinkio\Shlink\Installer\Config\Plugin;
use Shlinkio\Shlink\Installer\Model\CustomizableAppConfig;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Exception\InvalidArgumentException;
use Symfony\Component\Console\Exception\LogicException;
use Symfony\Component\Console\Exception\RuntimeException;
use Symfony\Component\Console\Helper\ProcessHelper;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\Filesystem\Exception\IOException;
use Symfony\Component\Filesystem\Filesystem;
use Symfony\Component\Process\PhpExecutableFinder;
use Zend\Config\Writer\WriterInterface;
class InstallCommand extends Command
{
public const GENERATED_CONFIG_PATH = 'config/params/generated_config.php';
/**
* @var SymfonyStyle
*/
private $io;
/**
* @var ProcessHelper
*/
private $processHelper;
/**
* @var WriterInterface
*/
private $configWriter;
/**
* @var Filesystem
*/
private $filesystem;
/**
* @var \Shlinkio\Shlink\Installer\Config\ConfigCustomizerManagerInterface
*/
private $configCustomizers;
/**
* @var bool
*/
private $isUpdate;
/**
* @var PhpExecutableFinder
*/
private $phpFinder;
/**
* @var string|bool
*/
private $phpBinary;
/**
* InstallCommand constructor.
* @param WriterInterface $configWriter
* @param Filesystem $filesystem
* @param ConfigCustomizerManagerInterface $configCustomizers
* @param bool $isUpdate
* @param PhpExecutableFinder|null $phpFinder
* @throws LogicException
*/
public function __construct(
WriterInterface $configWriter,
Filesystem $filesystem,
ConfigCustomizerManagerInterface $configCustomizers,
bool $isUpdate = false,
PhpExecutableFinder $phpFinder = null
) {
parent::__construct();
$this->configWriter = $configWriter;
$this->isUpdate = $isUpdate;
$this->filesystem = $filesystem;
$this->configCustomizers = $configCustomizers;
$this->phpFinder = $phpFinder ?: new PhpExecutableFinder();
}
protected function configure(): void
{
$this
->setName('shlink:install')
->setDescription('Installs or updates Shlink');
}
/**
* @param InputInterface $input
* @param OutputInterface $output
* @return void
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
*/
protected function execute(InputInterface $input, OutputInterface $output): void
{
$this->io = new SymfonyStyle($input, $output);
$this->io->writeln([
'<info>Welcome to Shlink!!</info>',
'This tool will guide you through the installation process.',
]);
// Check if a cached config file exists and drop it if so
if ($this->filesystem->exists('data/cache/app_config.php')) {
$this->io->write('Deleting old cached config...');
try {
$this->filesystem->remove('data/cache/app_config.php');
$this->io->writeln(' <info>Success</info>');
} catch (IOException $e) {
$this->io->error(
'Failed! You will have to manually delete the data/cache/app_config.php file to'
. ' get new config applied.'
);
if ($this->io->isVerbose()) {
$this->getApplication()->renderException($e, $output);
}
return;
}
}
// If running update command, ask the user to import previous config
$config = $this->isUpdate ? $this->importConfig() : new CustomizableAppConfig();
// Ask for custom config params
foreach ([
Plugin\DatabaseConfigCustomizer::class,
Plugin\UrlShortenerConfigCustomizer::class,
Plugin\LanguageConfigCustomizer::class,
Plugin\ApplicationConfigCustomizer::class,
] as $pluginName) {
/** @var Plugin\ConfigCustomizerInterface $configCustomizer */
$configCustomizer = $this->configCustomizers->get($pluginName);
$configCustomizer->process($this->io, $config);
}
// Generate config params files
$this->configWriter->toFile(self::GENERATED_CONFIG_PATH, $config->getArrayCopy(), false);
$this->io->writeln(['<info>Custom configuration properly generated!</info>', '']);
// If current command is not update, generate database
if (! $this->isUpdate) {
$this->io->write('Initializing database...');
if (! $this->runPhpCommand(
'vendor/doctrine/orm/bin/doctrine.php orm:schema-tool:create',
'Error generating database.',
$output
)) {
return;
}
}
// Run database migrations
$this->io->write('Updating database...');
if (! $this->runPhpCommand(
'vendor/doctrine/migrations/bin/doctrine-migrations.php migrations:migrate',
'Error updating database.',
$output
)) {
return;
}
// Generate proxies
$this->io->write('Generating proxies...');
if (! $this->runPhpCommand(
'vendor/doctrine/orm/bin/doctrine.php orm:generate-proxies',
'Error generating proxies.',
$output
)) {
return;
}
$this->io->success('Installation complete!');
}
/**
* @return CustomizableAppConfig
* @throws RuntimeException
*/
private function importConfig(): CustomizableAppConfig
{
$config = new CustomizableAppConfig();
// Ask the user if he/she wants to import an older configuration
$importConfig = $this->io->confirm('Do you want to import configuration from previous installation?');
if (! $importConfig) {
return $config;
}
// Ask the user for the older shlink path
$keepAsking = true;
do {
$config->setImportedInstallationPath($this->io->ask(
'Previous shlink installation path from which to import config'
));
$configFile = $config->getImportedInstallationPath() . '/' . self::GENERATED_CONFIG_PATH;
$configExists = $this->filesystem->exists($configFile);
if (! $configExists) {
$keepAsking = $this->io->confirm(
'Provided path does not seem to be a valid shlink root path. Do you want to try another path?'
);
}
} while (! $configExists && $keepAsking);
// If after some retries the user has chosen not to test another path, return
if (! $configExists) {
return $config;
}
// Read the config file
$config->exchangeArray(include $configFile);
return $config;
}
/**
* @param string $command
* @param string $errorMessage
* @param OutputInterface $output
* @return bool
* @throws LogicException
* @throws InvalidArgumentException
*/
private function runPhpCommand($command, $errorMessage, OutputInterface $output): bool
{
if ($this->processHelper === null) {
$this->processHelper = $this->getHelper('process');
}
if ($this->phpBinary === null) {
$this->phpBinary = $this->phpFinder->find(false) ?: 'php';
}
$this->io->write(
' <options=bold>[Running "' . sprintf('%s %s', $this->phpBinary, $command) . '"]</> ',
false,
OutputInterface::VERBOSITY_VERBOSE
);
$process = $this->processHelper->run($output, sprintf('%s %s', $this->phpBinary, $command));
if ($process->isSuccessful()) {
$this->io->writeln(' <info>Success!</info>');
return true;
}
if (! $this->io->isVerbose()) {
$this->io->error($errorMessage . ' Run this command with -vvv to see specific error info.');
}
return false;
}
}

View File

@@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\Installer\Config;
use Shlinkio\Shlink\Installer\Config\Plugin\ConfigCustomizerInterface;
use Zend\ServiceManager\AbstractPluginManager;
class ConfigCustomizerManager extends AbstractPluginManager implements ConfigCustomizerManagerInterface
{
protected $instanceOf = ConfigCustomizerInterface::class;
}

View File

@@ -0,0 +1,10 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\Installer\Config;
use Psr\Container\ContainerInterface;
interface ConfigCustomizerManagerInterface extends ContainerInterface
{
}

View File

@@ -0,0 +1,44 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\Installer\Config\Plugin;
use Shlinkio\Shlink\Common\Util\StringUtilsTrait;
use Shlinkio\Shlink\Installer\Model\CustomizableAppConfig;
use Symfony\Component\Console\Style\SymfonyStyle;
class ApplicationConfigCustomizer implements ConfigCustomizerInterface
{
use StringUtilsTrait;
/**
* @param SymfonyStyle $io
* @param CustomizableAppConfig $appConfig
* @return void
*/
public function process(SymfonyStyle $io, CustomizableAppConfig $appConfig)
{
$io->title('APPLICATION');
if ($appConfig->hasApp() && $io->confirm('Do you want to keep imported application config?')) {
return;
}
$validator = function ($value) {
return $value;
};
$appConfig->setApp([
'SECRET' => $io->ask(
'Define a secret string that will be used to sign API tokens (leave empty to autogenerate one)',
null,
$validator
) ?: $this->generateRandomString(32),
'DISABLE_TRACK_PARAM' => $io->ask(
'Provide a parameter name that you will be able to use to disable tracking on specific request to '
. 'short URLs (leave empty and this feature won\'t be enabled)',
null,
$validator
),
]);
}
}

View File

@@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\Installer\Config\Plugin;
use Shlinkio\Shlink\Installer\Model\CustomizableAppConfig;
use Symfony\Component\Console\Style\SymfonyStyle;
interface ConfigCustomizerInterface
{
/**
* @param SymfonyStyle $io
* @param CustomizableAppConfig $appConfig
* @return void
*/
public function process(SymfonyStyle $io, CustomizableAppConfig $appConfig);
}

View File

@@ -0,0 +1,79 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\Installer\Config\Plugin;
use Shlinkio\Shlink\Installer\Config\Plugin\ConfigCustomizerInterface;
use Shlinkio\Shlink\Installer\Model\CustomizableAppConfig;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\Filesystem\Exception\IOException;
use Symfony\Component\Filesystem\Filesystem;
class DatabaseConfigCustomizer implements ConfigCustomizerInterface
{
private const DATABASE_DRIVERS = [
'MySQL' => 'pdo_mysql',
'PostgreSQL' => 'pdo_pgsql',
'SQLite' => 'pdo_sqlite',
];
/**
* @var Filesystem
*/
private $filesystem;
public function __construct(Filesystem $filesystem)
{
$this->filesystem = $filesystem;
}
/**
* @param SymfonyStyle $io
* @param CustomizableAppConfig $appConfig
* @return void
* @throws IOException
*/
public function process(SymfonyStyle $io, CustomizableAppConfig $appConfig)
{
$io->title('DATABASE');
if ($appConfig->hasDatabase() && $io->confirm('Do you want to keep imported database config?')) {
// If the user selected to keep DB config and is configured to use sqlite, copy DB file
if ($appConfig->getDatabase()['DRIVER'] === self::DATABASE_DRIVERS['SQLite']) {
try {
$this->filesystem->copy(
$appConfig->getImportedInstallationPath() . '/' . CustomizableAppConfig::SQLITE_DB_PATH,
CustomizableAppConfig::SQLITE_DB_PATH
);
} catch (IOException $e) {
$io->error('It wasn\'t possible to import the SQLite database');
throw $e;
}
}
return;
}
// Select database type
$params = [];
$databases = \array_keys(self::DATABASE_DRIVERS);
$dbType = $io->choice('Select database type', $databases, $databases[0]);
$params['DRIVER'] = self::DATABASE_DRIVERS[$dbType];
// Ask for connection params if database is not SQLite
if ($params['DRIVER'] !== self::DATABASE_DRIVERS['SQLite']) {
$params['NAME'] = $io->ask('Database name', 'shlink');
$params['USER'] = $io->ask('Database username');
$params['PASSWORD'] = $io->ask('Database password');
$params['HOST'] = $io->ask('Database host', 'localhost');
$params['PORT'] = $io->ask('Database port', $this->getDefaultDbPort($params['DRIVER']));
}
$appConfig->setDatabase($params);
}
private function getDefaultDbPort(string $driver): string
{
return $driver === 'pdo_mysql' ? '3306' : '5432';
}
}

View File

@@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\Installer\Config\Plugin;
use Shlinkio\Shlink\Installer\Config\Plugin\ConfigCustomizerInterface;
use Shlinkio\Shlink\Installer\Model\CustomizableAppConfig;
use Symfony\Component\Console\Style\SymfonyStyle;
class LanguageConfigCustomizer implements ConfigCustomizerInterface
{
private const SUPPORTED_LANGUAGES = ['en', 'es'];
/**
* @param SymfonyStyle $io
* @param \Shlinkio\Shlink\Installer\Model\CustomizableAppConfig $appConfig
* @return void
*/
public function process(SymfonyStyle $io, CustomizableAppConfig $appConfig)
{
$io->title('LANGUAGE');
if ($appConfig->hasLanguage() && $io->confirm('Do you want to keep imported language?')) {
return;
}
$appConfig->setLanguage([
'DEFAULT' => $this->chooseLanguage('Select default language for the application in general', $io),
'CLI' => $this->chooseLanguage('Select default language for CLI executions', $io),
]);
}
private function chooseLanguage(string $message, SymfonyStyle $io): string
{
return $io->choice($message, self::SUPPORTED_LANGUAGES, self::SUPPORTED_LANGUAGES[0]);
}
}

View File

@@ -0,0 +1,44 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\Installer\Config\Plugin;
use Shlinkio\Shlink\Core\Service\UrlShortener;
use Shlinkio\Shlink\Installer\Model\CustomizableAppConfig;
use Symfony\Component\Console\Style\SymfonyStyle;
use function str_shuffle;
class UrlShortenerConfigCustomizer implements ConfigCustomizerInterface
{
/**
* @param SymfonyStyle $io
* @param CustomizableAppConfig $appConfig
* @return void
*/
public function process(SymfonyStyle $io, CustomizableAppConfig $appConfig)
{
$io->title('URL SHORTENER');
if ($appConfig->hasUrlShortener() && $io->confirm('Do you want to keep imported URL shortener config?')) {
return;
}
// Ask for URL shortener params
$appConfig->setUrlShortener([
'SCHEMA' => $io->choice(
'Select schema for generated short URLs',
['http', 'https'],
'http'
),
'HOSTNAME' => $io->ask('Hostname for generated URLs'),
'CHARS' => $io->ask(
'Character set for generated short codes (leave empty to autogenerate one)',
null,
function ($value) {
return $value;
}
) ?: str_shuffle(UrlShortener::DEFAULT_CHARS),
'VALIDATE_URL' => $io->confirm('Do you want to validate long urls by 200 HTTP status code on response'),
]);
}
}

View File

@@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\Installer;
use Zend\Config\Factory;
use Zend\Stdlib\Glob;
class ConfigProvider
{
public function __invoke()
{
return Factory::fromFiles(Glob::glob(__DIR__ . '/../config/{,*.}config.php', Glob::GLOB_BRACE));
}
}

View File

@@ -0,0 +1,57 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\Installer\Factory;
use Interop\Container\ContainerInterface;
use Interop\Container\Exception\ContainerException;
use Shlinkio\Shlink\Installer\Command\InstallCommand;
use Shlinkio\Shlink\Installer\Config\ConfigCustomizerManager;
use Shlinkio\Shlink\Installer\Config\Plugin;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Exception\LogicException;
use Symfony\Component\Filesystem\Filesystem;
use Zend\Config\Writer\PhpArray;
use Zend\ServiceManager\AbstractFactory\ConfigAbstractFactory;
use Zend\ServiceManager\Exception\ServiceNotCreatedException;
use Zend\ServiceManager\Exception\ServiceNotFoundException;
use Zend\ServiceManager\Factory\FactoryInterface;
use Zend\ServiceManager\Factory\InvokableFactory;
class InstallApplicationFactory implements FactoryInterface
{
/**
* Create an object
*
* @param ContainerInterface $container
* @param string $requestedName
* @param null|array $options
* @return object
* @throws LogicException
* @throws ServiceNotFoundException if unable to resolve the service.
* @throws ServiceNotCreatedException if an exception is raised when
* creating a service.
* @throws ContainerException if any other error occurs
*/
public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
{
$isUpdate = $options !== null && isset($options['isUpdate']) ? (bool) $options['isUpdate'] : false;
$app = new Application();
$command = new InstallCommand(
new PhpArray(),
$container->get(Filesystem::class),
new ConfigCustomizerManager($container, ['factories' => [
Plugin\DatabaseConfigCustomizer::class => ConfigAbstractFactory::class,
Plugin\UrlShortenerConfigCustomizer::class => InvokableFactory::class,
Plugin\LanguageConfigCustomizer::class => InvokableFactory::class,
Plugin\ApplicationConfigCustomizer::class => InvokableFactory::class,
]]),
$isUpdate
);
$app->add($command);
$app->setDefaultCommand($command->getName(), true);
return $app;
}
}

View File

@@ -0,0 +1,270 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\Installer\Model;
use Zend\Stdlib\ArraySerializableInterface;
final class CustomizableAppConfig implements ArraySerializableInterface
{
public const SQLITE_DB_PATH = 'data/database.sqlite';
/**
* @var array
*/
private $database;
/**
* @var array
*/
private $urlShortener;
/**
* @var array
*/
private $language;
/**
* @var array
*/
private $app;
/**
* @var string
*/
private $importedInstallationPath;
/**
* @return array
*/
public function getDatabase()
{
return $this->database;
}
/**
* @param array $database
* @return $this
*/
public function setDatabase(array $database)
{
$this->database = $database;
return $this;
}
/**
* @return bool
*/
public function hasDatabase()
{
return ! empty($this->database);
}
/**
* @return array
*/
public function getUrlShortener()
{
return $this->urlShortener;
}
/**
* @param array $urlShortener
* @return $this
*/
public function setUrlShortener(array $urlShortener)
{
$this->urlShortener = $urlShortener;
return $this;
}
/**
* @return bool
*/
public function hasUrlShortener()
{
return ! empty($this->urlShortener);
}
/**
* @return array
*/
public function getLanguage()
{
return $this->language;
}
/**
* @param array $language
* @return $this
*/
public function setLanguage(array $language)
{
$this->language = $language;
return $this;
}
/**
* @return bool
*/
public function hasLanguage()
{
return ! empty($this->language);
}
/**
* @return array
*/
public function getApp()
{
return $this->app;
}
/**
* @param array $app
* @return $this
*/
public function setApp(array $app)
{
$this->app = $app;
return $this;
}
/**
* @return bool
*/
public function hasApp()
{
return ! empty($this->app);
}
/**
* @return string
*/
public function getImportedInstallationPath()
{
return $this->importedInstallationPath;
}
/**
* @param string $importedInstallationPath
* @return $this|self
*/
public function setImportedInstallationPath($importedInstallationPath)
{
$this->importedInstallationPath = $importedInstallationPath;
return $this;
}
/**
* @return bool
*/
public function hasImportedInstallationPath()
{
return $this->importedInstallationPath !== null;
}
/**
* Exchange internal values from provided array
*
* @param array $array
* @return void
*/
public function exchangeArray(array $array)
{
if (isset($array['app_options'], $array['app_options']['secret_key'])) {
$this->setApp([
'SECRET' => $array['app_options']['secret_key'],
]);
}
if (isset($array['entity_manager'], $array['entity_manager']['connection'])) {
$this->deserializeDatabase($array['entity_manager']['connection']);
}
if (isset($array['translator'], $array['translator']['locale'], $array['cli'], $array['cli']['locale'])) {
$this->setLanguage([
'DEFAULT' => $array['translator']['locale'],
'CLI' => $array['cli']['locale'],
]);
}
if (isset($array['url_shortener'])) {
$urlShortener = $array['url_shortener'];
$this->setUrlShortener([
'SCHEMA' => $urlShortener['domain']['schema'],
'HOSTNAME' => $urlShortener['domain']['hostname'],
'CHARS' => $urlShortener['shortcode_chars'],
'VALIDATE_URL' => $urlShortener['validate_url'] ?? true,
]);
}
}
private function deserializeDatabase(array $conn)
{
if (! isset($conn['driver'])) {
return;
}
$driver = $conn['driver'];
$params = ['DRIVER' => $driver];
if ($driver !== 'pdo_sqlite') {
$params['USER'] = $conn['user'];
$params['PASSWORD'] = $conn['password'];
$params['NAME'] = $conn['dbname'];
$params['HOST'] = $conn['host'];
$params['PORT'] = $conn['port'];
}
$this->setDatabase($params);
}
/**
* Return an array representation of the object
*
* @return array
*/
public function getArrayCopy()
{
$config = [
'app_options' => [
'secret_key' => $this->app['SECRET'],
'disable_track_param' => $this->app['DISABLE_TRACK_PARAM'] ?? null,
],
'entity_manager' => [
'connection' => [
'driver' => $this->database['DRIVER'],
],
],
'translator' => [
'locale' => $this->language['DEFAULT'],
],
'cli' => [
'locale' => $this->language['CLI'],
],
'url_shortener' => [
'domain' => [
'schema' => $this->urlShortener['SCHEMA'],
'hostname' => $this->urlShortener['HOSTNAME'],
],
'shortcode_chars' => $this->urlShortener['CHARS'],
'validate_url' => $this->urlShortener['VALIDATE_URL'],
],
];
// Build dynamic database config based on selected driver
if ($this->database['DRIVER'] === 'pdo_sqlite') {
$config['entity_manager']['connection']['path'] = self::SQLITE_DB_PATH;
} else {
$config['entity_manager']['connection']['user'] = $this->database['USER'];
$config['entity_manager']['connection']['password'] = $this->database['PASSWORD'];
$config['entity_manager']['connection']['dbname'] = $this->database['NAME'];
$config['entity_manager']['connection']['host'] = $this->database['HOST'];
$config['entity_manager']['connection']['port'] = $this->database['PORT'];
if ($this->database['DRIVER'] === 'pdo_mysql') {
$config['entity_manager']['connection']['driverOptions'] = [
\PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES utf8',
];
}
}
return $config;
}
}

View File

@@ -0,0 +1,2 @@
<?php
return [];

View File

@@ -0,0 +1,147 @@
<?php
declare(strict_types=1);
namespace ShlinkioTest\Shlink\Installer\Command;
use PHPUnit\Framework\TestCase;
use Prophecy\Argument;
use Prophecy\Prophecy\MethodProphecy;
use Prophecy\Prophecy\ObjectProphecy;
use Shlinkio\Shlink\Installer\Command\InstallCommand;
use Shlinkio\Shlink\Installer\Config\ConfigCustomizerManagerInterface;
use Shlinkio\Shlink\Installer\Config\Plugin\ConfigCustomizerInterface;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Helper\ProcessHelper;
use Symfony\Component\Console\Tester\CommandTester;
use Symfony\Component\Filesystem\Exception\IOException;
use Symfony\Component\Filesystem\Filesystem;
use Symfony\Component\Process\PhpExecutableFinder;
use Symfony\Component\Process\Process;
use Zend\Config\Writer\WriterInterface;
class InstallCommandTest extends TestCase
{
/**
* @var InstallCommand
*/
protected $command;
/**
* @var CommandTester
*/
protected $commandTester;
/**
* @var ObjectProphecy
*/
protected $configWriter;
/**
* @var ObjectProphecy
*/
protected $filesystem;
public function setUp()
{
$processMock = $this->prophesize(Process::class);
$processMock->isSuccessful()->willReturn(true);
$processHelper = $this->prophesize(ProcessHelper::class);
$processHelper->getName()->willReturn('process');
$processHelper->setHelperSet(Argument::any())->willReturn(null);
$processHelper->run(Argument::cetera())->willReturn($processMock->reveal());
$this->filesystem = $this->prophesize(Filesystem::class);
$this->filesystem->exists(Argument::cetera())->willReturn(false);
$this->configWriter = $this->prophesize(WriterInterface::class);
$configCustomizer = $this->prophesize(ConfigCustomizerInterface::class);
$configCustomizers = $this->prophesize(ConfigCustomizerManagerInterface::class);
$configCustomizers->get(Argument::cetera())->willReturn($configCustomizer->reveal());
$finder = $this->prophesize(PhpExecutableFinder::class);
$finder->find(false)->willReturn('php');
$app = new Application();
$helperSet = $app->getHelperSet();
$helperSet->set($processHelper->reveal());
$app->setHelperSet($helperSet);
$this->command = new InstallCommand(
$this->configWriter->reveal(),
$this->filesystem->reveal(),
$configCustomizers->reveal(),
false,
$finder->reveal()
);
$app->add($this->command);
$this->commandTester = new CommandTester($this->command);
}
/**
* @test
*/
public function generatedConfigIsProperlyPersisted()
{
$this->configWriter->toFile(Argument::any(), Argument::type('array'), false)->shouldBeCalledTimes(1);
$this->commandTester->execute([]);
}
/**
* @test
*/
public function cachedConfigIsDeletedIfExists()
{
/** @var MethodProphecy $appConfigExists */
$appConfigExists = $this->filesystem->exists('data/cache/app_config.php')->willReturn(true);
/** @var MethodProphecy $appConfigRemove */
$appConfigRemove = $this->filesystem->remove('data/cache/app_config.php')->willReturn(null);
$this->commandTester->execute([]);
$appConfigExists->shouldHaveBeenCalledTimes(1);
$appConfigRemove->shouldHaveBeenCalledTimes(1);
}
/**
* @test
*/
public function exceptionWhileDeletingCachedConfigCancelsProcess()
{
/** @var MethodProphecy $appConfigExists */
$appConfigExists = $this->filesystem->exists('data/cache/app_config.php')->willReturn(true);
/** @var MethodProphecy $appConfigRemove */
$appConfigRemove = $this->filesystem->remove('data/cache/app_config.php')->willThrow(IOException::class);
/** @var MethodProphecy $configToFile */
$configToFile = $this->configWriter->toFile(Argument::cetera())->willReturn(true);
$this->commandTester->execute([]);
$appConfigExists->shouldHaveBeenCalledTimes(1);
$appConfigRemove->shouldHaveBeenCalledTimes(1);
$configToFile->shouldNotHaveBeenCalled();
}
/**
* @test
*/
public function whenCommandIsUpdatePreviousConfigCanBeImported()
{
$ref = new \ReflectionObject($this->command);
$prop = $ref->getProperty('isUpdate');
$prop->setAccessible(true);
$prop->setValue($this->command, true);
/** @var MethodProphecy $importedConfigExists */
$importedConfigExists = $this->filesystem->exists(
__DIR__ . '/../../test-resources/' . InstallCommand::GENERATED_CONFIG_PATH
)->willReturn(true);
$this->commandTester->setInputs([
'',
'/foo/bar/wrong_previous_shlink',
'',
__DIR__ . '/../../test-resources',
]);
$this->commandTester->execute([]);
$importedConfigExists->shouldHaveBeenCalled();
}
}

View File

@@ -0,0 +1,91 @@
<?php
declare(strict_types=1);
namespace ShlinkioTest\Shlink\Installer\Config\Plugin;
use PHPUnit\Framework\TestCase;
use Prophecy\Argument;
use Prophecy\Prophecy\ObjectProphecy;
use Shlinkio\Shlink\Installer\Config\Plugin\ApplicationConfigCustomizer;
use Shlinkio\Shlink\Installer\Model\CustomizableAppConfig;
use Symfony\Component\Console\Style\SymfonyStyle;
class ApplicationConfigCustomizerTest extends TestCase
{
/**
* @var ApplicationConfigCustomizer
*/
private $plugin;
/**
* @var ObjectProphecy
*/
private $io;
public function setUp()
{
$this->io = $this->prophesize(SymfonyStyle::class);
$this->io->title(Argument::any())->willReturn(null);
$this->plugin = new ApplicationConfigCustomizer();
}
/**
* @test
*/
public function configIsRequestedToTheUser()
{
$ask = $this->io->ask(Argument::cetera())->willReturn('the_secret');
$config = new CustomizableAppConfig();
$this->plugin->process($this->io->reveal(), $config);
$this->assertTrue($config->hasApp());
$this->assertEquals([
'SECRET' => 'the_secret',
'DISABLE_TRACK_PARAM' => 'the_secret',
], $config->getApp());
$ask->shouldHaveBeenCalledTimes(2);
}
/**
* @test
*/
public function overwriteIsRequestedIfValueIsAlreadySet()
{
$ask = $this->io->ask(Argument::cetera())->willReturn('the_new_secret');
$confirm = $this->io->confirm(Argument::cetera())->willReturn(false);
$config = new CustomizableAppConfig();
$config->setApp([
'SECRET' => 'foo',
]);
$this->plugin->process($this->io->reveal(), $config);
$this->assertEquals([
'SECRET' => 'the_new_secret',
'DISABLE_TRACK_PARAM' => 'the_new_secret',
], $config->getApp());
$ask->shouldHaveBeenCalledTimes(2);
$confirm->shouldHaveBeenCalledTimes(1);
}
/**
* @test
*/
public function existingValueIsKeptIfRequested()
{
$confirm = $this->io->confirm(Argument::cetera())->willReturn(true);
$config = new CustomizableAppConfig();
$config->setApp([
'SECRET' => 'foo',
]);
$this->plugin->process($this->io->reveal(), $config);
$this->assertEquals([
'SECRET' => 'foo',
], $config->getApp());
$confirm->shouldHaveBeenCalledTimes(1);
}
}

View File

@@ -0,0 +1,146 @@
<?php
declare(strict_types=1);
namespace ShlinkioTest\Shlink\Installer\Config\Plugin;
use PHPUnit\Framework\TestCase;
use Prophecy\Argument;
use Prophecy\Prophecy\ObjectProphecy;
use Shlinkio\Shlink\Installer\Config\Plugin\DatabaseConfigCustomizer;
use Shlinkio\Shlink\Installer\Model\CustomizableAppConfig;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\Filesystem\Filesystem;
class DatabaseConfigCustomizerTest extends TestCase
{
/**
* @var DatabaseConfigCustomizer
*/
private $plugin;
/**
* @var ObjectProphecy
*/
private $io;
/**
* @var ObjectProphecy
*/
private $filesystem;
public function setUp()
{
$this->io = $this->prophesize(SymfonyStyle::class);
$this->io->title(Argument::any())->willReturn(null);
$this->filesystem = $this->prophesize(Filesystem::class);
$this->plugin = new DatabaseConfigCustomizer($this->filesystem->reveal());
}
/**
* @test
*/
public function configIsRequestedToTheUser()
{
$choice = $this->io->choice(Argument::cetera())->willReturn('MySQL');
$ask = $this->io->ask(Argument::cetera())->willReturn('param');
$config = new CustomizableAppConfig();
$this->plugin->process($this->io->reveal(), $config);
$this->assertTrue($config->hasDatabase());
$this->assertEquals([
'DRIVER' => 'pdo_mysql',
'NAME' => 'param',
'USER' => 'param',
'PASSWORD' => 'param',
'HOST' => 'param',
'PORT' => 'param',
], $config->getDatabase());
$choice->shouldHaveBeenCalledTimes(1);
$ask->shouldHaveBeenCalledTimes(5);
}
/**
* @test
*/
public function overwriteIsRequestedIfValueIsAlreadySet()
{
$choice = $this->io->choice(Argument::cetera())->willReturn('MySQL');
$confirm = $this->io->confirm(Argument::cetera())->willReturn(false);
$ask = $this->io->ask(Argument::cetera())->willReturn('MySQL');
$config = new CustomizableAppConfig();
$config->setDatabase([
'DRIVER' => 'pdo_pgsql',
'NAME' => 'MySQL',
'USER' => 'MySQL',
'PASSWORD' => 'MySQL',
'HOST' => 'MySQL',
'PORT' => 'MySQL',
]);
$this->plugin->process($this->io->reveal(), $config);
$this->assertEquals([
'DRIVER' => 'pdo_mysql',
'NAME' => 'MySQL',
'USER' => 'MySQL',
'PASSWORD' => 'MySQL',
'HOST' => 'MySQL',
'PORT' => 'MySQL',
], $config->getDatabase());
$confirm->shouldHaveBeenCalledTimes(1);
$choice->shouldHaveBeenCalledTimes(1);
$ask->shouldHaveBeenCalledTimes(5);
}
/**
* @test
*/
public function existingValueIsKeptIfRequested()
{
$confirm = $this->io->confirm(Argument::cetera())->willReturn(true);
$config = new CustomizableAppConfig();
$config->setDatabase([
'DRIVER' => 'pdo_pgsql',
'NAME' => 'MySQL',
'USER' => 'MySQL',
'PASSWORD' => 'MySQL',
'HOST' => 'MySQL',
'PORT' => 'MySQL',
]);
$this->plugin->process($this->io->reveal(), $config);
$this->assertEquals([
'DRIVER' => 'pdo_pgsql',
'NAME' => 'MySQL',
'USER' => 'MySQL',
'PASSWORD' => 'MySQL',
'HOST' => 'MySQL',
'PORT' => 'MySQL',
], $config->getDatabase());
$confirm->shouldHaveBeenCalledTimes(1);
}
/**
* @test
*/
public function sqliteDatabaseIsImportedWhenRequested()
{
$confirm = $this->io->confirm(Argument::cetera())->willReturn(true);
$copy = $this->filesystem->copy(Argument::cetera())->willReturn(null);
$config = new CustomizableAppConfig();
$config->setDatabase([
'DRIVER' => 'pdo_sqlite',
]);
$this->plugin->process($this->io->reveal(), $config);
$this->assertEquals([
'DRIVER' => 'pdo_sqlite',
], $config->getDatabase());
$confirm->shouldHaveBeenCalledTimes(1);
$copy->shouldHaveBeenCalledTimes(1);
}
}

View File

@@ -0,0 +1,93 @@
<?php
declare(strict_types=1);
namespace ShlinkioTest\Shlink\Installer\Config\Plugin;
use PHPUnit\Framework\TestCase;
use Prophecy\Argument;
use Prophecy\Prophecy\ObjectProphecy;
use Shlinkio\Shlink\Installer\Config\Plugin\LanguageConfigCustomizer;
use Shlinkio\Shlink\Installer\Model\CustomizableAppConfig;
use Symfony\Component\Console\Style\SymfonyStyle;
class LanguageConfigCustomizerTest extends TestCase
{
/**
* @var LanguageConfigCustomizer
*/
protected $plugin;
/**
* @var ObjectProphecy
*/
protected $io;
public function setUp()
{
$this->io = $this->prophesize(SymfonyStyle::class);
$this->io->title(Argument::any())->willReturn(null);
$this->plugin = new LanguageConfigCustomizer();
}
/**
* @test
*/
public function configIsRequestedToTheUser()
{
$ask = $this->io->choice(Argument::cetera())->willReturn('en');
$config = new CustomizableAppConfig();
$this->plugin->process($this->io->reveal(), $config);
$this->assertTrue($config->hasLanguage());
$this->assertEquals([
'DEFAULT' => 'en',
'CLI' => 'en',
], $config->getLanguage());
$ask->shouldHaveBeenCalledTimes(2);
}
/**
* @test
*/
public function overwriteIsRequestedIfValueIsAlreadySet()
{
$choice = $this->io->choice(Argument::cetera())->willReturn('es');
$confirm = $this->io->confirm(Argument::cetera())->willReturn(false);
$config = new CustomizableAppConfig();
$config->setLanguage([
'DEFAULT' => 'en',
'CLI' => 'en',
]);
$this->plugin->process($this->io->reveal(), $config);
$this->assertEquals([
'DEFAULT' => 'es',
'CLI' => 'es',
], $config->getLanguage());
$choice->shouldHaveBeenCalledTimes(2);
$confirm->shouldHaveBeenCalledTimes(1);
}
/**
* @test
*/
public function existingValueIsKeptIfRequested()
{
$ask = $this->io->confirm(Argument::cetera())->willReturn(true);
$config = new CustomizableAppConfig();
$config->setLanguage([
'DEFAULT' => 'es',
'CLI' => 'es',
]);
$this->plugin->process($this->io->reveal(), $config);
$this->assertEquals([
'DEFAULT' => 'es',
'CLI' => 'es',
], $config->getLanguage());
$ask->shouldHaveBeenCalledTimes(1);
}
}

View File

@@ -0,0 +1,109 @@
<?php
declare(strict_types=1);
namespace ShlinkioTest\Shlink\Installer\Config\Plugin;
use PHPUnit\Framework\TestCase;
use Prophecy\Argument;
use Prophecy\Prophecy\ObjectProphecy;
use Shlinkio\Shlink\Installer\Config\Plugin\UrlShortenerConfigCustomizer;
use Shlinkio\Shlink\Installer\Model\CustomizableAppConfig;
use Symfony\Component\Console\Style\SymfonyStyle;
class UrlShortenerConfigCustomizerTest extends TestCase
{
/**
* @var UrlShortenerConfigCustomizer
*/
private $plugin;
/**
* @var ObjectProphecy
*/
private $io;
public function setUp()
{
$this->io = $this->prophesize(SymfonyStyle::class);
$this->io->title(Argument::any())->willReturn(null);
$this->plugin = new UrlShortenerConfigCustomizer();
}
/**
* @test
*/
public function configIsRequestedToTheUser()
{
$choice = $this->io->choice(Argument::cetera())->willReturn('something');
$ask = $this->io->ask(Argument::cetera())->willReturn('something');
$confirm = $this->io->confirm(Argument::cetera())->willReturn(true);
$config = new CustomizableAppConfig();
$this->plugin->process($this->io->reveal(), $config);
$this->assertTrue($config->hasUrlShortener());
$this->assertEquals([
'SCHEMA' => 'something',
'HOSTNAME' => 'something',
'CHARS' => 'something',
'VALIDATE_URL' => true,
], $config->getUrlShortener());
$ask->shouldHaveBeenCalledTimes(2);
$choice->shouldHaveBeenCalledTimes(1);
$confirm->shouldHaveBeenCalledTimes(1);
}
/**
* @test
*/
public function overwriteIsRequestedIfValueIsAlreadySet()
{
$choice = $this->io->choice(Argument::cetera())->willReturn('foo');
$ask = $this->io->ask(Argument::cetera())->willReturn('foo');
$confirm = $this->io->confirm(Argument::cetera())->willReturn(false);
$config = new CustomizableAppConfig();
$config->setUrlShortener([
'SCHEMA' => 'bar',
'HOSTNAME' => 'bar',
'CHARS' => 'bar',
'VALIDATE_URL' => true,
]);
$this->plugin->process($this->io->reveal(), $config);
$this->assertEquals([
'SCHEMA' => 'foo',
'HOSTNAME' => 'foo',
'CHARS' => 'foo',
'VALIDATE_URL' => false,
], $config->getUrlShortener());
$ask->shouldHaveBeenCalledTimes(2);
$choice->shouldHaveBeenCalledTimes(1);
$confirm->shouldHaveBeenCalledTimes(2);
}
/**
* @test
*/
public function existingValueIsKeptIfRequested()
{
$confirm = $this->io->confirm(Argument::cetera())->willReturn(true);
$config = new CustomizableAppConfig();
$config->setUrlShortener([
'SCHEMA' => 'foo',
'HOSTNAME' => 'foo',
'CHARS' => 'foo',
'VALIDATE_URL' => 'foo',
]);
$this->plugin->process($this->io->reveal(), $config);
$this->assertEquals([
'SCHEMA' => 'foo',
'HOSTNAME' => 'foo',
'CHARS' => 'foo',
'VALIDATE_URL' => 'foo',
], $config->getUrlShortener());
$confirm->shouldHaveBeenCalledTimes(1);
}
}

View File

@@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
namespace ShlinkioTest\Shlink\Installer\Factory;
use PHPUnit\Framework\TestCase;
use Shlinkio\Shlink\Installer\Factory\InstallApplicationFactory;
use Symfony\Component\Console\Application;
use Symfony\Component\Filesystem\Filesystem;
use Zend\ServiceManager\ServiceManager;
class InstallApplicationFactoryTest extends TestCase
{
/**
* @var InstallApplicationFactory
*/
private $factory;
public function setUp()
{
$this->factory = new InstallApplicationFactory();
}
/**
* @test
*/
public function serviceIsCreated()
{
$instance = $this->factory->__invoke(new ServiceManager(['services' => [
Filesystem::class => $this->prophesize(Filesystem::class)->reveal(),
]]), '');
$this->assertInstanceOf(Application::class, $instance);
}
}