From 4866fe241e1afe5762ea82814f021fc602884243 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sun, 14 Apr 2019 17:59:22 +0200 Subject: [PATCH] Updated LocateVisitsCommand to update the database if needed --- module/CLI/config/dependencies.config.php | 1 + .../src/Command/Visit/LocateVisitsCommand.php | 67 +++++++++++++++---- .../Command/Visit/LocateVisitsCommandTest.php | 45 ++++++++++++- 3 files changed, 99 insertions(+), 14 deletions(-) diff --git a/module/CLI/config/dependencies.config.php b/module/CLI/config/dependencies.config.php index d296d1aa..b56ff76e 100644 --- a/module/CLI/config/dependencies.config.php +++ b/module/CLI/config/dependencies.config.php @@ -61,6 +61,7 @@ return [ Service\VisitService::class, IpLocationResolverInterface::class, Lock\Factory::class, + GeolocationDbUpdater::class, ], Command\Visit\UpdateDbCommand::class => [DbUpdater::class], diff --git a/module/CLI/src/Command/Visit/LocateVisitsCommand.php b/module/CLI/src/Command/Visit/LocateVisitsCommand.php index 6f34625b..fa458413 100644 --- a/module/CLI/src/Command/Visit/LocateVisitsCommand.php +++ b/module/CLI/src/Command/Visit/LocateVisitsCommand.php @@ -3,7 +3,9 @@ declare(strict_types=1); namespace Shlinkio\Shlink\CLI\Command\Visit; +use Shlinkio\Shlink\CLI\Exception\GeolocationDbUpdateFailedException; use Shlinkio\Shlink\CLI\Util\ExitCodes; +use Shlinkio\Shlink\CLI\Util\GeolocationDbUpdaterInterface; use Shlinkio\Shlink\Common\Exception\WrongIpException; use Shlinkio\Shlink\Common\IpGeolocation\IpLocationResolverInterface; use Shlinkio\Shlink\Common\IpGeolocation\Model\Location; @@ -13,6 +15,7 @@ use Shlinkio\Shlink\Core\Entity\VisitLocation; use Shlinkio\Shlink\Core\Exception\IpCannotBeLocatedException; use Shlinkio\Shlink\Core\Service\VisitServiceInterface; use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Helper\ProgressBar; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; @@ -31,18 +34,25 @@ class LocateVisitsCommand extends Command private $ipLocationResolver; /** @var Locker */ private $locker; - /** @var OutputInterface */ - private $output; + /** @var GeolocationDbUpdaterInterface */ + private $dbUpdater; + + /** @var SymfonyStyle */ + private $io; + /** @var ProgressBar */ + private $progressBar; public function __construct( VisitServiceInterface $visitService, IpLocationResolverInterface $ipLocationResolver, - Locker $locker + Locker $locker, + GeolocationDbUpdaterInterface $dbUpdater ) { parent::__construct(); $this->visitService = $visitService; $this->ipLocationResolver = $ipLocationResolver; $this->locker = $locker; + $this->dbUpdater = $dbUpdater; } protected function configure(): void @@ -55,16 +65,17 @@ class LocateVisitsCommand extends Command protected function execute(InputInterface $input, OutputInterface $output): ?int { - $this->output = $output; - $io = new SymfonyStyle($input, $output); + $this->io = new SymfonyStyle($input, $output); $lock = $this->locker->createLock(self::NAME); if (! $lock->acquire()) { - $io->warning(sprintf('There is already an instance of the "%s" command in execution', self::NAME)); + $this->io->warning(sprintf('There is already an instance of the "%s" command in execution', self::NAME)); return ExitCodes::EXIT_WARNING; } try { + $this->checkDbUpdate(); + $this->visitService->locateUnlocatedVisits( [$this, 'getGeolocationDataForVisit'], function (VisitLocation $location) use ($output) { @@ -76,7 +87,7 @@ class LocateVisitsCommand extends Command } ); - $io->success('Finished processing all IPs'); + $this->io->success('Finished processing all IPs'); } finally { $lock->release(); return ExitCodes::EXIT_SUCCESS; @@ -86,7 +97,7 @@ class LocateVisitsCommand extends Command public function getGeolocationDataForVisit(Visit $visit): Location { if (! $visit->hasRemoteAddr()) { - $this->output->writeln( + $this->io->writeln( 'Ignored visit with no IP address', OutputInterface::VERBOSITY_VERBOSE ); @@ -94,21 +105,51 @@ class LocateVisitsCommand extends Command } $ipAddr = $visit->getRemoteAddr(); - $this->output->write(sprintf('Processing IP %s', $ipAddr)); + $this->io->write(sprintf('Processing IP %s', $ipAddr)); if ($ipAddr === IpAddress::LOCALHOST) { - $this->output->writeln(' [Ignored localhost address]'); + $this->io->writeln(' [Ignored localhost address]'); throw IpCannotBeLocatedException::forLocalhost(); } try { return $this->ipLocationResolver->resolveIpLocation($ipAddr); } catch (WrongIpException $e) { - $this->output->writeln(' [An error occurred while locating IP. Skipped]'); - if ($this->output->isVerbose()) { - $this->getApplication()->renderException($e, $this->output); + $this->io->writeln(' [An error occurred while locating IP. Skipped]'); + if ($this->io->isVerbose()) { + $this->getApplication()->renderException($e, $this->io); } throw IpCannotBeLocatedException::forError($e); } } + + private function checkDbUpdate(): void + { + try { + $this->dbUpdater->checkDbUpdate(function (bool $olderDbExists) { + $this->io->writeln( + sprintf('%s GeoLite2 database...', $olderDbExists ? 'Updating' : 'Downloading') + ); + $this->progressBar = new ProgressBar($this->io); + }, function (int $total, int $downloaded) { + $this->progressBar->setMaxSteps($total); + $this->progressBar->setProgress($downloaded); + }); + + 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.' + ); + } + } } diff --git a/module/CLI/test/Command/Visit/LocateVisitsCommandTest.php b/module/CLI/test/Command/Visit/LocateVisitsCommandTest.php index af536383..573b0c17 100644 --- a/module/CLI/test/Command/Visit/LocateVisitsCommandTest.php +++ b/module/CLI/test/Command/Visit/LocateVisitsCommandTest.php @@ -7,6 +7,8 @@ use PHPUnit\Framework\TestCase; use Prophecy\Argument; use Prophecy\Prophecy\ObjectProphecy; use Shlinkio\Shlink\CLI\Command\Visit\LocateVisitsCommand; +use Shlinkio\Shlink\CLI\Exception\GeolocationDbUpdateFailedException; +use Shlinkio\Shlink\CLI\Util\GeolocationDbUpdaterInterface; use Shlinkio\Shlink\Common\Exception\WrongIpException; use Shlinkio\Shlink\Common\IpGeolocation\IpApiLocationResolver; use Shlinkio\Shlink\Common\IpGeolocation\Model\Location; @@ -36,11 +38,14 @@ class LocateVisitsCommandTest extends TestCase private $locker; /** @var ObjectProphecy */ private $lock; + /** @var ObjectProphecy */ + private $dbUpdater; public function setUp(): void { $this->visitService = $this->prophesize(VisitService::class); $this->ipResolver = $this->prophesize(IpApiLocationResolver::class); + $this->dbUpdater = $this->prophesize(GeolocationDbUpdaterInterface::class); $this->locker = $this->prophesize(Lock\Factory::class); $this->lock = $this->prophesize(Lock\LockInterface::class); @@ -52,7 +57,8 @@ class LocateVisitsCommandTest extends TestCase $command = new LocateVisitsCommand( $this->visitService->reveal(), $this->ipResolver->reveal(), - $this->locker->reveal() + $this->locker->reveal(), + $this->dbUpdater->reveal() ); $app = new Application(); $app->add($command); @@ -182,4 +188,41 @@ class LocateVisitsCommandTest extends TestCase $locateVisits->shouldNotHaveBeenCalled(); $resolveIpLocation->shouldNotHaveBeenCalled(); } + + /** + * @test + * @dataProvider provideParams + */ + public function showsProperMessageWhenGeoLiteUpdateFails(bool $olderDbExists, string $expectedMessage): void + { + $locateVisits = $this->visitService->locateUnlocatedVisits(Argument::cetera())->will(function () { + }); + $checkDbUpdate = $this->dbUpdater->checkDbUpdate(Argument::cetera())->will( + function (array $args) use ($olderDbExists) { + [$mustBeUpdated, $handleProgress] = $args; + + $mustBeUpdated($olderDbExists); + $handleProgress(100, 50); + + throw GeolocationDbUpdateFailedException::create($olderDbExists); + } + ); + + $this->commandTester->execute([]); + $output = $this->commandTester->getDisplay(); + + $this->assertStringContainsString( + sprintf('%s GeoLite2 database...', $olderDbExists ? 'Updating' : 'Downloading'), + $output + ); + $this->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.']; + } }