From f7b6f4ba19c8b7dcdc20a0f404aa8b541dc8643f Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Thu, 8 Apr 2021 13:12:37 +0200 Subject: [PATCH] Created new command containing the logic to download the GeoLite2 db file --- module/CLI/config/cli.config.php | 1 + module/CLI/config/dependencies.config.php | 3 +- .../Visit/DownloadGeoLiteDbCommand.php | 80 +++++++++++++++++ .../src/Command/Visit/LocateVisitsCommand.php | 51 +++-------- module/CLI/test/CliTestUtilsTrait.php | 32 +++++++ .../Command/Visit/LocateVisitsCommandTest.php | 90 +++++++++++-------- .../test/Factory/ApplicationFactoryTest.php | 21 +---- 7 files changed, 182 insertions(+), 96 deletions(-) create mode 100644 module/CLI/src/Command/Visit/DownloadGeoLiteDbCommand.php create mode 100644 module/CLI/test/CliTestUtilsTrait.php diff --git a/module/CLI/config/cli.config.php b/module/CLI/config/cli.config.php index 6e32428a..6043833b 100644 --- a/module/CLI/config/cli.config.php +++ b/module/CLI/config/cli.config.php @@ -15,6 +15,7 @@ return [ Command\ShortUrl\DeleteShortUrlCommand::NAME => Command\ShortUrl\DeleteShortUrlCommand::class, Command\Visit\LocateVisitsCommand::NAME => Command\Visit\LocateVisitsCommand::class, + Command\Visit\DownloadGeoLiteDbCommand::NAME => Command\Visit\DownloadGeoLiteDbCommand::class, Command\Api\GenerateKeyCommand::NAME => Command\Api\GenerateKeyCommand::class, Command\Api\DisableKeyCommand::NAME => Command\Api\DisableKeyCommand::class, diff --git a/module/CLI/config/dependencies.config.php b/module/CLI/config/dependencies.config.php index 80b26b8d..7d7e2865 100644 --- a/module/CLI/config/dependencies.config.php +++ b/module/CLI/config/dependencies.config.php @@ -44,6 +44,7 @@ return [ Command\ShortUrl\GetVisitsCommand::class => ConfigAbstractFactory::class, Command\ShortUrl\DeleteShortUrlCommand::class => ConfigAbstractFactory::class, + Command\Visit\DownloadGeoLiteDbCommand::class => ConfigAbstractFactory::class, Command\Visit\LocateVisitsCommand::class => ConfigAbstractFactory::class, Command\Api\GenerateKeyCommand::class => ConfigAbstractFactory::class, @@ -80,11 +81,11 @@ return [ Command\ShortUrl\GetVisitsCommand::class => [Visit\VisitsStatsHelper::class], Command\ShortUrl\DeleteShortUrlCommand::class => [Service\ShortUrl\DeleteShortUrlService::class], + Command\Visit\DownloadGeoLiteDbCommand::class => [Util\GeolocationDbUpdater::class], Command\Visit\LocateVisitsCommand::class => [ Visit\VisitLocator::class, IpLocationResolverInterface::class, LockFactory::class, - Util\GeolocationDbUpdater::class, ], Command\Api\GenerateKeyCommand::class => [ApiKeyService::class, ApiKey\RoleResolver::class], diff --git a/module/CLI/src/Command/Visit/DownloadGeoLiteDbCommand.php b/module/CLI/src/Command/Visit/DownloadGeoLiteDbCommand.php new file mode 100644 index 00000000..5f52c3b7 --- /dev/null +++ b/module/CLI/src/Command/Visit/DownloadGeoLiteDbCommand.php @@ -0,0 +1,80 @@ +dbUpdater = $dbUpdater; + } + + protected function configure(): void + { + $this + ->setName(self::NAME) + ->setDescription( + 'Checks if the GeoLite2 db file is too old or it does not exist, and tries to download an up-to-date ' + . 'copy if so.', + ); + } + + protected function execute(InputInterface $input, OutputInterface $output): ?int + { + $io = new SymfonyStyle($input, $output); + + try { + $this->dbUpdater->checkDbUpdate(function (bool $olderDbExists) use ($io): void { + $io->text(sprintf('%s GeoLite2 db file...', $olderDbExists ? 'Updating' : 'Downloading')); + $this->progressBar = new ProgressBar($io); + }, function (int $total, int $downloaded): void { + $this->progressBar->setMaxSteps($total); + $this->progressBar->setProgress($downloaded); + }); + + if ($this->progressBar !== null) { + $this->progressBar->finish(); + $io->success('GeoLite2 db file properly downloaded.'); + } else { + $io->info('GeoLite2 db file is up to date.'); + } + + return ExitCodes::EXIT_SUCCESS; + } catch (GeolocationDbUpdateFailedException $e) { + $olderDbExists = $e->olderDbExists(); + + if ($olderDbExists) { + $io->warning( + 'GeoLite2 db file update failed. Visits will continue to be located with the old version.', + ); + } else { + $io->error('GeoLite2 db file download failed. It will not be possible to locate visits.'); + } + + if ($io->isVerbose()) { + $this->getApplication()->renderThrowable($e, $io); + } + + return $olderDbExists ? ExitCodes::EXIT_WARNING : ExitCodes::EXIT_FAILURE; + } + } +} diff --git a/module/CLI/src/Command/Visit/LocateVisitsCommand.php b/module/CLI/src/Command/Visit/LocateVisitsCommand.php index a71ee410..0bcfb1d7 100644 --- a/module/CLI/src/Command/Visit/LocateVisitsCommand.php +++ b/module/CLI/src/Command/Visit/LocateVisitsCommand.php @@ -6,9 +6,7 @@ namespace Shlinkio\Shlink\CLI\Command\Visit; use Shlinkio\Shlink\CLI\Command\Util\AbstractLockedCommand; use Shlinkio\Shlink\CLI\Command\Util\LockedCommandConfig; -use Shlinkio\Shlink\CLI\Exception\GeolocationDbUpdateFailedException; use Shlinkio\Shlink\CLI\Util\ExitCodes; -use Shlinkio\Shlink\CLI\Util\GeolocationDbUpdaterInterface; use Shlinkio\Shlink\Common\Util\IpAddress; use Shlinkio\Shlink\Core\Entity\Visit; use Shlinkio\Shlink\Core\Entity\VisitLocation; @@ -19,7 +17,6 @@ use Shlinkio\Shlink\IpGeolocation\Exception\WrongIpException; use Shlinkio\Shlink\IpGeolocation\Model\Location; use Shlinkio\Shlink\IpGeolocation\Resolver\IpLocationResolverInterface; use Symfony\Component\Console\Exception\RuntimeException; -use Symfony\Component\Console\Helper\ProgressBar; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; @@ -35,28 +32,26 @@ class LocateVisitsCommand extends AbstractLockedCommand implements VisitGeolocat private VisitLocatorInterface $visitLocator; private IpLocationResolverInterface $ipLocationResolver; - private GeolocationDbUpdaterInterface $dbUpdater; private SymfonyStyle $io; - private ?ProgressBar $progressBar = null; public function __construct( VisitLocatorInterface $visitLocator, IpLocationResolverInterface $ipLocationResolver, - LockFactory $locker, - GeolocationDbUpdaterInterface $dbUpdater + LockFactory $locker ) { parent::__construct($locker); $this->visitLocator = $visitLocator; $this->ipLocationResolver = $ipLocationResolver; - $this->dbUpdater = $dbUpdater; } protected function configure(): void { $this ->setName(self::NAME) - ->setDescription('Resolves visits origin locations.') + ->setDescription( + 'Resolves visits origin locations. It implicitly downloads/updates the GeoLite2 db file if needed.', + ) ->addOption( 'retry', 'r', @@ -90,12 +85,12 @@ class LocateVisitsCommand extends AbstractLockedCommand implements VisitGeolocat ); } - if ($all && $retry && ! $this->warnAndVerifyContinue()) { + if ($all && $retry && ! $this->warnAndVerifyContinue($input)) { throw new RuntimeException('Execution aborted'); } } - private function warnAndVerifyContinue(): bool + private function warnAndVerifyContinue(InputInterface $input): bool { $this->io->warning([ 'You are about to process the location of all existing visits your short URLs received.', @@ -113,7 +108,7 @@ class LocateVisitsCommand extends AbstractLockedCommand implements VisitGeolocat $all = $retry && $input->getOption('all'); try { - $this->checkDbUpdate(); + $this->checkDbUpdate($input); if ($all) { $this->visitLocator->locateAllVisits($this); @@ -128,7 +123,7 @@ class LocateVisitsCommand extends AbstractLockedCommand implements VisitGeolocat return ExitCodes::EXIT_SUCCESS; } catch (Throwable $e) { $this->io->error($e->getMessage()); - if ($e instanceof Throwable && $this->io->isVerbose()) { + if ($this->io->isVerbose()) { $this->getApplication()->renderThrowable($e, $this->io); } @@ -176,33 +171,13 @@ class LocateVisitsCommand extends AbstractLockedCommand implements VisitGeolocat $this->io->writeln($message); } - private function checkDbUpdate(): void + private function checkDbUpdate(InputInterface $input): void { - try { - $this->dbUpdater->checkDbUpdate(function (bool $olderDbExists): void { - $this->io->writeln( - sprintf('%s GeoLite2 db file...', $olderDbExists ? 'Updating' : 'Downloading'), - ); - $this->progressBar = new ProgressBar($this->io); - }, function (int $total, int $downloaded): void { - $this->progressBar->setMaxSteps($total); - $this->progressBar->setProgress($downloaded); - }); + $downloadDbCommand = $this->getApplication()->find(DownloadGeoLiteDbCommand::NAME); + $exitCode = $downloadDbCommand->run($input, $this->io); - if ($this->progressBar !== null) { - $this->progressBar->finish(); - $this->io->newLine(); - } - } catch (GeolocationDbUpdateFailedException $e) { - if (! $e->olderDbExists()) { - $this->io->error('GeoLite2 database download failed. It is not possible to locate visits.'); - throw $e; - } - - $this->io->newLine(); - $this->io->writeln( - '[Warning] GeoLite2 database update failed. Proceeding with old version.', - ); + if ($exitCode === ExitCodes::EXIT_FAILURE) { + throw new RuntimeException('It is not possible to locate visits without a GeoLite2 db file.'); } } diff --git a/module/CLI/test/CliTestUtilsTrait.php b/module/CLI/test/CliTestUtilsTrait.php new file mode 100644 index 00000000..b5d81a76 --- /dev/null +++ b/module/CLI/test/CliTestUtilsTrait.php @@ -0,0 +1,32 @@ +prophesize(Command::class); + $command->getName()->willReturn($name); + $command->getDefinition()->willReturn($name); + $command->isEnabled()->willReturn(true); + $command->getAliases()->willReturn([]); + $command->setApplication(Argument::type(Application::class))->willReturn(function (): void { + }); + + return $command; + } +} diff --git a/module/CLI/test/Command/Visit/LocateVisitsCommandTest.php b/module/CLI/test/Command/Visit/LocateVisitsCommandTest.php index be6846a8..e5632034 100644 --- a/module/CLI/test/Command/Visit/LocateVisitsCommandTest.php +++ b/module/CLI/test/Command/Visit/LocateVisitsCommandTest.php @@ -6,11 +6,10 @@ namespace ShlinkioTest\Shlink\CLI\Command\Visit; use PHPUnit\Framework\TestCase; use Prophecy\Argument; -use Prophecy\PhpUnit\ProphecyTrait; use Prophecy\Prophecy\ObjectProphecy; +use Shlinkio\Shlink\CLI\Command\Visit\DownloadGeoLiteDbCommand; use Shlinkio\Shlink\CLI\Command\Visit\LocateVisitsCommand; -use Shlinkio\Shlink\CLI\Exception\GeolocationDbUpdateFailedException; -use Shlinkio\Shlink\CLI\Util\GeolocationDbUpdaterInterface; +use Shlinkio\Shlink\CLI\Util\ExitCodes; use Shlinkio\Shlink\Common\Util\IpAddress; use Shlinkio\Shlink\Core\Entity\ShortUrl; use Shlinkio\Shlink\Core\Entity\Visit; @@ -21,6 +20,7 @@ use Shlinkio\Shlink\Core\Visit\VisitLocator; use Shlinkio\Shlink\IpGeolocation\Exception\WrongIpException; use Shlinkio\Shlink\IpGeolocation\Model\Location; use Shlinkio\Shlink\IpGeolocation\Resolver\IpLocationResolverInterface; +use ShlinkioTest\Shlink\CLI\CliTestUtilsTrait; use Symfony\Component\Console\Application; use Symfony\Component\Console\Exception\RuntimeException; use Symfony\Component\Console\Output\OutputInterface; @@ -33,19 +33,18 @@ use const PHP_EOL; class LocateVisitsCommandTest extends TestCase { - use ProphecyTrait; + use CliTestUtilsTrait; private CommandTester $commandTester; private ObjectProphecy $visitService; private ObjectProphecy $ipResolver; private ObjectProphecy $lock; - private ObjectProphecy $dbUpdater; + private ObjectProphecy $downloadDbCommand; public function setUp(): void { $this->visitService = $this->prophesize(VisitLocator::class); $this->ipResolver = $this->prophesize(IpLocationResolverInterface::class); - $this->dbUpdater = $this->prophesize(GeolocationDbUpdaterInterface::class); $locker = $this->prophesize(Lock\LockFactory::class); $this->lock = $this->prophesize(Lock\LockInterface::class); @@ -58,11 +57,14 @@ class LocateVisitsCommandTest extends TestCase $this->visitService->reveal(), $this->ipResolver->reveal(), $locker->reveal(), - $this->dbUpdater->reveal(), ); $app = new Application(); $app->add($command); + $this->downloadDbCommand = $this->createCommandMock(DownloadGeoLiteDbCommand::NAME); + $this->downloadDbCommand->run(Argument::cetera())->willReturn(ExitCodes::EXIT_SUCCESS); + $app->add($this->downloadDbCommand->reveal()); + $this->commandTester = new CommandTester($command); } @@ -202,44 +204,56 @@ class LocateVisitsCommandTest extends TestCase $resolveIpLocation->shouldNotHaveBeenCalled(); } - /** - * @test - * @dataProvider provideParams - */ - public function showsProperMessageWhenGeoLiteUpdateFails(bool $olderDbExists, string $expectedMessage): void + /** @test */ + public function showsProperMessageWhenGeoLiteUpdateFails(): void { - $locateVisits = $this->visitService->locateUnlocatedVisits(Argument::cetera())->will(function (): void { - }); - $checkDbUpdate = $this->dbUpdater->checkDbUpdate(Argument::cetera())->will( - function (array $args) use ($olderDbExists): void { - [$mustBeUpdated, $handleProgress] = $args; - - $mustBeUpdated($olderDbExists); - $handleProgress(100, 50); - - throw $olderDbExists - ? GeolocationDbUpdateFailedException::withOlderDb() - : GeolocationDbUpdateFailedException::withoutOlderDb(); - }, - ); + $this->downloadDbCommand->run(Argument::cetera())->willReturn(ExitCodes::EXIT_FAILURE); $this->commandTester->execute([]); $output = $this->commandTester->getDisplay(); - self::assertStringContainsString( - sprintf('%s GeoLite2 db file...', $olderDbExists ? 'Updating' : 'Downloading'), - $output, - ); - self::assertStringContainsString($expectedMessage, $output); - $locateVisits->shouldHaveBeenCalledTimes((int) $olderDbExists); - $checkDbUpdate->shouldHaveBeenCalledOnce(); + self::assertStringContainsString('It is not possible to locate visits without a GeoLite2 db file.', $output); + $this->visitService->locateUnlocatedVisits(Argument::cetera())->shouldNotHaveBeenCalled(); } - public function provideParams(): iterable - { - yield [true, '[Warning] GeoLite2 database update failed. Proceeding with old version.']; - yield [false, 'GeoLite2 database download failed. It is not possible to locate visits.']; - } +// /** +// * @test +// * @dataProvider provideParams +// */ +// public function showsProperMessageWhenGeoLiteUpdateFails(bool $olderDbExists, string $expectedMessage): void +// { +// $locateVisits = $this->visitService->locateUnlocatedVisits(Argument::cetera())->will(function (): void { +// }); +// $checkDbUpdate = $this->dbUpdater->checkDbUpdate(Argument::cetera())->will( +// function (array $args) use ($olderDbExists): void { +// [$mustBeUpdated, $handleProgress] = $args; +// +// $mustBeUpdated($olderDbExists); +// $handleProgress(100, 50); +// +// throw $olderDbExists +// ? GeolocationDbUpdateFailedException::withOlderDb() +// : GeolocationDbUpdateFailedException::withoutOlderDb(); +// }, +// ); +// +// $this->commandTester->execute([]); +// $output = $this->commandTester->getDisplay(); +// +// self::assertStringContainsString( +// sprintf('%s GeoLite2 db file...', $olderDbExists ? 'Updating' : 'Downloading'), +// $output, +// ); +// self::assertStringContainsString($expectedMessage, $output); +// $locateVisits->shouldHaveBeenCalledTimes((int) $olderDbExists); +// $checkDbUpdate->shouldHaveBeenCalledOnce(); +// } +// +// public function provideParams(): iterable +// { +// yield [true, '[Warning] GeoLite2 database update failed. Proceeding with old version.']; +// yield [false, 'GeoLite2 database download failed. It is not possible to locate visits.']; +// } /** @test */ public function providingAllFlagOnItsOwnDisplaysNotice(): void diff --git a/module/CLI/test/Factory/ApplicationFactoryTest.php b/module/CLI/test/Factory/ApplicationFactoryTest.php index ee0793bc..fbb5ace9 100644 --- a/module/CLI/test/Factory/ApplicationFactoryTest.php +++ b/module/CLI/test/Factory/ApplicationFactoryTest.php @@ -6,17 +6,13 @@ namespace ShlinkioTest\Shlink\CLI\Factory; use Laminas\ServiceManager\ServiceManager; use PHPUnit\Framework\TestCase; -use Prophecy\Argument; -use Prophecy\PhpUnit\ProphecyTrait; -use Prophecy\Prophecy\ObjectProphecy; use Shlinkio\Shlink\CLI\Factory\ApplicationFactory; use Shlinkio\Shlink\Core\Options\AppOptions; -use Symfony\Component\Console\Application; -use Symfony\Component\Console\Command\Command; +use ShlinkioTest\Shlink\CLI\CliTestUtilsTrait; class ApplicationFactoryTest extends TestCase { - use ProphecyTrait; + use CliTestUtilsTrait; private ApplicationFactory $factory; @@ -54,17 +50,4 @@ class ApplicationFactoryTest extends TestCase AppOptions::class => new AppOptions(), ]]); } - - private function createCommandMock(string $name): ObjectProphecy - { - $command = $this->prophesize(Command::class); - $command->getName()->willReturn($name); - $command->getDefinition()->willReturn($name); - $command->isEnabled()->willReturn(true); - $command->getAliases()->willReturn([]); - $command->setApplication(Argument::type(Application::class))->willReturn(function (): void { - }); - - return $command; - } }